Skip to content

Commit 808d7ad

Browse files
committed
test_runner: add 'test:summary' event
This commit adds a new 'test:summary' event to the test runner's reporting interface. This new event serves two purposes: - In the future, the test runner internals will no longer need to change the process exit code. This may be important to run() users. Unfortunately, this is a breaking change, so it needs to be changed in a major version. - The reporting interface now has a single event that can identify passing or failing test runs. Refs: nodejs#53867 Refs: nodejs#54812
1 parent a65105e commit 808d7ad

File tree

7 files changed

+59
-9
lines changed

7 files changed

+59
-9
lines changed

doc/api/test.md

+25
Original file line numberDiff line numberDiff line change
@@ -3016,6 +3016,31 @@ This event is only emitted if `--test` flag is passed.
30163016
This event is not guaranteed to be emitted in the same order as the tests are
30173017
defined.
30183018

3019+
### Event: `'test:summary'`
3020+
3021+
* `data` {Object}
3022+
* `counts` {Object} An object containing the counts of various test results.
3023+
* `all` {number} The total number of tests run, excluding suites.
3024+
* `cancelled` {number} The total number of cancelled tests.
3025+
* `failed` {number} The total number of failed tests.
3026+
* `passed` {number} The total number of passed tests.
3027+
* `skipped` {number} The total number of skipped tests.
3028+
* `suites` {number} The total number of suites run.
3029+
* `todo` {number} The total number of TODO tests.
3030+
* `topLevel` {number} The total number of top level tests and suites.
3031+
* `duration_ms` {number} The duration of the test run in milliseconds.
3032+
* `file` {string|undefined} The path of the test file that generated the
3033+
summary. If the summary corresponds to multiple files, this value is
3034+
`undefined`.
3035+
* `success` {boolean} Indicates whether or not the test run is considered
3036+
successful or not. If any error condition occurs, such as a failing test or
3037+
unmet coverage threshold, this value will be set to `false`.
3038+
3039+
Emitted when a test run completes. This event contains metrics pertaining to
3040+
the completed test run, and is useful for determining if a test run passed or
3041+
failed. If process-level test isolation is used, a `'test:summary'` event is
3042+
generated for each test file in addition to a final cumulative summary.
3043+
30193044
### Event: `'test:watch:drained'`
30203045

30213046
Emitted when no more tests are queued for execution in watch mode.

lib/internal/main/test_runner.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ if (isUsingInspector() && options.isolation === 'process') {
3131
options.globPatterns = ArrayPrototypeSlice(process.argv, 1);
3232

3333
debug('test runner configuration:', options);
34-
run(options).on('test:fail', (data) => {
35-
if (data.todo === undefined || data.todo === false) {
34+
run(options).on('test:summary', (data) => {
35+
if (!data.success) {
3636
process.exitCode = kGenericUserError;
3737
}
3838
});

lib/internal/test_runner/harness.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function createTestTree(rootTestOptions, globalOptions) {
6262
suites: 0,
6363
};
6464
},
65+
success: true,
6566
counters: null,
6667
shouldColorizeTestFiles: shouldColorizeTestFiles(globalOptions.destinations),
6768
teardown: null,
@@ -130,6 +131,7 @@ function createProcessEventHandler(eventName, rootTest) {
130131
}
131132

132133
rootTest.diagnostic(msg);
134+
rootTest.harness.success = false;
133135
process.exitCode = kGenericUserError;
134136
return;
135137
}
@@ -152,6 +154,7 @@ function configureCoverage(rootTest, globalOptions) {
152154
const msg = `Warning: Code coverage could not be enabled. ${err}`;
153155

154156
rootTest.diagnostic(msg);
157+
rootTest.harness.success = false;
155158
process.exitCode = kGenericUserError;
156159
}
157160
}
@@ -167,13 +170,15 @@ function collectCoverage(rootTest, coverage) {
167170
summary = coverage.summary();
168171
} catch (err) {
169172
rootTest.diagnostic(`Warning: Could not report code coverage. ${err}`);
173+
rootTest.harness.success = false;
170174
process.exitCode = kGenericUserError;
171175
}
172176

173177
try {
174178
coverage.cleanup();
175179
} catch (err) {
176180
rootTest.diagnostic(`Warning: Could not clean up code coverage. ${err}`);
181+
rootTest.harness.success = false;
177182
process.exitCode = kGenericUserError;
178183
}
179184

@@ -248,14 +253,16 @@ function lazyBootstrapRoot() {
248253
if (!globalRoot) {
249254
// This is where the test runner is bootstrapped when node:test is used
250255
// without the --test flag or the run() API.
256+
const entryFile = process.argv?.[1];
251257
const rootTestOptions = {
252258
__proto__: null,
253-
entryFile: process.argv?.[1],
259+
entryFile,
260+
loc: entryFile ? [1, 1, entryFile] : undefined,
254261
};
255262
const globalOptions = parseCommandLine();
256263
createTestTree(rootTestOptions, globalOptions);
257-
globalRoot.reporter.on('test:fail', (data) => {
258-
if (data.todo === undefined || data.todo === false) {
264+
globalRoot.reporter.on('test:summary', (data) => {
265+
if (!data.success) {
259266
process.exitCode = kGenericUserError;
260267
}
261268
});

lib/internal/test_runner/test.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -1033,14 +1033,15 @@ class Test extends AsyncResource {
10331033
reporter.diagnostic(nesting, loc, diagnostics[i]);
10341034
}
10351035

1036+
const duration = this.duration();
10361037
reporter.diagnostic(nesting, loc, `tests ${harness.counters.all}`);
10371038
reporter.diagnostic(nesting, loc, `suites ${harness.counters.suites}`);
10381039
reporter.diagnostic(nesting, loc, `pass ${harness.counters.passed}`);
10391040
reporter.diagnostic(nesting, loc, `fail ${harness.counters.failed}`);
10401041
reporter.diagnostic(nesting, loc, `cancelled ${harness.counters.cancelled}`);
10411042
reporter.diagnostic(nesting, loc, `skipped ${harness.counters.skipped}`);
10421043
reporter.diagnostic(nesting, loc, `todo ${harness.counters.todo}`);
1043-
reporter.diagnostic(nesting, loc, `duration_ms ${this.duration()}`);
1044+
reporter.diagnostic(nesting, loc, `duration_ms ${duration}`);
10441045

10451046
if (coverage) {
10461047
const coverages = [
@@ -1057,6 +1058,7 @@ class Test extends AsyncResource {
10571058
for (let i = 0; i < coverages.length; i++) {
10581059
const { threshold, actual, name } = coverages[i];
10591060
if (actual < threshold) {
1061+
harness.success = false;
10601062
process.exitCode = kGenericUserError;
10611063
reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(actual, 2)}% ${name} coverage does not meet threshold of ${threshold}%.`);
10621064
}
@@ -1065,6 +1067,10 @@ class Test extends AsyncResource {
10651067
reporter.coverage(nesting, loc, coverage);
10661068
}
10671069

1070+
reporter.summary(
1071+
nesting, loc?.file, harness.success, harness.counters, duration,
1072+
);
1073+
10681074
if (harness.watching) {
10691075
this.reported = false;
10701076
harness.resetCounters();

lib/internal/test_runner/tests_stream.js

+10
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ class TestsStream extends Readable {
132132
});
133133
}
134134

135+
summary(nesting, file, success, counts, duration_ms) {
136+
this[kEmitMessage]('test:summary', {
137+
__proto__: null,
138+
success,
139+
counts,
140+
duration_ms,
141+
file,
142+
});
143+
}
144+
135145
end() {
136146
this.#tryPush(null);
137147
}

lib/internal/test_runner/utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,10 @@ function countCompletedTest(test, harness = test.root.harness) {
355355
harness.counters.todo++;
356356
} else if (test.cancelled) {
357357
harness.counters.cancelled++;
358+
harness.success = false;
358359
} else if (!test.passed) {
359360
harness.counters.failed++;
361+
harness.success = false;
360362
} else {
361363
harness.counters.passed++;
362364
}

test/parallel/test-runner-reporters.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe('node:test reporters', { concurrency: true }, () => {
113113
testFile]);
114114
assert.strictEqual(child.stderr.toString(), '');
115115
const stdout = child.stdout.toString();
116-
assert.match(stdout, /{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:diagnostic":\d+}$/);
116+
assert.match(stdout, /{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:summary":2,"test:diagnostic":\d+}$/);
117117
assert.strictEqual(stdout.slice(0, filename.length + 2), `${filename} {`);
118118
});
119119
});
@@ -125,7 +125,7 @@ describe('node:test reporters', { concurrency: true }, () => {
125125
assert.strictEqual(child.stderr.toString(), '');
126126
assert.match(
127127
child.stdout.toString(),
128-
/^package: reporter-cjs{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:diagnostic":\d+}$/,
128+
/^package: reporter-cjs{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:summary":2,"test:diagnostic":\d+}$/,
129129
);
130130
});
131131

@@ -136,7 +136,7 @@ describe('node:test reporters', { concurrency: true }, () => {
136136
assert.strictEqual(child.stderr.toString(), '');
137137
assert.match(
138138
child.stdout.toString(),
139-
/^package: reporter-esm{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:diagnostic":\d+}$/,
139+
/^package: reporter-esm{"test:enqueue":5,"test:dequeue":5,"test:complete":5,"test:start":4,"test:pass":2,"test:fail":2,"test:plan":2,"test:summary":2,"test:diagnostic":\d+}$/,
140140
);
141141
});
142142

0 commit comments

Comments
 (0)