Skip to content

Commit d0d45b2

Browse files
committed
test: unify assertSnapshot stacktrace transform
The snapshotted stack frames should hide node internal stack frames so that general node core development does not need updating the snapshot. For userland stack frames, they are highly fixture related and any fixture change should reflect in a change of the snapshot. Additionally, the line and column number are highly relevant to the correctness of the snapshot, these should not be redacted. A change in node core that affects userland stack frames should be alarming and be reflected in the snapshots. Features like test runner and source map support both should snapshot userland stack frames to ensure that userland code locations are computed correctly.
1 parent 1818bb7 commit d0d45b2

File tree

74 files changed

+1082
-2290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+1082
-2290
lines changed

test/common/assertSnapshot.js

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,25 @@ const assert = require('node:assert/strict');
77
const { pathToFileURL } = require('node:url');
88
const { hostname } = require('node:os');
99

10-
const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g;
10+
/* eslint-disable @stylistic/js/max-len,no-control-regex */
11+
/**
12+
* Group 1: Line start (including color codes and escapes)
13+
* Group 2: Function name
14+
* Group 3: Filename
15+
* Group 4: Line number
16+
* Group 5: Column number
17+
* Group 6: Line end (including color codes and `{` which indicates the start of an error object details)
18+
*/
19+
// Mappings: (g1 ) (g2 ) (g3 ) (g4 ) (g5 ) (g6 )
20+
const internalStackFramesRegexp = /(?<=\n)(\s*(?:\x1b?\[\d+m\s+)?(?:at\s)?)(?:(.+?)\s+\()?(?:(node:.+?):(\d+)(?::(\d+))?)\)?((?:\x1b?\[\d+m)?\s*{?\n|$)/g;
21+
/**
22+
* Group 1: Filename
23+
* Group 2: Line number
24+
* Group 3: Line end and source code line
25+
*/
26+
const internalErrorSourceLines = /(?<=\n|^)(node:.+?):(\d+)(\n.*\n\s*\^(?:\n|$))/g;
27+
/* eslint-enable @stylistic/js/max-len,no-control-regex */
28+
1129
const windowNewlineRegexp = /\r/g;
1230

1331
// Replaces the current Node.js executable version strings with a
@@ -17,14 +35,33 @@ function replaceNodeVersion(str) {
1735
return str.replaceAll(process.version, '<node-version>');
1836
}
1937

20-
function replaceStackTrace(str, replacement = '$1*$7$8\n') {
21-
return str.replace(stackFramesRegexp, replacement);
38+
// Collapse consecutive identical lines containing the keyword into
39+
// one single line. The `str` should have been processed by `replaceWindowsLineEndings`.
40+
function foldIdenticalLines(str, keyword) {
41+
const lines = str.split('\n');
42+
const folded = lines.filter((line, idx) => {
43+
if (idx === 0) {
44+
return true;
45+
}
46+
if (line.includes(keyword) && line === lines[idx - 1]) {
47+
return false;
48+
}
49+
return true;
50+
});
51+
return folded.join('\n');
2252
}
2353

54+
const kInternalFrame = '<node-internal-frames>';
55+
// Replace non-internal frame `at TracingChannel.traceSync (node:diagnostics_channel:328:14)`
56+
// as well as `at node:internal/main/run_main_module:33:47` with `at <node-internal-frames>`.
57+
// Also replaces error source line like:
58+
// node:internal/mod.js:44
59+
// throw err;
60+
// ^
2461
function replaceInternalStackTrace(str) {
25-
// Replace non-internal frame `at TracingChannel.traceSync (node:diagnostics_channel:328:14)`
26-
// as well as `at node:internal/main/run_main_module:33:47` with `*`.
27-
return str.replaceAll(/(\W+).*[(\s]node:.*/g, '$1*');
62+
const result = str.replaceAll(internalErrorSourceLines, `$1:<line>$3`)
63+
.replaceAll(internalStackFramesRegexp, `$1${kInternalFrame}$6`);
64+
return foldIdenticalLines(result, kInternalFrame);
2865
}
2966

3067
// Replaces Windows line endings with posix line endings for unified snapshots
@@ -55,11 +92,11 @@ function replaceWarningPid(str) {
5592
return str.replaceAll(/\(node:\d+\)/g, '(node:<pid>)');
5693
}
5794

95+
const projectRoot = path.resolve(__dirname, '..', '..');
5896
// Replaces path strings representing the nodejs/node repo full project root with
5997
// `<project-root>`. Also replaces file URLs containing the full project root path.
6098
// The project root path may contain unicode characters.
6199
function transformProjectRoot(replacement = '<project-root>') {
62-
const projectRoot = path.resolve(__dirname, '../..');
63100
// Handles output already processed by `replaceWindowsPaths`.
64101
const winPath = replaceWindowsPaths(projectRoot);
65102
// Handles URL encoded project root in file URL strings as well.
@@ -73,6 +110,11 @@ function transformProjectRoot(replacement = '<project-root>') {
73110
};
74111
}
75112

113+
// Replaces tmpdirs created by `test/common/tmpdir.js`.
114+
function transformTmpDir(str) {
115+
return str.replaceAll(/\/\.tmp\.\d+\//g, '/<tmpdir>/');
116+
}
117+
76118
function transform(...args) {
77119
return (str) => args.reduce((acc, fn) => fn(acc), str);
78120
}
@@ -142,34 +184,24 @@ function replaceTestDuration(str) {
142184
.replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *');
143185
}
144186

145-
const root = path.resolve(__dirname, '..', '..');
146-
const color = '(\\[\\d+m)';
147-
const stackTraceBasePath = new RegExp(`${color}\\(${RegExp.escape(root)}/?${color}(.*)${color}\\)`, 'g');
148-
149187
function replaceSpecDuration(str) {
150188
return str
151189
.replaceAll(/[0-9.]+ms/g, '*ms')
152-
.replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *')
153-
.replace(stackTraceBasePath, '$3');
190+
.replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *');
154191
}
155192

156193
function replaceJunitDuration(str) {
157194
return str
158195
.replaceAll(/time="[0-9.]+"/g, 'time="*"')
159196
.replaceAll(/duration_ms [0-9.]+/g, 'duration_ms *')
160197
.replaceAll(`hostname="${hostname()}"`, 'hostname="HOSTNAME"')
161-
.replaceAll(/file="[^"]*"/g, 'file="*"')
162-
.replace(stackTraceBasePath, '$3');
198+
.replaceAll(/file="[^"]*"/g, 'file="*"');
163199
}
164200

165201
function removeWindowsPathEscaping(str) {
166202
return common.isWindows ? str.replaceAll(/\\\\/g, '\\') : str;
167203
}
168204

169-
function replaceTestLocationLine(str) {
170-
return str.replaceAll(/(js:)(\d+)(:\d+)/g, '$1(LINE)$3');
171-
}
172-
173205
// The Node test coverage returns results for all files called by the test. This
174206
// will make the output file change if files like test/common/index.js change.
175207
// This transform picks only the first line and then the lines from the test
@@ -188,9 +220,11 @@ function pickTestFileFromLcov(str) {
188220
}
189221

190222
// Transforms basic patterns like:
191-
// - platform specific path and line endings,
192-
// - line trailing spaces,
193-
// - executable specific path and versions.
223+
// - platform specific path and line endings
224+
// - line trailing spaces
225+
// - executable specific path and versions
226+
// - project root path and tmpdir
227+
// - node internal stack frames
194228
const basicTransform = transform(
195229
replaceWindowsLineEndings,
196230
replaceTrailingSpaces,
@@ -199,35 +233,31 @@ const basicTransform = transform(
199233
replaceNodeVersion,
200234
generalizeExeName,
201235
replaceWarningPid,
236+
transformProjectRoot(),
237+
transformTmpDir,
238+
replaceInternalStackTrace,
202239
);
203240

204241
const defaultTransform = transform(
205242
basicTransform,
206-
replaceStackTrace,
207-
transformProjectRoot(),
208243
replaceTestDuration,
209-
replaceTestLocationLine,
210244
);
211245
const specTransform = transform(
212246
replaceSpecDuration,
213247
basicTransform,
214-
replaceStackTrace,
215248
);
216249
const junitTransform = transform(
217250
replaceJunitDuration,
218251
basicTransform,
219-
replaceStackTrace,
220252
);
221253
const lcovTransform = transform(
222254
basicTransform,
223-
replaceStackTrace,
224-
transformProjectRoot(),
225255
pickTestFileFromLcov,
226256
);
227257

228258
function ensureCwdIsProjectRoot() {
229-
if (process.cwd() !== root) {
230-
process.chdir(root);
259+
if (process.cwd() !== projectRoot) {
260+
process.chdir(projectRoot);
231261
}
232262
}
233263

@@ -240,7 +270,6 @@ module.exports = {
240270
assertSnapshot,
241271
getSnapshotPath,
242272
replaceNodeVersion,
243-
replaceStackTrace,
244273
replaceInternalStackTrace,
245274
replaceWindowsLineEndings,
246275
replaceWindowsPaths,
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
11
Trace: foo
2-
at *
3-
at *
4-
at *
5-
at *
6-
at *
7-
at *
8-
at *
9-
at *
2+
at Object.<anonymous> (<project-root>/test/fixtures/console/console.js:5:9)
3+
at <node-internal-frames>

test/fixtures/console/stack_overflow.snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
before
2-
<project-root>/test/fixtures/console/stack_overflow.js:*
2+
<project-root>/test/fixtures/console/stack_overflow.js:39
33
JSON.stringify(array);
44
^
55

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Error: test
22
at one (<project-root>/test/fixtures/async-error.js:4:9)
33
at two (<project-root>/test/fixtures/async-error.js:17:9)
4-
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
4+
at <node-internal-frames>
55
at async three (<project-root>/test/fixtures/async-error.js:20:3)
66
at async four (<project-root>/test/fixtures/async-error.js:24:3)
77
at async main (<project-root>/test/fixtures/errors/async_error_nexttick_main.js:7:5)
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
node:punycode:54
1+
node:punycode:<line>
22
throw new RangeError(errors[type]);
33
^
44

55
RangeError: Invalid input
6-
at error (node:punycode:54:8)
7-
at Object.decode (node:punycode:247:5)
6+
at <node-internal-frames>
87
at Object.<anonymous> (<project-root>/test/fixtures/errors/core_line_numbers.js:13:10)
98

109
Node.js <node-version>

test/fixtures/errors/error_aggregateTwoErrors.snapshot

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:*
1+
<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:15
22
throw aggregateTwoErrors(err, originalError);
33
^
44

55
AggregateError: original
6-
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:*:*) {
6+
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:15:7) {
77
code: 'ERR0',
88
[errors]: [
99
Error: original
10-
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:*:*) {
10+
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:9:23) {
1111
code: 'ERR0'
1212
},
1313
Error: second error
14-
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:*:*) {
14+
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_aggregateTwoErrors.js:10:13) {
1515
code: 'ERR1'
1616
}
1717
]

test/fixtures/errors/error_exit.snapshot

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Exiting with code=1
2-
node:internal/assert/utils:*
2+
node:internal/assert/utils:<line>
33
throw error;
44
^
55

66
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
77

88
1 !== 2
99

10-
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_exit.js:*:*) {
10+
at Object.<anonymous> (<project-root>/test/fixtures/errors/error_exit.js:32:8) {
1111
generatedMessage: true,
1212
code: 'ERR_ASSERTION',
1313
actual: 1,
1 Byte
Binary file not shown.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
node:events:*
1+
node:events:<line>
22
throw er; // Unhandled 'error' event
33
^
44

55
Error: foo:bar
6-
at bar (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:*:*)
7-
at foo (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:*:*)
6+
at bar (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:9:12)
7+
at foo (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:12:10)
88
Emitted 'error' event at:
9-
at quux (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:*:*)
10-
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:*:*)
9+
at quux (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:19:6)
10+
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_common_trace.js:22:1)
1111

1212
Node.js <node-version>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
node:events:*
1+
node:events:<line>
22
throw er; // Unhandled 'error' event
33
^
44

55
Error
6-
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_nexttick.js:*:*)
6+
at Object.<anonymous> (<project-root>/test/fixtures/errors/events_unhandled_error_nexttick.js:6:12)
77
Emitted 'error' event at:
8-
at <project-root>/test/fixtures/errors/events_unhandled_error_nexttick.js:*:*
8+
at <project-root>/test/fixtures/errors/events_unhandled_error_nexttick.js:8:22
99

1010
Node.js <node-version>

0 commit comments

Comments
 (0)