Skip to content

Commit 0356639

Browse files
committed
Add support for asynchronous assertions through logging
1 parent db28686 commit 0356639

File tree

3 files changed

+111
-58
lines changed

3 files changed

+111
-58
lines changed

lib/doctest.js

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ module.exports = function(path, options) {
7575
console.log (source.replace (/\n$/, ''));
7676
return Promise.resolve ([]);
7777
} else if (options.silent) {
78-
return Promise.resolve (evaluate (options.module, source, path));
78+
return evaluate (options.module, source, path);
7979
} else {
8080
console.log ('running doctests in ' + path + '...');
81-
var results = evaluate (options.module, source, path);
82-
log (results);
83-
return Promise.resolve (results);
81+
return (evaluate (options.module, source, path))
82+
.then (function(results) {
83+
log (results);
84+
return results;
85+
});
8486
}
8587
};
8688

@@ -164,6 +166,9 @@ var CLOSED = 'closed';
164166
var OPEN = 'open';
165167
var INPUT = 'input';
166168
var OUTPUT = 'output';
169+
var LOG = 'log';
170+
171+
var MATCH_LOG = /^\[([a-zA-Z]+)\]:/;
167172

168173
// normalizeTest :: { output :: { value :: String } } -> Undefined
169174
function normalizeTest($test) {
@@ -201,16 +206,36 @@ function processLine(
201206
accum.tests.push ($test = {});
202207
$test[accum.state = INPUT] = {value: value};
203208
input ($test);
204-
} else if (trimmedLine.charAt (0) === '.') {
209+
} else if (accum.state === INPUT && trimmedLine.charAt (0) === '.') {
210+
value = stripLeading (1, ' ', stripLeading (Infinity, '.', trimmedLine));
211+
$test = accum.tests[accum.tests.length - 1];
212+
$test[INPUT].value += '\n' + value;
213+
appendToInput ($test);
214+
} else if (accum.state === OUTPUT && trimmedLine.charAt (0) === '.') {
205215
value = stripLeading (1, ' ', stripLeading (Infinity, '.', trimmedLine));
206216
$test = accum.tests[accum.tests.length - 1];
207-
$test[accum.state].value += '\n' + value;
208-
(accum.state === INPUT ? appendToInput : appendToOutput) ($test);
209-
} else if (accum.state === INPUT) {
217+
$test[OUTPUT][$test[OUTPUT].length - 1].value += '\n' + value;
218+
appendToOutput ($test);
219+
} else if (MATCH_LOG.test (trimmedLine)) {
220+
value = stripLeading (1, ' ', trimmedLine.replace (MATCH_LOG, ''));
221+
$test = accum.tests[accum.tests.length - 1];
222+
($test[accum.state = OUTPUT] = $test[accum.state] || []).push ({
223+
channel: MATCH_LOG.exec (trimmedLine)[1],
224+
value: value
225+
});
226+
if ($test[OUTPUT].length === 1) {
227+
output ($test);
228+
}
229+
} else {
210230
value = trimmedLine;
211231
$test = accum.tests[accum.tests.length - 1];
212-
$test[accum.state = OUTPUT] = {value: value};
213-
output ($test);
232+
($test[accum.state = OUTPUT] = $test[accum.state] || []).push ({
233+
channel: null,
234+
value: value
235+
});
236+
if ($test[OUTPUT].length === 1) {
237+
output ($test);
238+
}
214239
}
215240
}
216241
}
@@ -335,7 +360,9 @@ function wrap$js(test, sourceType) {
335360
' ":": ' + test[OUTPUT].loc.start.line + ',',
336361
' "!": ' + test['!'] + ',',
337362
' thunk: function() {',
338-
' return ' + test[OUTPUT].value + ';',
363+
' return ' + test[OUTPUT].map (function(out) {
364+
return '{channel: "' + out.channel + '", value: ' + out.value + '}';
365+
}) + ';',
339366
' }',
340367
'});'
341368
]).join ('\n');
@@ -526,47 +553,59 @@ function commonjsEval(source, path) {
526553
return run (queue);
527554
}
528555

529-
function run(queue) {
530-
return queue.reduce (function(accum, io) {
531-
var thunk = accum.thunk;
532-
if (io.type === INPUT) {
533-
if (thunk != null) thunk ();
534-
accum.thunk = io.thunk;
535-
} else if (io.type === OUTPUT) {
536-
var either;
537-
try {
538-
either = {tag: 'Right', value: thunk ()};
539-
} catch (err) {
540-
either = {tag: 'Left', value: err};
541-
}
542-
accum.thunk = null;
543-
var expected = io.thunk ();
544-
545-
var pass, repr;
546-
if (either.tag === 'Left') {
547-
var name = either.value.name;
548-
var message = either.value.message;
549-
pass = io['!'] &&
550-
name === expected.name &&
551-
message === expected.message.replace (/^$/, message);
552-
repr = '! ' + name +
553-
(expected.message && message.replace (/^(?!$)/, ': '));
554-
} else {
555-
pass = !io['!'] && Z.equals (either.value, expected);
556-
repr = show (either.value);
557-
}
556+
function run(queue, logMediator) {
557+
return queue.reduce (function(p, io) {
558+
return p.then (function(accum) {
559+
var thunk = accum.thunk;
560+
if (io.type === INPUT) {
561+
if (thunk != null) thunk ();
562+
accum.thunk = io.thunk;
563+
} else if (io.type === OUTPUT) {
564+
var either;
565+
var expected = io.thunk ();
566+
567+
accum.thunk = null;
568+
569+
// Instead of calling the io thunk straight away, we register
570+
// the appropriate listener on logMediator beforehand, to catch any
571+
// synchronous log calls the thunk might make.
572+
try {
573+
either = {tag: 'Right', value: thunk ()};
574+
} catch (err) {
575+
either = {tag: 'Left', value: err};
576+
}
558577

559-
accum.results.push ([
560-
pass,
561-
repr,
562-
io['!'] ?
563-
'! ' + expected.name + expected.message.replace (/^(?!$)/, ': ') :
564-
show (expected),
565-
io[':']
566-
]);
567-
}
568-
return accum;
569-
}, {results: [], thunk: null}).results;
578+
// Instead of just analyzing a single output on a single channel,
579+
// we compare output on al channels, in the order indicated by the
580+
// user.
581+
var pass, repr;
582+
if (either.tag === 'Left') {
583+
var name = either.value.name;
584+
var message = either.value.message;
585+
pass = io['!'] &&
586+
name === expected.name &&
587+
message === expected.message.replace (/^$/, message);
588+
repr = '! ' + name +
589+
(expected.message && message.replace (/^(?!$)/, ': '));
590+
} else {
591+
pass = !io['!'] && Z.equals (either.value, expected);
592+
repr = show (either.value);
593+
}
594+
595+
accum.results.push ([
596+
pass,
597+
repr,
598+
io['!'] ?
599+
'! ' + expected.name + expected.message.replace (/^(?!$)/, ': ') :
600+
show (expected),
601+
io[':']
602+
]);
603+
}
604+
return accum;
605+
});
606+
}, Promise.resolve ({results: [], thunk: null})).then (function(reduced) {
607+
return reduced.results;
608+
});
570609
}
571610

572611
module.exports.run = run;

lib/doctest.mjs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export default async function(path, options) {
2222
common.sanitizeFileContents (
2323
await util.promisify (fs.readFile) (path, 'utf8')
2424
)
25-
)
25+
),
26+
options.logFunction
2627
);
2728

2829
if (options.print) {
@@ -39,15 +40,20 @@ export default async function(path, options) {
3940
}
4041
}
4142

42-
function wrap(source) {
43+
function wrap(source, logFunction) {
4344
return common.unlines ([
4445
'export const __doctest = {',
4546
' queue: [],',
46-
' enqueue: function(io) { this.queue.push(io); }',
47+
' enqueue: function(io) { this.queue.push(io); },',
48+
' logMediator: {emit: function(){}}',
4749
'};',
48-
'',
49-
source
50-
]);
50+
''
51+
]) + (logFunction != null ? common.unlines ([
52+
'const ' + logFunction + ' = tag => value => {',
53+
' __doctest.logMediator.emit ({tag, value});',
54+
'};',
55+
''
56+
]) : []) + (source);
5157
}
5258

5359
function evaluate(source, path) {
@@ -64,7 +70,10 @@ function evaluate(source, path) {
6470

6571
return (util.promisify (fs.writeFile) (abspath, source))
6672
.then (function() { return import (abspath); })
67-
.then (function(module) { return doctest.run (module.__doctest.queue); })
73+
.then (function(module) {
74+
return doctest.run (module.__doctest.queue,
75+
module.__doctest.logMediator);
76+
})
6877
.then (cleanup (Promise.resolve.bind (Promise)),
6978
cleanup (Promise.reject.bind (Promise)));
7079
}

lib/program.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ program
1818
'specify line preceding doctest block (e.g. "```javascript")')
1919
.option (' --closing-delimiter <delimiter>',
2020
'specify line following doctest block (e.g. "```")')
21+
.option (' --log-function <name>',
22+
'enable log output assertions')
23+
.option (' --log-timeout <milliseconds>',
24+
'specify an alternative log timeout time (defaults to 100)',
25+
100)
2126
.option ('-p, --print',
2227
'output the rewritten source without running tests')
2328
.option ('-s, --silent',

0 commit comments

Comments
 (0)