diff --git a/lib/test.js b/lib/test.js index d7db42a6..678d423e 100644 --- a/lib/test.js +++ b/lib/test.js @@ -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) @@ -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 `` 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 `` 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( diff --git a/package.json b/package.json index a08a1c24..3d5ad3c5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/common.js b/test/common.js index f2098225..64dfb30f 100644 --- a/test/common.js +++ b/test/common.js @@ -44,13 +44,28 @@ 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(/, :\$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; @@ -59,7 +74,7 @@ module.exports.stripFullStack = function (output) { }); var withoutInternals = withDuplicates.filter(function (line) { - return !line.match(/ \(node:[^)]+\)$/); + return typeof line === 'string' && !line.match(/ \(node:[^)]+\)$/); }); var deduped = withoutInternals.filter(function (line, ix) { diff --git a/test/stackTrace.js b/test/stackTrace.js index ecd0f37e..ffa31aca 100644 --- a/test/stackTrace.js +++ b/test/stackTrace.js @@ -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; @@ -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); @@ -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. ($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. ($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. (' + 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. (' + 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(); + }); + }); + }); +}); diff --git a/test/stack_trace/cjs.js b/test/stack_trace/cjs.js new file mode 100644 index 00000000..28147b23 --- /dev/null +++ b/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'); +}); diff --git a/test/stack_trace/esm.mjs b/test/stack_trace/esm.mjs new file mode 100644 index 00000000..52c14233 --- /dev/null +++ b/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'); +});