Skip to content

Commit

Permalink
[Fix] handle native ESM URLs in at:
Browse files Browse the repository at this point in the history
Fixes #601
  • Loading branch information
ljharb committed Jan 24, 2024
1 parent 1e50cb3 commit 6a5df50
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 19 deletions.
26 changes: 11 additions & 15 deletions lib/test.js
Expand Up @@ -486,9 +486,8 @@ Test.prototype._assert = function assert(ok, opts) {

for (var i = 0; i < err.length; i++) {
/*
Stack trace lines may resemble one of the following. We need
to correctly extract a function name (if any) and path / line
number for each line.
Stack trace lines may resemble one of the following.
We need to correctly extract a function name (if any) and path / line number for each line.
at myFunction (/path/to/file.js:123:45)
at myFunction (/path/to/file.other-ext:123:45)
Expand All @@ -499,28 +498,25 @@ Test.prototype._assert = function assert(ok, opts) {
at Test.bound [as run] (/path/to/file.js:123:45)
at /path/to/file.js:123:45
Regex has three parts. First is non-capturing group for 'at '
(plus anything preceding it).
Regex has three parts. First is non-capturing group for 'at ' (plus anything preceding it).
/^(?:[^\s]*\s*\bat\s+)/
Second captures function call description (optional). This is
not necessarily a valid JS function name, but just what the
stack trace is using to represent a function call. It may look
like `<anonymous>` or 'Test.bound [as run]'.
Second captures function call description (optional).
This is not necessarily a valid JS function name, but just what the stack trace is using to represent a function call.
It may look like `<anonymous>` or 'Test.bound [as run]'.
For our purposes, we assume that, if there is a function
name, it's everything leading up to the first open
parentheses (trimmed) before our pathname.
For our purposes, we assume that, if there is a function name, it's everything leading up to the first open parentheses (trimmed) before our pathname.
/(?:(.*)\s+\()?/
Last part captures file path plus line no (and optional
column no).
Last part captures file path plus line no (and optional column no).
/((?:[/\\]|[a-zA-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)\)?/
In the future, if node supports more ESM URL protocols than `file`, the `file:` below will need to be expanded.
*/
var re = /^(?:[^\s]*\s*\bat\s+)(?:(.*)\s+\()?((?:[/\\]|[a-zA-Z]:\\)[^:)]+:(\d+)(?::(\d+))?)\)?$/;
var re = /^(?:[^\s]*\s*\bat\s+)(?:(.*)\s+\()?((?:[/\\]|[a-zA-Z]:\\|file:\/\/)[^:)]+:(\d+)(?::(\d+))?)\)?$/;
// first tokenize the PWD, then tokenize tape
var lineWithTokens = $replace(
$replace(
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -61,6 +61,7 @@
"eslint": "=8.8.0",
"falafel": "^2.2.5",
"intl-fallback-symbol": "^1.0.0",
"is-core-module": "^2.13.1",
"jackspeak": "=2.1.1",
"js-yaml": "^3.14.0",
"npm-run-posix-or-windows": "^2.0.2",
Expand Down
6 changes: 5 additions & 1 deletion test/async-await.js
Expand Up @@ -12,7 +12,8 @@ if (Number(majorVersion) < 8) {
process.exit(0); // eslint-disable-line no-process-exit
}

var node17 = Number(majorVersion) >= 17;
var node15 = Number(majorVersion) >= 15;
var node17 = node15 && Number(majorVersion) >= 17;

var lengthMessage = 'Cannot read property \'length\' of null';
try {
Expand Down Expand Up @@ -211,6 +212,9 @@ tap.test('sync-error', function (t) {
'Error: oopsie',
' at Test.myTest ($TEST/async-await/sync-error.js:$LINE:$COL)',
' at Test.run ($TAPE/lib/test.js:$LINE:$COL)',
node15 ? [
' at processImmediate (timers:$LINE:$COL)'
] : [],
node17 ? [
'',
'Node.js ' + process.version
Expand Down
21 changes: 18 additions & 3 deletions test/common.js
Expand Up @@ -44,19 +44,34 @@ var stripChangingData = function (line) {
var withoutPathSep = withoutPackageDir.replace(new RegExp('\\' + path.sep, 'g'), '/');
var withoutLineNumbers = withoutPathSep.replace(/:\d+:\d+/g, ':$LINE:$COL');
var withoutNestedLineNumbers = withoutLineNumbers.replace(/, <anonymous>:\$LINE:\$COL\)$/, ')');
return withoutNestedLineNumbers;
var withoutProcessImmediate = withoutNestedLineNumbers.replace(
/^(\s+)at (?:process\.)?(processImmediate|startup\.processNextTick\.process\._tickCallback) (?:\[as _immediateCallback\] )?\((node:internal\/timers|(?:internal\/)?timers\.js|node\.js):\$LINE:\$COL\)$/g,
'$1at processImmediate (timers:$$LINE:$$COL)'
);
var withNormalizedInternals = withoutProcessImmediate
.replace(/^(\s+)at Test\.assert \[as _assert\]/g, '$1at Test.assert')
.replace(/^(\s+)at (?:Object\.|Immediate\.)?next (?:\[as _onImmediate\] )?/g, '$1at Immediate.next ');

if (
(/^\s+at tryOnImmediate \(timers\.js:\$LINE:\$COL\)$/g).test(withNormalizedInternals) // node 5 - 10
|| (/^\s+at runCallback \(timers\.js:\$LINE:\$COL\)$/g).test(withNormalizedInternals) // node 6 - 10
) {
return null;
}
return withNormalizedInternals;
};
module.exports.stripChangingData = stripChangingData;

module.exports.stripFullStack = function (output) {
var stripped = ' [... stack stripped ...]';
var withDuplicates = output.split(/\r?\n/g).map(stripChangingData).map(function (line) {
var m = line.match(/[ ]{8}at .*\((.*)\)/);
var m = typeof line === 'string' && line.match(/[ ]{8}at .*\((.*)\)/);

if (m && m[1].slice(0, 5) !== '$TEST') {
return stripped;
}
return line;
});
}).filter(function (line) { return typeof line === 'string'; });

var withoutInternals = withDuplicates.filter(function (line) {
return !line.match(/ \(node:[^)]+\)$/);
Expand Down
108 changes: 108 additions & 0 deletions test/stackTrace.js
Expand Up @@ -2,8 +2,12 @@

var tape = require('../');
var tap = require('tap');
var spawn = require('child_process').spawn;
var url = require('url');
var concat = require('concat-stream');
var tapParser = require('tap-parser');
var assign = require('object.assign');
var hasDynamicImport = require('has-dynamic-import');
var common = require('./common');

var getDiag = common.getDiag;
Expand All @@ -12,6 +16,10 @@ function stripAt(body) {
return body.replace(/^\s*at:\s+Test.*$\n/m, '');
}

function isString(x) {
return typeof x === 'string';
}

tap.test('preserves stack trace with newlines', function (tt) {
tt.plan(3);

Expand Down Expand Up @@ -288,3 +296,103 @@ tap.test('preserves stack trace for failed assertions where actual===falsy', fun
t.equal(false, true, 'false should be true');
});
});

function spawnTape(args, options) {
var bin = __dirname + '/../bin/tape';

return spawn(process.execPath, [bin].concat(args.split(' ')), assign({ cwd: __dirname }, options));
}

function processRows(rows) {
return (typeof rows === 'string' ? rows.split('\n') : rows).map(common.stripChangingData).filter(isString).join('\n');
}

tap.test('CJS vs ESM: `at`', function (tt) {
tt.plan(2);

tt.test('CJS', function (ttt) {
ttt.plan(2);

var tc = function (rows) {
ttt.same(processRows(rows.toString('utf8')), processRows([
'TAP version 13',
'# test',
'not ok 1 should be strictly equal',
' ---',
' operator: equal',
' expected: \'foobar\'',
' actual: \'foobaz\'',
' at: Test.<anonymous> ($TEST/stack_trace/cjs.js:7:4)',
' stack: |-',
' Error: should be strictly equal',
' at Test.assert [as _assert] ($TAPE/lib/test.js:$LINE:$COL)',
' at Test.strictEqual ($TAPE/lib/test.js:$LINE:$COL)',
' at Test.<anonymous> ($TEST/stack_trace/cjs.js:7:4)',
' at Test.run ($TAPE/lib/test.js:$LINE:$COL)',
' at Immediate.next ($TAPE/lib/results.js:$LINE:$COL)',
' at processImmediate (timers:$LINE:$COL)',
' ...',
'',
'1..1',
'# tests 1',
'# pass 0',
'# fail 1',
'',
''
]));
};

var ps = spawnTape('stack_trace/cjs.js');
ps.stdout.pipe(concat(tc));
ps.stderr.pipe(process.stderr);
ps.on('exit', function (code) {
ttt.notEqual(code, 0);
ttt.end();
});
});

hasDynamicImport().then(function (hasSupport) {
tt.test('ESM', { skip: !url.pathToFileURL || !hasSupport }, function (ttt) {
ttt.plan(2);

var tc = function (rows) {
ttt.same(processRows(rows.toString('utf8')), processRows([
'TAP version 13',
'# test',
'not ok 1 should be strictly equal',
' ---',
' operator: equal',
' expected: \'foobar\'',
' actual: \'foobaz\'',
' at: Test.<anonymous> (' + url.pathToFileURL(__dirname + '/stack_trace/esm.mjs:5:4') + ')',
' stack: |-',
' Error: should be strictly equal',
' at Test.assert [as _assert] ($TAPE/lib/test.js:$LINE:$COL)',
' at Test.strictEqual ($TAPE/lib/test.js:$LINE:$COL)',
' at Test.<anonymous> (' + url.pathToFileURL(__dirname + '/stack_trace/esm.mjs:5:4') + ')',
' at Test.run ($TAPE/lib/test.js:$LINE:$COL)',
' at Immediate.next ($TAPE/lib/results.js:$LINE:$COL)',
// node ?
// at runCallback (timers.js:$LINE:$COL)
' at process.processImmediate (node:internal/timers:478:21)',
' ...',
'',
'1..1',
'# tests 1',
'# pass 0',
'# fail 1',
'',
''
]));
};

var ps = spawnTape('stack_trace/esm.mjs');
ps.stdout.pipe(concat(tc));
ps.stderr.pipe(process.stderr);
ps.on('exit', function (code) {
ttt.equal(code, 1);
ttt.end();
});
});
});
});
8 changes: 8 additions & 0 deletions test/stack_trace/cjs.js
@@ -0,0 +1,8 @@
'use strict';

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

test('test', function (t) {
t.plan(1);
t.equal('foobaz', 'foobar');
});
6 changes: 6 additions & 0 deletions test/stack_trace/esm.mjs
@@ -0,0 +1,6 @@
import test from '../../index.js';

test('test', function (t) {
t.plan(1);
t.equal('foobaz', 'foobar');
});

0 comments on commit 6a5df50

Please sign in to comment.