Skip to content

Commit

Permalink
[Fix] preserve stack traces for returned Promises (async/await)
Browse files Browse the repository at this point in the history
  • Loading branch information
Raynos authored and ljharb committed Dec 31, 2020
1 parent 82c3904 commit d505cdf
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 9 deletions.
7 changes: 6 additions & 1 deletion lib/test.js
Expand Up @@ -16,6 +16,7 @@ var isEnumerable = bind.call(Function.call, Object.prototype.propertyIsEnumerabl
var toLowerCase = bind.call(Function.call, String.prototype.toLowerCase);
var isProto = bind.call(Function.call, Object.prototype.isPrototypeOf);
var $test = bind.call(Function.call, RegExp.prototype.test);
var objectToString = bind.call(Function.call, Object.prototype.toString);

module.exports = Test;

Expand Down Expand Up @@ -111,7 +112,11 @@ Test.prototype.run = function () {
self.end();
}
})['catch'](function onError(err) {
self.fail(err);
if (err instanceof Error || objectToString(err) === '[object Error]') {
self.ifError(err);
} else {
self.fail(err);
}
self.end();
});
return;
Expand Down
44 changes: 42 additions & 2 deletions test/async-await.js
Expand Up @@ -217,9 +217,10 @@ tap.test('async-error', function (t) {
'ok 1 before throw',
'not ok 2 Error: oopsie',
' ---',
' operator: fail',
' operator: error',
' stack: |-',
' Error: Error: oopsie',
' Error: oopsie',
' at Test.myTest ($TEST/async-await/async-error.js:$LINE:$COL)',
' [... stack stripped ...]',
' ...',
'',
Expand All @@ -245,3 +246,42 @@ tap.test('async-error', function (t) {
t.end();
});
});

tap.test('async-bug', function (t) {
runProgram('async-await', 'async-bug.js', function (r) {
var stdout = r.stdout.toString('utf8');
var lines = stdout.split('\n');
lines = lines.filter(function (line) {
return !/^(\s+)at(\s+)<anonymous>$/.test(line);
});
stdout = lines.join('\n');

t.same(stripFullStack(stdout), [
'TAP version 13',
'# async-error',
'ok 1 before throw',
'ok 2 should be strictly equal',
'not ok 3 TypeError: Cannot read property \'length\' of null',
' ---',
' operator: error',
' stack: |-',
' TypeError: Cannot read property \'length\' of null',
' at myCode ($TEST/async-await/async-bug.js:$LINE:$COL)',
' at Test.myTest ($TEST/async-await/async-bug.js:$LINE:$COL)',
' ...',
'',
'1..3',
'# tests 3',
'# pass 2',
'# fail 1',
'',
'',
].join('\n'));
t.same(r.exitCode, 1);

var stderr = r.stderr.toString('utf8');

t.same(stderr, '');
t.end();
});
});
32 changes: 32 additions & 0 deletions test/async-await/async-bug.js
@@ -0,0 +1,32 @@
'use strict';

var test = require('../../');

function myCode(arr) {
let sum = 0;
// oops forgot to handle null
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}

test('async-error', async function myTest(t) {
await sleep(100);
t.ok(true, 'before throw');

const sum = myCode([1, 2, 3]);
t.equal(sum, 6);

const sum2 = myCode(null);
t.equal(sum2, 0);

t.end();
});


function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
28 changes: 22 additions & 6 deletions test/promise_fail.js
Expand Up @@ -20,21 +20,29 @@ tap.test('callback returning rejected promise should cause that test (and only t
}

var strippedString = stripFullStack(rowsString);
var lines = strippedString.split('\n');
lines = lines.filter(function (line) {
return !/^(\s+)at(\s+)(?:Test\.)?<anonymous>(?:$|\s)/.test(line);
});
strippedString = lines.join('\n');

// hack for consistency across all versions of node
// some versions produce a longer stack trace for some reason
// since this doesn't affect the validity of the test, the extra line is removed if present
// the regex just removes the lines "at <anonymous>" and "[... stack stripped ...]" if they occur together
strippedString = strippedString.replace(/.+at <anonymous>\n.+\[\.\.\. stack stripped \.\.\.\]\n/, '');
strippedString = strippedString
.replace(/.+at (?:Test\.)?<anonymous>\n.+\[\.\.\. stack stripped \.\.\.\]\n/g, '')
.replace(/(?:(.+)\[\.\.\. stack stripped \.\.\.\]\n)+/g, '$1[... stack stripped ...]\n');

tt.same(strippedString, [
'TAP version 13',
'# promise',
'not ok 1 Error: rejection message',
' ---',
' operator: fail',
' operator: error',
' stack: |-',
' Error: Error: rejection message',
' Error: rejection message',
' at $TEST/promises/fail.js:$LINE:$COL',
' [... stack stripped ...]',
' ...',
'# after',
Expand Down Expand Up @@ -63,22 +71,30 @@ tap.test('subtest callback returning rejected promise should cause that subtest
}

var strippedString = stripFullStack(rowsString);
var lines = strippedString.split('\n');
lines = lines.filter(function (line) {
return !/^(\s+)at(\s+)(?:Test\.)?<anonymous>(?:$|\s)/.test(line);
});
strippedString = lines.join('\n');

// hack for consistency across all versions of node
// some versions produce a longer stack trace for some reason
// since this doesn't affect the validity of the test, the extra line is removed if present
// the regex just removes the lines "at <anonymous>" and "[... stack stripped ...]" if they occur together
strippedString = strippedString.replace(/.+at <anonymous>\n.+\[\.\.\. stack stripped \.\.\.\]\n/, '');
strippedString = strippedString
.replace(/.+at (?:Test\.)?<anonymous>\n.+\[\.\.\. stack stripped \.\.\.\]\n/, '')
.replace(/(?:(.+)\[\.\.\. stack stripped \.\.\.\]\n)+/g, '$1[... stack stripped ...]\n');

tt.same(strippedString, [
'TAP version 13',
'# promise',
'# sub test that should fail',
'not ok 1 Error: rejection message',
' ---',
' operator: fail',
' operator: error',
' stack: |-',
' Error: Error: rejection message',
' Error: rejection message',
' at $TEST/promises/subTests.js:$LINE:$COL',
' [... stack stripped ...]',
' ...',
'# sub test that should pass',
Expand Down

0 comments on commit d505cdf

Please sign in to comment.