Skip to content

Instantly share code, notes, and snippets.

@stoyan
Last active August 30, 2018 16:06
Show Gist options
  • Save stoyan/c981af5ff4b85aa584c8527a0ae77b70 to your computer and use it in GitHub Desktop.
Save stoyan/c981af5ff4b85aa584c8527a0ae77b70 to your computer and use it in GitHub Desktop.
Script to lint and unit-test a JavaScript book written in AsciiDoc

Markup and conventions

The code in a chapter is wrapped like so

[source,javascript]
----
// code here
----

To unit test, add the expected value after a comment, followed by ::, like so:

[source,javascript]
----
let somevar;
somevar === undefined; //:: true
----

The //:: turns into just // in the cleanup script and that reads fine in the final version, like:

let somevar;
somevar === undefined; // true

NaN is a special case as it doesn't equal itself:

[source,javascript]
----
1 * undefined; //:: NaN
----

Errors can be unit-tested too, so long as they are not parsing errors:

[source,javascript]
----
foo; //:: ReferenceError:: foo is not defined
----

Extra comments for the book version can be added after ,, and the unit testing ignores them, like:

[source,javascript]
----
let s = "100";
typeof s; //:: "string"
s = s * 1; //:: 100,, you can also use `s *= 1`

... which after the cleanup script reads nicely, like:

let s = "100";
typeof s; // "string"
s = s * 1; // 100 you can also use `s *= 1`

////-- means do not test this snippet, it's untestable, e.g. a parsing error:

[source,javascript]
----////--
const hello = 1;
let hello;
----

//-- means that the code snippet depends on the previous, so test them together, e.g.:

[source,javascript]
----
let a = 5;
a += 3; //:: 8
----

You can continue:

[source,javascript]
----//--
a -= 3; //:: 5
----

++-- means don't add "use strict"; to the snippet being tested, e.g.:

[source,javascript]
----++--
var a = 012;
a === 10; //:: true
----

/*nolint*/ means this snippet fails to lint, but we're ok with that:

[source,javascript]
----++--
/*nolint*/
var a = 012;
a === 10; //:: true
----
// cleanup the extra markup to make it valid AsciiDoc
const clean = require('fs').readFileSync('2.primitive.asciidoc').toString().split('\n').filter(line => {
if (line.indexOf('/*nolint*/') === 0 ||
line.indexOf('/*global') === 0 ||
line.indexOf('/*jshint') === 0) {
return false;
}
return true;
})
.join('\n')
.replace(/--\+\+--/g, '--')
.replace(/--\/\/--/g, '--')
.replace(/--\/\/\/\/--/g, '--')
.replace(/\/\/::/g, '//')
.replace(/Error::/g, 'Error:')
.replace(/,,/g, ',')
;
console.log(clean);
const assert = require('assert');
const fs = require('fs');
const CLIEngine = require('eslint').CLIEngine;
const cli = new CLIEngine({
'parserOptions': {
'ecmaVersion': 6,
},
rules: {
'no-unused-vars': 'off',
}
});
// buncha vars
let snip, rawsnip, start = false, skipping = false, collecting = false;
let passed = 0, skipped = 0;
let lints = 0, nolints = 0;
// short
const log = console.log;
// read the chapter one line at a time
fs.readFileSync('2.primitive.asciidoc').toString().split('\n').forEach((src, num) => {
num++;
line = src.trim();
// collect JS snippets
if (line === '[source,javascript]') {
collecting = start = true;
return;
}
if (start) {
start = false;
if (line.indexOf('////') > 0) {
skipping = true;
return;
}
if (line.indexOf('--//--') === -1) { // new snippet
// --//-- signals this snippet depends on the previous
// used for longer snippets that are broken up by explanation
snip = line.indexOf('++--') === -1 ? '"use strict";\n' : ''; // ++ says "dont use strict"
rawsnip = snip; // we want to lint both raw and instrumented snippets
// so far they are the same
}
return;
}
if (collecting || skipping) {
if (line.indexOf('----') === 0) { // end of snippet
if (skipping) {
collecting = skipping = false;
skipped++;
return;
}
if (snip.indexOf('/*nolint*/') === -1) {
lint(rawsnip);
lints++;
} else {
nolints++;
}
// run the snippet
exec(snip);
collecting = false;
} else if (!skipping){ // yet another line, instrument and add to the snippet
snip += prep(src, num) + '\n';
rawsnip += src + '\n';
}
}
});
// Add asserts
function prep(l, n) {
const parts = l.split(/;\s*\/\/::/); // "//::" separates expression to execute from its result
const nonspace = parts[0].match(/\S/);
const spaces = nonspace === null ? "" : Array(nonspace.index + 1).join(" ");
parts[0] = parts[0].trim();
if (parts[1]) {
const r = parts[1].split(/\s*(,,|::)\s*/)[0].trim(); // the result may have ,, or ::, ignore what's on the right
// e.g. //:: true,, of course!
// e.g. //:: ReferenceError::Invalid whatever
if (r.indexOf('Error') !== -1) {
// expect //:: Error to throw
return `${spaces}assert.throws(function () {${parts[0]};}, ${r}, "error line #${n}");`;
}
if (r === 'NaN') {
// special NaN case
return `${spaces}assert(isNaN(${parts[0]}), true, "error line #${n}");`
}
// usual
return `${spaces}assert.deepEqual(${parts[0]}, ${r}, "error line #${n}");`;
}
return l;
}
// run a snippet
function exec(snip) {
// muck some stuff up and zap log()
const mock = 'function define(){}; function alert(){}; console.log = function(){};';
try {
eval(snip + mock);
passed++;
} catch (e) {
log('------');
log(snip);
log('------');
log(e.message);
process.exit();
}
}
// lint a snippet
function lint(snip) {
const report = cli.executeOnText(snip, 'foo.js').results[0];
if (report.errorCount !== 0) {
log('------');
log(snip);
log('------');
log(report.messages);
process.exit();
}
}
// report
log(`passed: ${passed}, skipped: ${skipped}`);
log(`linted: ${lints}, nolints: ${nolints}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment