Skip to content

Commit 8d3f03a

Browse files
Raynosljharb
authored andcommitted
[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.
1 parent c3924d3 commit 8d3f03a

File tree

11 files changed

+400
-1
lines changed

11 files changed

+400
-1
lines changed

.eslintrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@
88
"named": "never",
99
}],
1010
},
11+
"overrides": [
12+
{
13+
"files": ["test/async-await/*"],
14+
"parserOptions": {
15+
"ecmaVersion": 2017,
16+
},
17+
},
18+
],
1119
}

lib/test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,23 @@ Test.prototype.run = function () {
9191
if (this._timeout != null) {
9292
this.timeoutAfter(this._timeout);
9393
}
94-
this._cb(this);
94+
95+
var callbackReturn = this._cb(this);
96+
97+
if (
98+
typeof Promise === 'function' &&
99+
callbackReturn &&
100+
typeof callbackReturn.then === 'function' &&
101+
typeof callbackReturn.catch === 'function'
102+
) {
103+
callbackReturn.catch(function onError(err) {
104+
nextTick(function rethrowError() {
105+
throw err
106+
})
107+
})
108+
return
109+
}
110+
95111
this.emit('run');
96112
};
97113

test/async-await.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
var tap = require('tap');
2+
3+
var stripFullStack = require('./common').stripFullStack;
4+
var runProgram = require('./common').runProgram;
5+
6+
var nodeVersion = process.versions.node;
7+
var majorVersion = nodeVersion.split('.')[0];
8+
9+
if (Number(majorVersion) < 8) {
10+
process.exit(0);
11+
}
12+
13+
tap.test('async1', function (t) {
14+
runProgram('async-await', 'async1.js', function (r) {
15+
t.same(r.stdout.toString('utf8'), [
16+
'TAP version 13',
17+
'# async1',
18+
'ok 1 before await',
19+
'ok 2 after await',
20+
'',
21+
'1..2',
22+
'# tests 2',
23+
'# pass 2',
24+
'',
25+
'# ok'
26+
].join('\n') + '\n\n');
27+
t.same(r.exitCode, 0);
28+
t.same(r.stderr.toString('utf8'), '');
29+
t.end();
30+
});
31+
});
32+
33+
tap.test('async2', function (t) {
34+
runProgram('async-await', 'async2.js', function (r) {
35+
var stdout = r.stdout.toString('utf8');
36+
var lines = stdout.split('\n');
37+
lines = lines.filter(function (line) {
38+
return ! /^(\s+)at(\s+)<anonymous>$/.test(line);
39+
});
40+
stdout = lines.join('\n');
41+
42+
t.same(stripFullStack(stdout), [
43+
'TAP version 13',
44+
'# async2',
45+
'ok 1 before await',
46+
'not ok 2 after await',
47+
' ---',
48+
' operator: ok',
49+
' expected: true',
50+
' actual: false',
51+
' at: Test.myTest ($TEST/async-await/async2.js:$LINE:$COL)',
52+
' stack: |-',
53+
' Error: after await',
54+
' [... stack stripped ...]',
55+
' at Test.myTest ($TEST/async-await/async2.js:$LINE:$COL)',
56+
' ...',
57+
'',
58+
'1..2',
59+
'# tests 2',
60+
'# pass 1',
61+
'# fail 1'
62+
].join('\n') + '\n\n');
63+
t.same(r.exitCode, 1);
64+
t.same(r.stderr.toString('utf8'), '');
65+
t.end();
66+
});
67+
});
68+
69+
tap.test('async3', function (t) {
70+
runProgram('async-await', 'async3.js', function (r) {
71+
t.same(r.stdout.toString('utf8'), [
72+
'TAP version 13',
73+
'# async3',
74+
'ok 1 before await',
75+
'ok 2 after await',
76+
'',
77+
'1..2',
78+
'# tests 2',
79+
'# pass 2',
80+
'',
81+
'# ok'
82+
].join('\n') + '\n\n');
83+
t.same(r.exitCode, 0);
84+
t.same(r.stderr.toString('utf8'), '');
85+
t.end();
86+
});
87+
});
88+
89+
tap.test('async4', function (t) {
90+
runProgram('async-await', 'async4.js', function (r) {
91+
t.same(stripFullStack(r.stdout.toString('utf8')), [
92+
'TAP version 13',
93+
'# async4',
94+
'ok 1 before await',
95+
'not ok 2 Error: oops',
96+
' ---',
97+
' operator: error',
98+
' expected: |-',
99+
' undefined',
100+
' actual: |-',
101+
' [Error: oops]',
102+
' at: Test.myTest ($TEST/async-await/async4.js:$LINE:$COL)',
103+
' stack: |-',
104+
' Error: oops',
105+
' at Timeout.myTimeout [as _onTimeout] ($TEST/async-await/async4.js:$LINE:$COL)',
106+
' [... stack stripped ...]',
107+
' ...',
108+
'',
109+
'1..2',
110+
'# tests 2',
111+
'# pass 1',
112+
'# fail 1'
113+
].join('\n') + '\n\n');
114+
t.same(r.exitCode, 1);
115+
t.same(r.stderr.toString('utf8'), '');
116+
t.end();
117+
});
118+
});
119+
120+
tap.test('async5', function (t) {
121+
runProgram('async-await', 'async5.js', function (r) {
122+
t.same(r.stdout.toString('utf8'), [
123+
'TAP version 13',
124+
'# async5',
125+
'ok 1 before server',
126+
'ok 2 after server',
127+
'ok 3 before request',
128+
'ok 4 after request',
129+
'ok 5 should be equal',
130+
'ok 6 should be equal',
131+
'ok 7 undefined',
132+
'',
133+
'1..7',
134+
'# tests 7',
135+
'# pass 7',
136+
'',
137+
'# ok'
138+
].join('\n') + '\n\n');
139+
t.same(r.exitCode, 0);
140+
t.same(r.stderr.toString('utf8'), '');
141+
t.end();
142+
});
143+
});
144+
145+
tap.test('sync-error', function (t) {
146+
runProgram('async-await', 'sync-error.js', function (r) {
147+
t.same(stripFullStack(r.stdout.toString('utf8')), [
148+
'TAP version 13',
149+
'# sync-error',
150+
'ok 1 before throw',
151+
''
152+
].join('\n'));
153+
t.same(r.exitCode, 1);
154+
155+
var stderr = r.stderr.toString('utf8');
156+
var lines = stderr.split('\n');
157+
lines = lines.filter(function (line) {
158+
return ! /\(timers.js:/.test(line) &&
159+
! /\(internal\/timers.js:/.test(line) &&
160+
! /Immediate\.next/.test(line);
161+
});
162+
stderr = lines.join('\n');
163+
164+
t.same(stripFullStack(stderr), [
165+
'$TEST/async-await/sync-error.js:5',
166+
' throw new Error(\'oopsie\');',
167+
' ^',
168+
'',
169+
'Error: oopsie',
170+
' at Test.myTest ($TEST/async-await/sync-error.js:$LINE:$COL)',
171+
' at Test.bound [as _cb] ($TAPE/lib/test.js:$LINE:$COL)',
172+
' at Test.run ($TAPE/lib/test.js:$LINE:$COL)',
173+
' at Test.bound [as run] ($TAPE/lib/test.js:$LINE:$COL)',
174+
''
175+
].join('\n'));
176+
t.end();
177+
});
178+
});
179+
180+
tap.test('async-error', function (t) {
181+
runProgram('async-await', 'async-error.js', function (r) {
182+
var stdout = r.stdout.toString('utf8');
183+
var lines = stdout.split('\n');
184+
lines = lines.filter(function (line) {
185+
return ! /^(\s+)at(\s+)<anonymous>$/.test(line);
186+
});
187+
stdout = lines.join('\n');
188+
189+
t.same(stripFullStack(stdout.toString('utf8')), [
190+
'TAP version 13',
191+
'# async-error',
192+
'ok 1 before throw',
193+
''
194+
].join('\n'));
195+
t.same(r.exitCode, 1);
196+
197+
var stderr = r.stderr.toString('utf8');
198+
var lines = stderr.split('\n');
199+
lines = lines.filter(function (line) {
200+
return ! /\(timers.js:/.test(line) &&
201+
! /\(internal\/timers.js:/.test(line) &&
202+
! /Immediate\.next/.test(line);
203+
});
204+
stderr = lines.join('\n');
205+
206+
t.same(stripFullStack(stderr), [
207+
'$TAPE/lib/test.js:106',
208+
' throw err',
209+
' ^',
210+
'',
211+
'Error: oopsie',
212+
' at Test.myTest ($TEST/async-await/async-error.js:$LINE:$COL)',
213+
' at Test.bound [as _cb] ($TAPE/lib/test.js:$LINE:$COL)',
214+
' at Test.run ($TAPE/lib/test.js:$LINE:$COL)',
215+
' at Test.bound [as run] ($TAPE/lib/test.js:$LINE:$COL)',
216+
''
217+
].join('\n'));
218+
t.end();
219+
});
220+
});

test/async-await/async-error.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var test = require('../../');
2+
3+
test('async-error', async function myTest(t) {
4+
t.ok(true, 'before throw');
5+
throw new Error('oopsie');
6+
t.ok(true, 'after throw');
7+
t.end();
8+
});

test/async-await/async1.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
var test = require('../../');
2+
3+
test('async1', async function myTest(t) {
4+
try {
5+
t.ok(true, 'before await');
6+
await new Promise((resolve) => {
7+
setTimeout(resolve, 10);
8+
});
9+
t.ok(true, 'after await');
10+
t.end();
11+
} catch (err) {
12+
t.ifError(err);
13+
t.end();
14+
}
15+
});

test/async-await/async2.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
var test = require('../../');
2+
3+
test('async2', async function myTest(t) {
4+
try {
5+
t.ok(true, 'before await');
6+
await new Promise((resolve) => {
7+
setTimeout(resolve, 10);
8+
});
9+
t.ok(false, 'after await');
10+
t.end();
11+
} catch (err) {
12+
t.ifError(err);
13+
t.end();
14+
}
15+
});

test/async-await/async3.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var test = require('../../');
2+
3+
test('async3', async function myTest(t) {
4+
t.ok(true, 'before await');
5+
await new Promise((resolve) => {
6+
setTimeout(resolve, 10);
7+
});
8+
t.ok(true, 'after await');
9+
t.end();
10+
});

test/async-await/async4.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
var test = require('../../');
2+
3+
test('async4', async function myTest(t) {
4+
try {
5+
t.ok(true, 'before await');
6+
await new Promise((resolve, reject) => {
7+
setTimeout(function myTimeout() {
8+
reject(new Error('oops'));
9+
}, 10);
10+
});
11+
t.ok(true, 'after await');
12+
t.end();
13+
} catch (err) {
14+
t.ifError(err);
15+
t.end();
16+
}
17+
});

test/async-await/async5.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
var util = require('util');
2+
var http = require('http');
3+
4+
var test = require('../../');
5+
6+
test('async5', async function myTest(t) {
7+
try {
8+
t.ok(true, 'before server');
9+
10+
var mockDb = { state: 'old' };
11+
var server = http.createServer(function (req, res) {
12+
res.end('OK');
13+
14+
// Pretend we write to the DB and it takes time.
15+
setTimeout(function () {
16+
mockDb.state = 'new';
17+
}, 10);
18+
});
19+
20+
await util.promisify(function (cb) {
21+
server.listen(0, cb);
22+
})();
23+
24+
t.ok(true, 'after server');
25+
26+
t.ok(true, 'before request');
27+
28+
var res = await util.promisify(function (cb) {
29+
var req = http.request({
30+
hostname: 'localhost',
31+
port: server.address().port,
32+
path: '/',
33+
method: 'GET'
34+
}, function (res) {
35+
cb(null, res);
36+
});
37+
req.end();
38+
})();
39+
40+
t.ok(true, 'after request');
41+
42+
res.resume();
43+
t.equal(res.statusCode, 200);
44+
45+
setTimeout(function () {
46+
t.equal(mockDb.state, 'new');
47+
48+
server.close(function (err) {
49+
t.ifError(err);
50+
t.end();
51+
});
52+
}, 50);
53+
} catch (err) {
54+
t.ifError(err);
55+
t.end();
56+
}
57+
});

test/async-await/sync-error.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
var test = require('../../');
2+
3+
test('sync-error', function myTest(t) {
4+
t.ok(true, 'before throw');
5+
throw new Error('oopsie');
6+
t.ok(true, 'after throw');
7+
t.end();
8+
});

0 commit comments

Comments
 (0)