While finishing off this book, which I choose to write in AsciiDoc format, I really appreciate the time I took to write myself a handy little script for testing the little code examples. Because, something always gets wrong in the final steps of editing and is nice to have some automated assurance that you didn't break something stupid.
AsciiDoc or MarkDown or some other text setup is how technical books should be written. Or any books, really. Having clear plain text lets you focus on the content and not wrestle with Word formatting. And it's also easy to parse and test.
Code in AsciiDoc
The code blocks in AsciiDoc are easy to spot. They look like:
[source,js]
----------------------------------------------------------------------
var a = 0xff;
a === 255; //:: true
----------------------------------------------------------------------
Testing and linting
All you have to do in a little unit testing utility is to extract these clearly defined blocks and run them to make sure there are no silly syntax errors, creeping in in the very last edits.
But why stop with a syntax check? Why not also run jslint and also instrument and run the code and see any expected values. Asserts, in testspeak.
In my setup I did just that: lint with jshint, instrument the places where I know the expected values, lint these too (why the hell not?), run them and assert the expected values.
Output
Here's how the output of the utility looks like:
$ node scripts/test.js
passed: 362, skipped: 43
linted: 317, nolints: 45
Since this is a book with some educational bad examples, not everything can be linted. Or run for that matter.
JSHint
I choose JSHint over JSLint as it allows more flexibility to relax the rules. Also it's a node module I can import.
var jslint = require('jshint').JSHINT;
These are my lint options:
var lintopts = {
indent: 2,
trailing: true,
white: true,
plusplus: false,
browser: true,
node: true,
expr: true,
loopfunc: true,
newcap: false,
proto: true,
};
And the lint function that lints a snippet of code:
function lint(snip) {
if (!jslint(snip, lintopts, {assert: false})) {
log('------');
log(snip);
log('------');
log(jslint.errors[0]);
process.exit();
}
}
Instrumenting and executing a snippet
Execution is simple:
function exec(snip) {
var mock = "function define(){}; function alert(){}; console.log = function(){};";
try {
eval(snip + mock);
passed++;
} catch (e) {
log('------');
log(snip);
log('------');
log(e.message);
process.exit();
}
}
The instrumentation is a little more interesting.
I often write snippets like:
// assign
var a = 1;
// test
a; // 1
Now I need a little bit of marker that will tell the instrumentation that a;
is a piece of code to execute and 1
is the expected value. I came up with //::
So the above becomes:
var a = 1;
a; //:: 1
I also like to add some more explanation besides the returned value. I use ,,
for that. So:
var a = 1;
a; //:: 1,, as you'd expect
Instrumented, this becomes:
var a = 1;
assert.deepEqual("a;", 1, "Error line #2");
The line is the line in the book with all code and prose, so it's easy to find.
The special markup like //::
and ,,
gets stripped by another script that I run before commit.
A few other features are: support for NaN
which doesn't deepEqual to anything and expecting errors with assert.throws(..)
So I can write and test code like:
sum(21, 21); //:: 42
plum(21, 21); //:: Error:: plum() is not defined
(:: that follow Error is the same as ,,)
Also:
Number("3,14"); //:: NaN
So here's the code instrumentation:
function prep(l, n) {
var parts = l.split(/;\s*\/\/::/);
var nonspace = parts[0].match(/\S/);
var spaces = nonspace === null ? "" : Array(nonspace.index + 1).join(" ");
parts[0] = parts[0].trim();
if (parts[1]) {
var r = parts[1].split(/\s*(,,|::)\s*/)[0].trim();
if (r.indexOf('Error') !== -1) {
return spaces + 'assert.throws(function () {' + parts[0] + '; }, ' + r + ', "error line #' + n + '");';
}
if (r === 'NaN') {
return spaces + 'assert(isNaN(' + parts[0] + '), true, "error line #' + n + '");'
}
return spaces + 'assert.deepEqual(' + parts[0] + ', ' + r + ', "error line #' + n + '");';
}
return l;
}
Main
Dependencies, locals, options and the main parser loop is how it all begins/ends:
var assert = require('assert');
var fs = require('fs');
var jslint = require('jshint').JSHINT;
var snip, rawsnip.....;
var log = console.log;
var lintopts = {
indent: 2,
};
fs.readFileSync('book.asc').toString().split('\n').forEach(function(src, num) {
});
There are a few additional features at snippet-level:
Ability to continue from a previous snippet using --//-- at the top of the snippet delimiter
Let's declare a variable:
[source,js]
----------------------------------------------------------------------
var a = 1;
----------------------------------------------------------------------
And then another one:
[source,js]
--------------------------------------------------------------------//--
var b = 2;
----------------------------------------------------------------------
And let's sum
[source,js]
--------------------------------------------------------------------//--
a + b; //:: 3
----------------------------------------------------------------------
Ability to skip a non-working snippet using ////
[source,js]
----------------------------------------------------------------------////--
var 1v; // invalid
----------------------------------------------------------------------
Ability to run in non-strict mode (because strict is default) using ++
[source,js]
----------------------------------------------------------------------++--
var a = 012;
a === 10; //:: true
----------------------------------------------------------------------
nolint option
[source,js]
----------------------------------------------------------------------
/*nolint*/
assoc["one"]; //:: 1
----------------------------------------------------------------------
Cleanup before commit
Cleaning up all instrumentation markers and hints for the lint and the tests (gist):
var clean = require('fs').readFileSync('book.asc').toString().split('\n').filter(function(line) {
if (line.indexOf('/*nolint*/') === 0 || line.indexOf('/*global') === 0) {
return false;
}
return true;
})
.join('\n')
.replace(/--\+\+--/g, '--')
.replace(/--\/\/--/g, '--')
.replace(/--\/\/\/\/--/g, '--');
console.log(clean);
Github gist
Here's the test.js script in its entirety.