From c4c44ab79450914adccfc27ae0dbceb06091ba4a Mon Sep 17 00:00:00 2001 From: Aldwin Vlasblom Date: Wed, 12 Jun 2019 22:27:47 +0200 Subject: [PATCH] Add support for asynchronous assertions through logging For full history see 73ee48e13fbbf08dae378f6b2c97c1b7dc72b322. --- lib/command.mjs | 15 +- lib/doctest.js | 369 +++++++++++++++++++++++++++++---------- lib/doctest.mjs | 11 +- lib/program.js | 12 ++ test/esm/async.mjs | 7 + test/index.js | 160 +++++++++++++++-- test/shared/async.coffee | 7 + test/shared/async.js | 7 + test/shared/logging.js | 75 ++++++++ 9 files changed, 549 insertions(+), 114 deletions(-) create mode 100644 test/esm/async.mjs create mode 100644 test/shared/async.coffee create mode 100644 test/shared/async.js create mode 100644 test/shared/logging.js diff --git a/lib/command.mjs b/lib/command.mjs index 897d47f1..d5cac60f 100644 --- a/lib/command.mjs +++ b/lib/command.mjs @@ -12,14 +12,15 @@ if (program.args.length === 0) { process.exit (1); } -Promise.all (program.args.map (function(path) { - return (doctest (path, program)).then (function(results) { - return results.reduce (function(status, tuple) { - return tuple[0] ? status : 1; - }, 0); +program.args.reduce (function(promise, path) { + return promise.then (function(ok) { + return (doctest (path, program)).then (function(results) { + return ok && results.every (function(t) { return t[0]; }); + }); }); -})).then (function(statuses) { - process.exit (statuses.every (function(s) { return s === 0; }) ? 0 : 1); +}, Promise.resolve (true)) +.then (function(ok) { + process.exit (ok ? 0 : 1); }, function(err) { process.stderr.write (formatErrors ([err.message])); process.exit (1); diff --git a/lib/doctest.js b/lib/doctest.js index cb87f72c..a689207f 100644 --- a/lib/doctest.js +++ b/lib/doctest.js @@ -12,6 +12,7 @@ var fs = require ('fs'); var pathlib = require ('path'); +var util = require ('util'); var CoffeeScript = require ('coffeescript'); var esprima = require ('esprima'); @@ -31,10 +32,10 @@ function inferType(path) { var rewriters = {coffee: rewrite$coffee, js: rewrite$js}; -function evaluate(moduleType, source, path) { - return moduleType === 'commonjs' ? - commonjsEval (source, path) : - functionEval (source); +function evaluate(options, source, path) { + return options.module === 'commonjs' ? + commonjsEval (options, source, path) : + functionEval (options, source); } module.exports = function(path, options) { @@ -65,6 +66,7 @@ module.exports = function(path, options) { {prefix: options.prefix == null ? '' : options.prefix, openingDelimiter: options.openingDelimiter, closingDelimiter: options.closingDelimiter, + logFunctionNames: options.logFunction, sourceType: 'script'}, common.sanitizeFileContents (fs.readFileSync (path, 'utf8')) ), @@ -75,12 +77,14 @@ module.exports = function(path, options) { console.log (source.replace (/\n$/, '')); return Promise.resolve ([]); } else if (options.silent) { - return Promise.resolve (evaluate (options.module, source, path)); + return evaluate (options, source, path); } else { console.log ('running doctests in ' + path + '...'); - var results = evaluate (options.module, source, path); - log (results); - return Promise.resolve (results); + return (evaluate (options, source, path)) + .then (function(results) { + log (results); + return results; + }); } }; @@ -89,6 +93,14 @@ function indentN(n, s) { return s.replace (/^(?!$)/gm, (Array (n + 1)).join (' ')); } +// last :: NonEmpty (Array x) -> x +function last(xs) { + return xs[xs.length - 1]; +} + +// noop :: a -> Undefined +function noop() {} + // object :: Array (Array2 String Any) -> Object function object(pairs) { return pairs.reduce (function(object, pair) { @@ -102,6 +114,11 @@ function quote(s) { return "'" + s.replace (/'/g, "\\'") + "'"; } +// repeat :: Number -> a -> Array a +function repeat(n, x) { + return (Array (n)).fill (x); +} + // show :: a -> String function show(x) { return Object.prototype.toString.call (x) === '[object Error]' ? @@ -164,23 +181,29 @@ var CLOSED = 'closed'; var OPEN = 'open'; var INPUT = 'input'; var OUTPUT = 'output'; +var LOG = 'log'; + +// isStandardOutput :: { channel :: Nullable String } -> Boolean +function isStandardOutput(output) { + return output.channel == null; +} // normalizeTest :: { output :: { value :: String } } -> Undefined function normalizeTest($test) { - var $output = $test[OUTPUT]; - if ($output != null) { + ($test[OUTPUT].filter (isStandardOutput)).forEach (function($output) { var match = $output.value.match (/^![ ]?([^:]*)(?::[ ]?(.*))?$/); $test['!'] = match != null; if ($test['!']) { $output.value = 'new ' + match[1] + '(' + quote (match[2] || '') + ')'; } - } + }); } function processLine( options, // :: { prefix :: String // , openingDelimiter :: Nullable String - // , closingDelimiter :: Nullable String } + // , closingDelimiter :: Nullable String + // , logFunctionNames :: Array String } accum, // :: { state :: State, tests :: Array Test } line, // :: String input, // :: Test -> Undefined @@ -188,7 +211,7 @@ function processLine( appendToInput, // :: Test -> Undefined appendToOutput // :: Test -> Undefined ) { - var $test, value; + var $test, value, match; var prefix = options.prefix; if (line.slice (0, prefix.length) === prefix) { var trimmedLine = (line.slice (prefix.length)).replace (/^\s*/, ''); @@ -197,20 +220,46 @@ function processLine( } else if (trimmedLine === options.closingDelimiter) { accum.state = CLOSED; } else if (trimmedLine.charAt (0) === '>') { + accum.state = INPUT; value = stripLeading (1, ' ', stripLeading (1, '>', trimmedLine)); accum.tests.push ($test = {}); - $test[accum.state = INPUT] = {value: value}; + $test[INPUT] = {value: value}; + $test[OUTPUT] = []; input ($test); - } else if (trimmedLine.charAt (0) === '.') { + } else if (accum.state === INPUT && trimmedLine.charAt (0) === '.') { value = stripLeading (1, ' ', stripLeading (Infinity, '.', trimmedLine)); - $test = accum.tests[accum.tests.length - 1]; - $test[accum.state].value += '\n' + value; - (accum.state === INPUT ? appendToInput : appendToOutput) ($test); - } else if (accum.state === INPUT) { - value = trimmedLine; - $test = accum.tests[accum.tests.length - 1]; - $test[accum.state = OUTPUT] = {value: value}; - output ($test); + $test = last (accum.tests); + $test[INPUT].value += '\n' + value; + appendToInput ($test); + } else if ((accum.state === OUTPUT || accum.state === LOG) && + (trimmedLine.charAt (0) === '.')) { + value = stripLeading (1, ' ', stripLeading (Infinity, '.', trimmedLine)); + $test = last (accum.tests); + (last ($test[OUTPUT])).value += '\n' + + value; + appendToOutput ($test); + } else if ((match = /^\[(.+?)\]:(.*)$/.exec (trimmedLine)) != null) { + if (options.logFunctionNames.includes (match[1])) { + accum.state = LOG; + value = stripLeading (1, ' ', match[2]); + $test = last (accum.tests); + $test[OUTPUT].push ({ + channel: match[1], + value: value + }); + output ($test); + } + } else if (accum.state === INPUT || accum.state === LOG) { + $test = last (accum.tests); + if (!$test[OUTPUT].some (isStandardOutput)) { + accum.state = OUTPUT; + value = trimmedLine; + $test[OUTPUT].push ({ + channel: null, + value: value + }); + output ($test); + } } } } @@ -242,10 +291,13 @@ function processLine( // . '!': false, // . input: { // . value: '6 * 7', -// . loc: {start: {line: 1, column: 0}, end: {line: 1, column: 10}}}, -// . output: { +// . loc: {start: {line: 1, column: 0}, end: {line: 1, column: 10}} +// . }, +// . output: [{ +// . channel: null, // . value: '42', -// . loc: {start: {line: 2, column: 0}, end: {line: 2, column: 5}}} +// . loc: {start: {line: 2, column: 0}, end: {line: 2, column: 5}} +// . }] // . }] function transformComments(options, comments) { var result = comments.reduce (function(accum, comment, commentIndex) { @@ -268,13 +320,16 @@ function transformComments(options, comments) { }, function($test) { $test.commentIndex = commentIndex; - $test[OUTPUT].loc = {start: start, end: end}; + (last ($test[OUTPUT])).loc = { + start: start, + end: end + }; }, function($test) { $test[INPUT].loc.end = end; }, function($test) { - $test[OUTPUT].loc.end = end; + (last ($test[OUTPUT])).loc.end = end; } ); return accum; @@ -315,46 +370,70 @@ function substring(input, start, end) { ); } -function wrap$js(test, sourceType) { +function wrap$js(test, sourceType, logFunctionNames) { var ast = esprima.parse (test[INPUT].value, {sourceType: sourceType}); var type = ast.body[0].type; return type === 'FunctionDeclaration' || type === 'ImportDeclaration' || type === 'VariableDeclaration' ? test[INPUT].value : + // TODO: Why are we enqueing two things if it could just be one structure? [ '__doctest.enqueue({', ' type: "' + INPUT + '",', - ' thunk: function() {', - ' return ' + test[INPUT].value + ';', + ' thunk: function(' + logFunctionNames.join (', ') + ') {', + ' return (', + indentN (6, test[INPUT].value), + ' );', ' }', '});' - ].concat (test[OUTPUT] == null ? [] : [ + ].concat (test[OUTPUT].length === 0 ? [] : [ '__doctest.enqueue({', ' type: "' + OUTPUT + '",', - ' ":": ' + test[OUTPUT].loc.start.line + ',', ' "!": ' + test['!'] + ',', ' thunk: function() {', - ' return ' + test[OUTPUT].value + ';', + ' return [', + indentN (6, test[OUTPUT].map (function(out) { + return [ + '{', + ' loc: ' + out.loc.start.line + ',', + ' channel: ' + JSON.stringify (out.channel) + ',', + ' value: (', + indentN (4, out.value), + ' )', + '}' + ].join ('\n'); + }).join (',\n')), + ' ];', ' }', '});' ]).join ('\n'); } -function wrap$coffee(test) { +function wrap$coffee(test, logFunctionNames) { return [ '__doctest.enqueue {', ' type: "' + INPUT + '"', - ' thunk: ->', + ' thunk: (' + logFunctionNames.join (', ') + ') ->', indentN (4, test[INPUT].value), '}' - ].concat (test[OUTPUT] == null ? [] : [ + ].concat (test[OUTPUT].length === 0 ? [] : [ '__doctest.enqueue {', ' type: "' + OUTPUT + '"', - ' ":": ' + test[OUTPUT].loc.start.line, ' "!": ' + test['!'], - ' thunk: ->', - indentN (4, test[OUTPUT].value), + ' thunk: -> [', + indentN (4, test[OUTPUT].map (function(out) { + return [ + '{', + ' loc: ' + out.loc.start.line + ',', + ' channel: ' + JSON.stringify (out.channel) + ',', + ' value: (', + indentN (4, out.value), + ' )', + '}' + ].join ('\n'); + }).join ('\n')), + ' ]', '}' ]).join ('\n'); } @@ -397,7 +476,7 @@ function rewrite$js(options, input) { } function wrapCode(js) { - return wrap$js (js, options.sourceType); + return wrap$js (js, options.sourceType, options.logFunctionNames); } // comments :: { Block :: Array Comment, Line :: Array Comment } @@ -410,10 +489,12 @@ function rewrite$js(options, input) { var lineTests = transformComments (options, comments.Line); var chunks = lineTests - .concat ([object ([[INPUT, bookend]])]) + .concat ([object ([[INPUT, bookend], [OUTPUT, []]])]) .reduce (function(accum, test) { accum.chunks.push (substring (input, accum.loc, test[INPUT].loc.start)); - accum.loc = (test[OUTPUT] == null ? test[INPUT] : test[OUTPUT]).loc.end; + accum.loc = ( + test[OUTPUT].length === 0 ? test[INPUT] : last (test[OUTPUT]) + ).loc.end; return accum; }, {chunks: [], loc: {line: 1, column: 0}}) .chunks; @@ -450,7 +531,7 @@ function rewrite$coffee(options, input) { var isComment = /^[ \t]*#(?!##)/.test (line); var current = isComment ? accum.commentChunks : accum.literalChunks; if (isComment === accum.isComment) { - current[current.length - 1].lines.push (line); + (last (current)).lines.push (line); } else { current.push ({lines: [line], loc: {start: {line: idx + 1}}}); } @@ -473,7 +554,7 @@ function rewrite$coffee(options, input) { $test.indent = match[1]; }, function($test) { - $test[OUTPUT].loc = { + (last ($test[OUTPUT])).loc = { start: {line: commentChunk.loc.start.line + idx} }; }, @@ -485,7 +566,10 @@ function rewrite$coffee(options, input) { return result.tests.map (function($test) { normalizeTest ($test); - return indentN ($test.indent.length, wrap$coffee ($test)); + return indentN ( + $test.indent.length, + wrap$coffee ($test, options.logFunctionNames) + ); }); }); @@ -500,7 +584,7 @@ function rewrite$coffee(options, input) { ); } -function functionEval(source) { +function functionEval(options, source) { // Functions created via the Function function are always run in the // global context, which ensures that doctests can't access variables // in _this_ context. @@ -509,64 +593,163 @@ function functionEval(source) { var evaluate = Function ('__doctest', source); var queue = []; evaluate ({enqueue: function(io) { queue.push (io); }}); - return run (queue); + return run (options, queue); } -function commonjsEval(source, path) { +function commonjsEval(options, source, path) { var abspath = (pathlib.resolve (path)).replace (/[.][^.]+$/, '-' + Date.now () + '.js'); - fs.writeFileSync (abspath, source); - var queue; - try { - queue = (require (abspath)).__doctest.queue; - } finally { - fs.unlinkSync (abspath); + function cleanup(f) { + return function(x) { + return (util.promisify (fs.unlink) (abspath)).then (function() { + return f (x); + }); + }; } - return run (queue); + + return (util.promisify (fs.writeFile) (abspath, source)) + .then (function() { + var module = require (abspath); + return run (options, module.__doctest.queue); + }) + .then (cleanup (Promise.resolve.bind (Promise)), + cleanup (Promise.reject.bind (Promise))); } -function run(queue) { - return queue.reduce (function(accum, io) { - var thunk = accum.thunk; - if (io.type === INPUT) { - if (thunk != null) thunk (); - accum.thunk = io.thunk; - } else if (io.type === OUTPUT) { - var either; - try { - either = {tag: 'Right', value: thunk ()}; - } catch (err) { - either = {tag: 'Left', value: err}; +function run(options, queue) { + var logFunctionNames = options.logFunction; + return queue.reduce (function(p, io) { + return p.then (function(accum) { + var thunk = accum.thunk; + + if (io.type === INPUT) { + if (thunk != null) { + thunk.apply (null, repeat (logFunctionNames.length, noop)); + } + accum.thunk = io.thunk; + return accum; } + + var errored; + var expecteds = io.thunk (); + var outputs = []; + accum.thunk = null; - var expected = io.thunk (); - - var pass, repr; - if (either.tag === 'Left') { - var name = either.value.name; - var message = either.value.message; - pass = io['!'] && - name === expected.name && - message === expected.message.replace (/^$/, message); - repr = '! ' + name + - (expected.message && message.replace (/^(?!$)/, ': ')); - } else { - pass = !io['!'] && Z.equals (either.value, expected); - repr = show (either.value); + + return new Promise (function(res) { + var logTimeout; + var open = true; + + function done() { + open = false; + clearTimeout (logTimeout); + res (); + } + + function makeLogFunction(name) { + return function log(value) { + if (open) { + outputs.push ({channel: name, value: value}); + clearTimeout (logTimeout); + logTimeout = setTimeout (done, options.logTimeout); + } + }; + } + + try { + outputs.push ({ + channel: null, + value: thunk.apply (null, logFunctionNames.map (makeLogFunction)) + }); + errored = false; + } catch (err) { + outputs.push ({channel: null, value: err}); + errored = true; + } + + if (logFunctionNames.length === 0) { + done (); + } else { + logTimeout = setTimeout (done, options.logTimeout); + } + }).then (verifyOutput); + + function showError(value) { + return '! ' + value.name + value.message.replace (/^(?!$)/, ': '); } - accum.results.push ([ - pass, - repr, - io['!'] ? - '! ' + expected.name + expected.message.replace (/^(?!$)/, ': ') : - show (expected), - io[':'] - ]); - } - return accum; - }, {results: [], thunk: null}).results; + function showOutput(output) { + return (isStandardOutput (output)) ? + (errored ? showError (output.value) : show (output.value)) : + ('[' + output.channel + ']: ' + show (output.value)); + } + + function showExpected(expected) { + return (isStandardOutput (expected)) ? + (io['!'] ? showError (expected.value) : show (expected.value)) : + ('[' + expected.channel + ']: ' + show (expected.value)); + } + + function verifyOutput() { + outputs.forEach (function(output, idx) { + var expected = expecteds[idx]; + var actualRepr = showOutput (output); + + if (!expected) { + return void accum.results.push ([ + false, + actualRepr, + 'no output', + // TODO: Show location of input once io is a single structure. + '-' + ]); + } + + var pass = false; + var expectedRepr = showExpected (expected); + + if (expected.channel !== output.channel) { + pass = false; + } else if (isStandardOutput (output)) { + if (errored) { + var name = output.value.name; + var message = output.value.message; + pass = io['!'] && + name === expected.value.name && + message === + expected.value.message.replace (/^$/, message); + actualRepr = io['!'] && expected.value.message === '' ? + '! ' + output.value.name : + showOutput (output); + } else { + pass = !io['!'] && Z.equals (output.value, expected.value); + } + } else { + pass = Z.equals (output.value, expected.value); + } + + accum.results.push ([pass, actualRepr, expectedRepr, expected.loc]); + }); + + expecteds + .slice (outputs.length) + .forEach (function(expected) { + accum.results.push ([ + false, + 'no output' + (isStandardOutput (expected) ? '' : ' fast enough'), + showExpected (expected), + expected.loc + ]); + }); + + return accum; + } + + }); + }, Promise.resolve ({results: [], thunk: null})).then (function(reduced) { + return reduced.results; + }); } module.exports.run = run; diff --git a/lib/doctest.mjs b/lib/doctest.mjs index 736062d3..eb129e7f 100644 --- a/lib/doctest.mjs +++ b/lib/doctest.mjs @@ -18,6 +18,7 @@ export default async function(path, options) { {prefix: options.prefix == null ? '' : options.prefix, openingDelimiter: options.openingDelimiter, closingDelimiter: options.closingDelimiter, + logFunctionNames: options.logFunction, sourceType: 'module'}, common.sanitizeFileContents ( await util.promisify (fs.readFile) (path, 'utf8') @@ -29,10 +30,10 @@ export default async function(path, options) { console.log (source.replace (/\n$/, '')); return []; } else if (options.silent) { - return evaluate (source, path); + return evaluate (options, source, path); } else { console.log ('running doctests in ' + path + '...'); - return (evaluate (source, path)).then (function(results) { + return (evaluate (options, source, path)).then (function(results) { doctest.log (results); return results; }); @@ -50,7 +51,7 @@ function wrap(source) { ]); } -function evaluate(source, path) { +function evaluate(options, source, path) { const abspath = (pathlib.resolve (path)).replace (/[.][^.]+$/, '-' + Date.now () + '.mjs'); @@ -64,7 +65,9 @@ function evaluate(source, path) { return (util.promisify (fs.writeFile) (abspath, source)) .then (function() { return import (abspath); }) - .then (function(module) { return doctest.run (module.__doctest.queue); }) + .then (function(module) { + return doctest.run (options, module.__doctest.queue); + }) .then (cleanup (Promise.resolve.bind (Promise)), cleanup (Promise.reject.bind (Promise))); } diff --git a/lib/program.js b/lib/program.js index 97803bcb..7b4b6023 100644 --- a/lib/program.js +++ b/lib/program.js @@ -5,6 +5,10 @@ var program = require ('commander'); var pkg = require ('../package.json'); +function append(x, xs) { + return xs.concat ([x]); +} + program .version (pkg.version) .usage ('[options] path/to/js/or/coffee/module') @@ -18,6 +22,14 @@ program 'specify line preceding doctest block (e.g. "```javascript")') .option (' --closing-delimiter ', 'specify line following doctest block (e.g. "```")') +.option (' --log-function ', + 'expose a log function with the given name to your doctests' + + ' (can be specified multiple times)', + append, + []) +.option (' --log-timeout ', + 'specify an alternative log timeout time (defaults to 100)', + 100) .option ('-p, --print', 'output the rewritten source without running tests') .option ('-s, --silent', diff --git a/test/esm/async.mjs b/test/esm/async.mjs new file mode 100644 index 00000000..9b71a6f1 --- /dev/null +++ b/test/esm/async.mjs @@ -0,0 +1,7 @@ +// > ( stdout (1) +// . , setTimeout (stdout, 1, 2) +// . , stderr (3) ) +// [stdout]: 1 +// [stderr]: 3 +// undefined +// [stdout]: 2 diff --git a/test/index.js b/test/index.js index f18e1745..e00de66a 100644 --- a/test/index.js +++ b/test/index.js @@ -40,10 +40,15 @@ function printResult(actual, expected, message) { } +var defaultOptions = { + logFunction: [] +}; + function testModule(path, options) { var type = (path.split ('.')).pop (); var expecteds = require (pathlib.resolve (path, '..', 'results.json')); - return (doctest (path, options)).then (function(actuals) { + return (doctest (path, Object.assign ({}, defaultOptions, options))) + .then (function(actuals) { for (var idx = 0; idx < expecteds.length; idx += 1) { printResult (actuals[idx], expecteds[idx][1], @@ -199,6 +204,50 @@ testCommand ('bin/doctest --type js test/bin/executable', { stderr: '' }); +testCommand ('bin/doctest --log-function stdout --log-function stderr test/shared/async.js', { + status: 0, + stdout: unlines ([ + 'running doctests in test/shared/async.js...', + '....' + ]), + stderr: '' +}); + +testCommand ('bin/doctest --module commonjs --log-function stdout --log-function stderr test/shared/async.js', { + status: 0, + stdout: unlines ([ + 'running doctests in test/shared/async.js...', + '....' + ]), + stderr: '' +}); + +testCommand ('bin/doctest --log-function stdout --log-function stderr test/shared/async.coffee', { + status: 0, + stdout: unlines ([ + 'running doctests in test/shared/async.coffee...', + '....' + ]), + stderr: '' +}); + +testCommand ('bin/doctest --log-function stdout --log-function stderr test/shared/logging.js', { + status: 1, + stdout: unlines ([ + 'running doctests in test/shared/logging.js...', + '...........xx.xxxx..x..x..', + 'FAIL: expected [stdout]: 2 on line 42 (got 3)', + 'FAIL: expected 3 on line 43 (got no output)', + 'FAIL: expected 3 on line 49 (got [stdout]: 2)', + 'FAIL: expected no output on line - (got 3)', + 'FAIL: expected [stdout]: 2 on line 54 (got [stdout]: 1)', + 'FAIL: expected [stdout]: 1 on line 55 (got [stdout]: 2)', + 'FAIL: expected [stderr]: 2 on line 62 (got [stdout]: 2)', + 'FAIL: expected [stdout]: 1 on line 69 (got no output fast enough)' + ]), + stderr: '' +}); + testCommand ('bin/doctest --module commonjs lib/doctest.js', { status: 0, stdout: unlines ([ @@ -236,21 +285,49 @@ testCommand ('bin/doctest --module esm test/esm/incorrect.mjs', { stderr: '' }); +testCommand ('bin/doctest --module esm test/esm/async.mjs', { + status: 1, + stdout: unlines ([ + 'running doctests in test/esm/async.mjs...', + 'x', + 'FAIL: expected undefined on line 6 (got ! ReferenceError: stdout is not defined)' + ]), + stderr: '' +}); + +testCommand ('bin/doctest --module esm --log-function stdout --log-function stderr test/esm/async.mjs', { + status: 0, + stdout: unlines ([ + 'running doctests in test/esm/async.mjs...', + '....' + ]), + stderr: '' +}); + testCommand ('bin/doctest --print test/commonjs/exports/index.js', { status: 0, stdout: unlines ([ '__doctest.enqueue({', ' type: "input",', ' thunk: function() {', - ' return exports.identity(42);', + ' return (', + ' exports.identity(42)', + ' );', ' }', '});', '__doctest.enqueue({', ' type: "output",', - ' ":": 2,', ' "!": false,', ' thunk: function() {', - ' return 42;', + ' return [', + ' {', + ' loc: 2,', + ' channel: null,', + ' value: (', + ' 42', + ' )', + ' }', + ' ];', ' }', '});', 'exports.identity = function(x) {', @@ -269,15 +346,24 @@ testCommand ('bin/doctest --print --module amd test/amd/index.js', { ' __doctest.enqueue({', ' type: "input",', ' thunk: function() {', - ' return toFahrenheit(0);', + ' return (', + ' toFahrenheit(0)', + ' );', ' }', '});', '__doctest.enqueue({', ' type: "output",', - ' ":": 5,', ' "!": false,', ' thunk: function() {', - ' return 32;', + ' return [', + ' {', + ' loc: 5,', + ' channel: null,', + ' value: (', + ' 32', + ' )', + ' }', + ' ];', ' }', '});', ' function toFahrenheit(degreesCelsius) {', @@ -312,15 +398,69 @@ testCommand ('bin/doctest --print --module commonjs test/commonjs/exports/index. ' __doctest.enqueue({', ' type: "input",', ' thunk: function() {', - ' return exports.identity(42);', + ' return (', + ' exports.identity(42)', + ' );', + ' }', + ' });', + ' __doctest.enqueue({', + ' type: "output",', + ' "!": false,', + ' thunk: function() {', + ' return [', + ' {', + ' loc: 2,', + ' channel: null,', + ' value: (', + ' 42', + ' )', + ' }', + ' ];', + ' }', + ' });', + ' exports.identity = function(x) {', + ' return x;', + ' };', + ' }.call(this);', + '', + ' (module.exports || exports).__doctest = __doctest;', + '}.call(this);' + ]), + stderr: '' +}); + +testCommand ('bin/doctest --print --module commonjs --log-function stdout --log-function stderr test/commonjs/exports/index.js', { + status: 0, + stdout: unlines ([ + 'void function() {', + ' var __doctest = {', + ' require: require,', + ' queue: [],', + ' enqueue: function(io) { this.queue.push(io); }', + ' };', + '', + ' void function() {', + ' __doctest.enqueue({', + ' type: "input",', + ' thunk: function(stdout, stderr) {', + ' return (', + ' exports.identity(42)', + ' );', ' }', ' });', ' __doctest.enqueue({', ' type: "output",', - ' ":": 2,', ' "!": false,', ' thunk: function() {', - ' return 42;', + ' return [', + ' {', + ' loc: 2,', + ' channel: null,', + ' value: (', + ' 42', + ' )', + ' }', + ' ];', ' }', ' });', ' exports.identity = function(x) {', diff --git a/test/shared/async.coffee b/test/shared/async.coffee new file mode 100644 index 00000000..e6546c97 --- /dev/null +++ b/test/shared/async.coffee @@ -0,0 +1,7 @@ +# > stdout 1; +# . setTimeout stdout, 1, 2; +# . stderr 3; +# [stdout]: 1 +# [stderr]: 3 +# undefined +# [stdout]: 2 diff --git a/test/shared/async.js b/test/shared/async.js new file mode 100644 index 00000000..9b71a6f1 --- /dev/null +++ b/test/shared/async.js @@ -0,0 +1,7 @@ +// > ( stdout (1) +// . , setTimeout (stdout, 1, 2) +// . , stderr (3) ) +// [stdout]: 1 +// [stderr]: 3 +// undefined +// [stdout]: 2 diff --git a/test/shared/logging.js b/test/shared/logging.js new file mode 100644 index 00000000..d6b29577 --- /dev/null +++ b/test/shared/logging.js @@ -0,0 +1,75 @@ +function crash(){ + throw new Error; +} + +// Synchronous log with output +// +// > (stdout (1), 2) +// [stdout]: 1 +// 2 + +// Synchronous log with exception +// +// > (stdout (1), crash()) +// [stdout]: 1 +// ! Error + +// Asynchronous log with output +// +// > (setImmediate (stdout, 1), 2) +// 2 +// [stdout]: 1 + +// Asynchronous log with exception +// +// > (setImmediate (stdout, 1), crash()) +// ! Error +// [stdout]: 1 + +// Output on non-existent channels is ignored +// +// > (stdout (1), 2) +// [spam]: "hi" +// [stdout]: 1 +// [spam]: "lalala" +// 2 +// [spam]: "whatever" + +// Failure due to not enough output +// +// > (stdout (1), 3) +// [stdout]: 1 +// [stdout]: 2 +// 3 + +// Failure due to too much output +// +// > (stdout (1), stdout (2), 3) +// [stdout]: 1 +// 3 + +// Failure due to incorrectly ordered output +// +// > (stdout (1), stdout (2), 3) +// [stdout]: 2 +// [stdout]: 1 +// 3 + +// Failure due to output on the wrong channel +// +// > (stdout (1), stdout (2), 3) +// [stdout]: 1 +// [stderr]: 2 +// 3 + +// Failure due to timing out +// +// > (setTimeout (stdout, 120, 1), 2) +// 2 +// [stdout]: 1 + +// Success after a previous timeout +// +// > (setTimeout (stdout, 80, 1), 2) +// 2 +// [stdout]: 1