From 8d3f03afede1fef1e73d3ce2ce9ff2c7c2e47319 Mon Sep 17 00:00:00 2001 From: Raynos Date: Thu, 20 Jun 2019 13:39:18 +0200 Subject: [PATCH 1/3] [Breaking] support exceptions in async functions This change makes tape work the same with synchronous and asynchronous functions. ``` test('my test', () => { throw new Error('oopsie') }) test('my async test', async () => { throw new Error('oopsie') }) ``` These two cases now have the same semantics which means you can safely use an async function because the unhandled rejection will be converted into a thrown exception. Failing a test when the return promise rejected will fail a test that was probably silently broken previously. Extra test cases have been added to reflect real world usage of tape with async functions which we preferably do not want to break. --- .eslintrc | 8 ++ lib/test.js | 18 ++- test/async-await.js | 220 ++++++++++++++++++++++++++++++++ test/async-await/async-error.js | 8 ++ test/async-await/async1.js | 15 +++ test/async-await/async2.js | 15 +++ test/async-await/async3.js | 10 ++ test/async-await/async4.js | 17 +++ test/async-await/async5.js | 57 +++++++++ test/async-await/sync-error.js | 8 ++ test/common.js | 25 ++++ 11 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 test/async-await.js create mode 100644 test/async-await/async-error.js create mode 100644 test/async-await/async1.js create mode 100644 test/async-await/async2.js create mode 100644 test/async-await/async3.js create mode 100644 test/async-await/async4.js create mode 100644 test/async-await/async5.js create mode 100644 test/async-await/sync-error.js diff --git a/.eslintrc b/.eslintrc index cc09afbc..85fac0f0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,4 +8,12 @@ "named": "never", }], }, + "overrides": [ + { + "files": ["test/async-await/*"], + "parserOptions": { + "ecmaVersion": 2017, + }, + }, + ], } diff --git a/lib/test.js b/lib/test.js index 398a68e6..43fe539a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -91,7 +91,23 @@ Test.prototype.run = function () { if (this._timeout != null) { this.timeoutAfter(this._timeout); } - this._cb(this); + + var callbackReturn = this._cb(this); + + if ( + typeof Promise === 'function' && + callbackReturn && + typeof callbackReturn.then === 'function' && + typeof callbackReturn.catch === 'function' + ) { + callbackReturn.catch(function onError(err) { + nextTick(function rethrowError() { + throw err + }) + }) + return + } + this.emit('run'); }; diff --git a/test/async-await.js b/test/async-await.js new file mode 100644 index 00000000..a5083ad7 --- /dev/null +++ b/test/async-await.js @@ -0,0 +1,220 @@ +var tap = require('tap'); + +var stripFullStack = require('./common').stripFullStack; +var runProgram = require('./common').runProgram; + +var nodeVersion = process.versions.node; +var majorVersion = nodeVersion.split('.')[0]; + +if (Number(majorVersion) < 8) { + process.exit(0); +} + +tap.test('async1', function (t) { + runProgram('async-await', 'async1.js', function (r) { + t.same(r.stdout.toString('utf8'), [ + 'TAP version 13', + '# async1', + 'ok 1 before await', + 'ok 2 after await', + '', + '1..2', + '# tests 2', + '# pass 2', + '', + '# ok' + ].join('\n') + '\n\n'); + t.same(r.exitCode, 0); + t.same(r.stderr.toString('utf8'), ''); + t.end(); + }); +}); + +tap.test('async2', function (t) { + runProgram('async-await', 'async2.js', function (r) { + var stdout = r.stdout.toString('utf8'); + var lines = stdout.split('\n'); + lines = lines.filter(function (line) { + return ! /^(\s+)at(\s+)$/.test(line); + }); + stdout = lines.join('\n'); + + t.same(stripFullStack(stdout), [ + 'TAP version 13', + '# async2', + 'ok 1 before await', + 'not ok 2 after await', + ' ---', + ' operator: ok', + ' expected: true', + ' actual: false', + ' at: Test.myTest ($TEST/async-await/async2.js:$LINE:$COL)', + ' stack: |-', + ' Error: after await', + ' [... stack stripped ...]', + ' at Test.myTest ($TEST/async-await/async2.js:$LINE:$COL)', + ' ...', + '', + '1..2', + '# tests 2', + '# pass 1', + '# fail 1' + ].join('\n') + '\n\n'); + t.same(r.exitCode, 1); + t.same(r.stderr.toString('utf8'), ''); + t.end(); + }); +}); + +tap.test('async3', function (t) { + runProgram('async-await', 'async3.js', function (r) { + t.same(r.stdout.toString('utf8'), [ + 'TAP version 13', + '# async3', + 'ok 1 before await', + 'ok 2 after await', + '', + '1..2', + '# tests 2', + '# pass 2', + '', + '# ok' + ].join('\n') + '\n\n'); + t.same(r.exitCode, 0); + t.same(r.stderr.toString('utf8'), ''); + t.end(); + }); +}); + +tap.test('async4', function (t) { + runProgram('async-await', 'async4.js', function (r) { + t.same(stripFullStack(r.stdout.toString('utf8')), [ + 'TAP version 13', + '# async4', + 'ok 1 before await', + 'not ok 2 Error: oops', + ' ---', + ' operator: error', + ' expected: |-', + ' undefined', + ' actual: |-', + ' [Error: oops]', + ' at: Test.myTest ($TEST/async-await/async4.js:$LINE:$COL)', + ' stack: |-', + ' Error: oops', + ' at Timeout.myTimeout [as _onTimeout] ($TEST/async-await/async4.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', + '', + '1..2', + '# tests 2', + '# pass 1', + '# fail 1' + ].join('\n') + '\n\n'); + t.same(r.exitCode, 1); + t.same(r.stderr.toString('utf8'), ''); + t.end(); + }); +}); + +tap.test('async5', function (t) { + runProgram('async-await', 'async5.js', function (r) { + t.same(r.stdout.toString('utf8'), [ + 'TAP version 13', + '# async5', + 'ok 1 before server', + 'ok 2 after server', + 'ok 3 before request', + 'ok 4 after request', + 'ok 5 should be equal', + 'ok 6 should be equal', + 'ok 7 undefined', + '', + '1..7', + '# tests 7', + '# pass 7', + '', + '# ok' + ].join('\n') + '\n\n'); + t.same(r.exitCode, 0); + t.same(r.stderr.toString('utf8'), ''); + t.end(); + }); +}); + +tap.test('sync-error', function (t) { + runProgram('async-await', 'sync-error.js', function (r) { + t.same(stripFullStack(r.stdout.toString('utf8')), [ + 'TAP version 13', + '# sync-error', + 'ok 1 before throw', + '' + ].join('\n')); + t.same(r.exitCode, 1); + + var stderr = r.stderr.toString('utf8'); + var lines = stderr.split('\n'); + lines = lines.filter(function (line) { + return ! /\(timers.js:/.test(line) && + ! /\(internal\/timers.js:/.test(line) && + ! /Immediate\.next/.test(line); + }); + stderr = lines.join('\n'); + + t.same(stripFullStack(stderr), [ + '$TEST/async-await/sync-error.js:5', + ' throw new Error(\'oopsie\');', + ' ^', + '', + 'Error: oopsie', + ' at Test.myTest ($TEST/async-await/sync-error.js:$LINE:$COL)', + ' at Test.bound [as _cb] ($TAPE/lib/test.js:$LINE:$COL)', + ' at Test.run ($TAPE/lib/test.js:$LINE:$COL)', + ' at Test.bound [as run] ($TAPE/lib/test.js:$LINE:$COL)', + '' + ].join('\n')); + t.end(); + }); +}); + +tap.test('async-error', function (t) { + runProgram('async-await', 'async-error.js', function (r) { + var stdout = r.stdout.toString('utf8'); + var lines = stdout.split('\n'); + lines = lines.filter(function (line) { + return ! /^(\s+)at(\s+)$/.test(line); + }); + stdout = lines.join('\n'); + + t.same(stripFullStack(stdout.toString('utf8')), [ + 'TAP version 13', + '# async-error', + 'ok 1 before throw', + '' + ].join('\n')); + t.same(r.exitCode, 1); + + var stderr = r.stderr.toString('utf8'); + var lines = stderr.split('\n'); + lines = lines.filter(function (line) { + return ! /\(timers.js:/.test(line) && + ! /\(internal\/timers.js:/.test(line) && + ! /Immediate\.next/.test(line); + }); + stderr = lines.join('\n'); + + t.same(stripFullStack(stderr), [ + '$TAPE/lib/test.js:106', + ' throw err', + ' ^', + '', + 'Error: oopsie', + ' at Test.myTest ($TEST/async-await/async-error.js:$LINE:$COL)', + ' at Test.bound [as _cb] ($TAPE/lib/test.js:$LINE:$COL)', + ' at Test.run ($TAPE/lib/test.js:$LINE:$COL)', + ' at Test.bound [as run] ($TAPE/lib/test.js:$LINE:$COL)', + '' + ].join('\n')); + t.end(); + }); +}); diff --git a/test/async-await/async-error.js b/test/async-await/async-error.js new file mode 100644 index 00000000..bf40f7aa --- /dev/null +++ b/test/async-await/async-error.js @@ -0,0 +1,8 @@ +var test = require('../../'); + +test('async-error', async function myTest(t) { + t.ok(true, 'before throw'); + throw new Error('oopsie'); + t.ok(true, 'after throw'); + t.end(); +}); diff --git a/test/async-await/async1.js b/test/async-await/async1.js new file mode 100644 index 00000000..8138dbf9 --- /dev/null +++ b/test/async-await/async1.js @@ -0,0 +1,15 @@ +var test = require('../../'); + +test('async1', async function myTest(t) { + try { + t.ok(true, 'before await'); + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + t.ok(true, 'after await'); + t.end(); + } catch (err) { + t.ifError(err); + t.end(); + } +}); diff --git a/test/async-await/async2.js b/test/async-await/async2.js new file mode 100644 index 00000000..07b52546 --- /dev/null +++ b/test/async-await/async2.js @@ -0,0 +1,15 @@ +var test = require('../../'); + +test('async2', async function myTest(t) { + try { + t.ok(true, 'before await'); + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + t.ok(false, 'after await'); + t.end(); + } catch (err) { + t.ifError(err); + t.end(); + } +}); diff --git a/test/async-await/async3.js b/test/async-await/async3.js new file mode 100644 index 00000000..0f045b0e --- /dev/null +++ b/test/async-await/async3.js @@ -0,0 +1,10 @@ +var test = require('../../'); + +test('async3', async function myTest(t) { + t.ok(true, 'before await'); + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + t.ok(true, 'after await'); + t.end(); +}); diff --git a/test/async-await/async4.js b/test/async-await/async4.js new file mode 100644 index 00000000..923861dc --- /dev/null +++ b/test/async-await/async4.js @@ -0,0 +1,17 @@ +var test = require('../../'); + +test('async4', async function myTest(t) { + try { + t.ok(true, 'before await'); + await new Promise((resolve, reject) => { + setTimeout(function myTimeout() { + reject(new Error('oops')); + }, 10); + }); + t.ok(true, 'after await'); + t.end(); + } catch (err) { + t.ifError(err); + t.end(); + } +}); diff --git a/test/async-await/async5.js b/test/async-await/async5.js new file mode 100644 index 00000000..390dab1b --- /dev/null +++ b/test/async-await/async5.js @@ -0,0 +1,57 @@ +var util = require('util'); +var http = require('http'); + +var test = require('../../'); + +test('async5', async function myTest(t) { + try { + t.ok(true, 'before server'); + + var mockDb = { state: 'old' }; + var server = http.createServer(function (req, res) { + res.end('OK'); + + // Pretend we write to the DB and it takes time. + setTimeout(function () { + mockDb.state = 'new'; + }, 10); + }); + + await util.promisify(function (cb) { + server.listen(0, cb); + })(); + + t.ok(true, 'after server'); + + t.ok(true, 'before request'); + + var res = await util.promisify(function (cb) { + var req = http.request({ + hostname: 'localhost', + port: server.address().port, + path: '/', + method: 'GET' + }, function (res) { + cb(null, res); + }); + req.end(); + })(); + + t.ok(true, 'after request'); + + res.resume(); + t.equal(res.statusCode, 200); + + setTimeout(function () { + t.equal(mockDb.state, 'new'); + + server.close(function (err) { + t.ifError(err); + t.end(); + }); + }, 50); + } catch (err) { + t.ifError(err); + t.end(); + } +}); diff --git a/test/async-await/sync-error.js b/test/async-await/sync-error.js new file mode 100644 index 00000000..77d7b2e7 --- /dev/null +++ b/test/async-await/sync-error.js @@ -0,0 +1,8 @@ +var test = require('../../'); + +test('sync-error', function myTest(t) { + t.ok(true, 'before throw'); + throw new Error('oopsie'); + t.ok(true, 'after throw'); + t.end(); +}); diff --git a/test/common.js b/test/common.js index 39b12400..1461d149 100644 --- a/test/common.js +++ b/test/common.js @@ -1,4 +1,6 @@ var path = require('path'); +var spawn = require('child_process').spawn; +var concat = require('concat-stream'); var yaml = require('js-yaml'); module.exports.getDiag = function (body) { @@ -61,3 +63,26 @@ module.exports.stripFullStack = function (output) { return deduped.join('\n'); }; + +module.exports.runProgram = function (folderName, fileName, cb) { + var result = { + stdout: null, + stderr: null, + exitCode: 0 + }; + var ps = spawn(process.execPath, [ + path.join(__dirname, folderName, fileName) + ]); + + ps.stdout.pipe(concat(function (stdoutRows) { + result.stdout = stdoutRows; + })); + ps.stderr.pipe(concat(function (stderrRows) { + result.stderr = stderrRows; + })); + + ps.on('exit', function (code) { + result.exitCode = code; + cb(result); + }); +}; From f248610eedc9e7236e7e6a2c4a5c0d4415dcde95 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sun, 23 Jun 2019 23:40:17 -0700 Subject: [PATCH 2/3] [Breaking] if a test callback returns a rejected thenable, fail the test. Also, implicitly call `.end()` if not already called when a Promise is returned, because the promise itself marks the end of the test. --- lib/test.js | 19 ++++++++++-------- test/async-await.js | 35 +++++++++++++++++---------------- test/async-await/async-error.js | 1 - test/async-await/async1.js | 2 -- test/async-await/async2.js | 2 -- test/async-await/async3.js | 1 - test/async-await/async4.js | 2 -- test/async-await/async5.js | 16 ++++++++------- 8 files changed, 38 insertions(+), 40 deletions(-) diff --git a/lib/test.js b/lib/test.js index 43fe539a..b24d97ef 100644 --- a/lib/test.js +++ b/lib/test.js @@ -97,15 +97,18 @@ Test.prototype.run = function () { if ( typeof Promise === 'function' && callbackReturn && - typeof callbackReturn.then === 'function' && - typeof callbackReturn.catch === 'function' + typeof callbackReturn.then === 'function' ) { - callbackReturn.catch(function onError(err) { - nextTick(function rethrowError() { - throw err - }) - }) - return + var self = this; + Promise.resolve(callbackReturn).then(function onResolve() { + if (!self.calledEnd) { + self.end(); + } + }).catch(function onError(err) { + self.fail(err); + self.end(); + }); + return; } this.emit('run'); diff --git a/test/async-await.js b/test/async-await.js index a5083ad7..fffd38fd 100644 --- a/test/async-await.js +++ b/test/async-await.js @@ -128,11 +128,10 @@ tap.test('async5', function (t) { 'ok 4 after request', 'ok 5 should be equal', 'ok 6 should be equal', - 'ok 7 undefined', '', - '1..7', - '# tests 7', - '# pass 7', + '1..6', + '# tests 6', + '# pass 6', '', '# ok' ].join('\n') + '\n\n'); @@ -190,7 +189,20 @@ tap.test('async-error', function (t) { 'TAP version 13', '# async-error', 'ok 1 before throw', - '' + 'not ok 2 Error: oopsie', + ' ---', + ' operator: fail', + ' stack: |-', + ' Error: Error: oopsie', + ' [... stack stripped ...]', + ' ...', + '', + '1..2', + '# tests 2', + '# pass 1', + '# fail 1', + '', + '', ].join('\n')); t.same(r.exitCode, 1); @@ -203,18 +215,7 @@ tap.test('async-error', function (t) { }); stderr = lines.join('\n'); - t.same(stripFullStack(stderr), [ - '$TAPE/lib/test.js:106', - ' throw err', - ' ^', - '', - 'Error: oopsie', - ' at Test.myTest ($TEST/async-await/async-error.js:$LINE:$COL)', - ' at Test.bound [as _cb] ($TAPE/lib/test.js:$LINE:$COL)', - ' at Test.run ($TAPE/lib/test.js:$LINE:$COL)', - ' at Test.bound [as run] ($TAPE/lib/test.js:$LINE:$COL)', - '' - ].join('\n')); + t.same(stderr, ''); t.end(); }); }); diff --git a/test/async-await/async-error.js b/test/async-await/async-error.js index bf40f7aa..b2d68b62 100644 --- a/test/async-await/async-error.js +++ b/test/async-await/async-error.js @@ -4,5 +4,4 @@ test('async-error', async function myTest(t) { t.ok(true, 'before throw'); throw new Error('oopsie'); t.ok(true, 'after throw'); - t.end(); }); diff --git a/test/async-await/async1.js b/test/async-await/async1.js index 8138dbf9..819768db 100644 --- a/test/async-await/async1.js +++ b/test/async-await/async1.js @@ -7,9 +7,7 @@ test('async1', async function myTest(t) { setTimeout(resolve, 10); }); t.ok(true, 'after await'); - t.end(); } catch (err) { t.ifError(err); - t.end(); } }); diff --git a/test/async-await/async2.js b/test/async-await/async2.js index 07b52546..4be6f76e 100644 --- a/test/async-await/async2.js +++ b/test/async-await/async2.js @@ -7,9 +7,7 @@ test('async2', async function myTest(t) { setTimeout(resolve, 10); }); t.ok(false, 'after await'); - t.end(); } catch (err) { t.ifError(err); - t.end(); } }); diff --git a/test/async-await/async3.js b/test/async-await/async3.js index 0f045b0e..5ca0e4cf 100644 --- a/test/async-await/async3.js +++ b/test/async-await/async3.js @@ -6,5 +6,4 @@ test('async3', async function myTest(t) { setTimeout(resolve, 10); }); t.ok(true, 'after await'); - t.end(); }); diff --git a/test/async-await/async4.js b/test/async-await/async4.js index 923861dc..9fd4ed30 100644 --- a/test/async-await/async4.js +++ b/test/async-await/async4.js @@ -9,9 +9,7 @@ test('async4', async function myTest(t) { }, 10); }); t.ok(true, 'after await'); - t.end(); } catch (err) { t.ifError(err); - t.end(); } }); diff --git a/test/async-await/async5.js b/test/async-await/async5.js index 390dab1b..6ceb8baf 100644 --- a/test/async-await/async5.js +++ b/test/async-await/async5.js @@ -42,14 +42,16 @@ test('async5', async function myTest(t) { res.resume(); t.equal(res.statusCode, 200); - setTimeout(function () { - t.equal(mockDb.state, 'new'); + await new Promise(function (resolve, reject) { + setTimeout(function () { + t.equal(mockDb.state, 'new'); - server.close(function (err) { - t.ifError(err); - t.end(); - }); - }, 50); + server.close(function (err) { + if (err) { reject(err); } + else { resolve(); } + }); + }, 50); + }); } catch (err) { t.ifError(err); t.end(); From 197019c78c0e452852806f330e573f5023eba91c Mon Sep 17 00:00:00 2001 From: Raynos Date: Tue, 2 Jul 2019 14:24:35 +0200 Subject: [PATCH 3/3] [Tests] update tests for more async/await cases --- test/async-await.js | 24 +++++++++++++++++------- test/async-await/async1.js | 1 + test/async-await/async5.js | 16 +++++++--------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/test/async-await.js b/test/async-await.js index fffd38fd..95d8fe33 100644 --- a/test/async-await.js +++ b/test/async-await.js @@ -119,7 +119,7 @@ tap.test('async4', function (t) { tap.test('async5', function (t) { runProgram('async-await', 'async5.js', function (r) { - t.same(r.stdout.toString('utf8'), [ + t.same(stripFullStack(r.stdout.toString('utf8')), [ 'TAP version 13', '# async5', 'ok 1 before server', @@ -128,14 +128,24 @@ tap.test('async5', function (t) { 'ok 4 after request', 'ok 5 should be equal', 'ok 6 should be equal', + 'ok 7 undefined', + 'not ok 8 .end() called twice', + ' ---', + ' operator: fail', + ' at: Server. ($TEST/async-await/async5.js:$LINE:$COL)', + ' stack: |-', + ' Error: .end() called twice', + ' [... stack stripped ...]', + ' at Server. ($TEST/async-await/async5.js:$LINE:$COL)', + ' [... stack stripped ...]', + ' ...', '', - '1..6', - '# tests 6', - '# pass 6', - '', - '# ok' + '1..8', + '# tests 8', + '# pass 7', + '# fail 1' ].join('\n') + '\n\n'); - t.same(r.exitCode, 0); + t.same(r.exitCode, 1); t.same(r.stderr.toString('utf8'), ''); t.end(); }); diff --git a/test/async-await/async1.js b/test/async-await/async1.js index 819768db..e64bec8a 100644 --- a/test/async-await/async1.js +++ b/test/async-await/async1.js @@ -7,6 +7,7 @@ test('async1', async function myTest(t) { setTimeout(resolve, 10); }); t.ok(true, 'after await'); + t.end(); } catch (err) { t.ifError(err); } diff --git a/test/async-await/async5.js b/test/async-await/async5.js index 6ceb8baf..390dab1b 100644 --- a/test/async-await/async5.js +++ b/test/async-await/async5.js @@ -42,16 +42,14 @@ test('async5', async function myTest(t) { res.resume(); t.equal(res.statusCode, 200); - await new Promise(function (resolve, reject) { - setTimeout(function () { - t.equal(mockDb.state, 'new'); + setTimeout(function () { + t.equal(mockDb.state, 'new'); - server.close(function (err) { - if (err) { reject(err); } - else { resolve(); } - }); - }, 50); - }); + server.close(function (err) { + t.ifError(err); + t.end(); + }); + }, 50); } catch (err) { t.ifError(err); t.end();