Skip to content

Commit 892976d

Browse files
authored
test_runner: add context.log() and test:log event
Add a log(message[, data]) method to TestContext and SuiteContext that emits a new test:log event. Unlike test:diagnostic, which is buffered so it is emitted in the order tests are defined, test:log is emitted immediately, in the order tests execute, including under process isolation where it bypasses the per-file declaration order buffer. This gives reporters that render the test tree unbuffered a live, attributed logging channel that captured stdout cannot provide under concurrency. The event carries the message, an optional opaque structured payload that the runner passes through untouched, and the emitting test's name, testId, parentId, nesting, and location. Built-in reporters render it the same way they render test:diagnostic. Signed-off-by: Moses Atlow <moshe@atlow.co.il> PR-URL: #64389 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent 17163ea commit 892976d

12 files changed

Lines changed: 233 additions & 5 deletions

File tree

doc/api/test.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3478,6 +3478,10 @@ ordered events, emitted immediately as the tests execute.
34783478
| [`'test:fail'`][] | [`'test:complete'`][] (`details.passed` is `false`) |
34793479
| [`'test:plan'`][] | |
34803480
| [`'test:diagnostic'`][] | |
3481+
| | [`'test:log'`][] |
3482+
3483+
[`'test:log'`][] is deliberately execution ordered only: it is the live
3484+
counterpart of [`'test:diagnostic'`][]'s buffered reporting.
34813485

34823486
File scoped and global events are always emitted immediately, in execution
34833487
order.
@@ -3747,6 +3751,38 @@ When using process isolation (the default), the test name will be the file path
37473751
since the parent runner only knows about file-level tests. When using
37483752
`--test-isolation=none`, the actual test name is shown.
37493753

3754+
### Event: `'test:log'`
3755+
3756+
<!-- YAML
3757+
added: REPLACEME
3758+
-->
3759+
3760+
* `data` {Object}
3761+
* `column` {number|undefined} The column number where the test is defined, or
3762+
`undefined` if the test was run through the REPL.
3763+
* `data` {any} The structured payload passed to [`context.log`][], or
3764+
`undefined` if none was provided. The test runner does not interpret this
3765+
value.
3766+
* `entryFile` {string|undefined} The path of the test file that was
3767+
executed as the entry point of the child process that emitted this event.
3768+
Only present when tests run with process isolation. May differ from
3769+
`file` when the test is defined in a module imported by the entry file.
3770+
* `file` {string|undefined} The path of the test file,
3771+
`undefined` if test was run through the REPL.
3772+
* `line` {number|undefined} The line number where the test is defined, or
3773+
`undefined` if the test was run through the REPL.
3774+
* `message` {string} The log message.
3775+
* `name` {string} The test name.
3776+
* `nesting` {number} The nesting level of the test.
3777+
* `parentId` {number|undefined} The `testId` of the enclosing test, or
3778+
`undefined` for top-level tests.
3779+
* `testId` {number} A numeric identifier for the test instance that emitted
3780+
the log message.
3781+
3782+
Emitted when [`context.log`][] is called. Unlike [`'test:diagnostic'`][],
3783+
this event is emitted immediately, in the order that the tests execute,
3784+
making it suitable for reporters that render test output unbuffered.
3785+
37503786
### Event: `'test:pass'`
37513787

37523788
* `data` {Object}
@@ -4272,6 +4308,29 @@ test('top level test', (t) => {
42724308
});
42734309
```
42744310

4311+
### `context.log(message[, data])`
4312+
4313+
<!-- YAML
4314+
added: REPLACEME
4315+
-->
4316+
4317+
* `message` {string} Message to be reported.
4318+
* `data` {any} Optional structured payload attached to the message. The test
4319+
runner passes it through untouched. When tests run with process isolation,
4320+
this value must be compatible with the [HTML structured clone algorithm][].
4321+
4322+
This function is used to write a log message to the output. Unlike
4323+
[`context.diagnostic`][], the resulting [`'test:log'`][] event is emitted
4324+
immediately, in the order that the tests execute, rather than being buffered
4325+
until the test reports its results. This function does not return a value.
4326+
4327+
```js
4328+
test('top level test', (t) => {
4329+
t.log('fetched user', { userId: 42 });
4330+
t.log('retrying flaky endpoint', { attempt: 3 });
4331+
});
4332+
```
4333+
42754334
### `context.filePath`
42764335

42774336
<!-- YAML
@@ -4761,6 +4820,26 @@ test.describe('my suite', (suite) => {
47614820
});
47624821
```
47634822

4823+
### `context.log(message[, data])`
4824+
4825+
<!-- YAML
4826+
added: REPLACEME
4827+
-->
4828+
4829+
* `message` {string} Message to be reported.
4830+
* `data` {any} Optional structured payload attached to the message. The test
4831+
runner passes it through untouched.
4832+
4833+
Write a log message to the output. The resulting [`'test:log'`][] event is
4834+
emitted immediately, in the order that the tests execute.
4835+
4836+
```js
4837+
test.describe('my suite', (suite) => {
4838+
suite.log('Suite log message');
4839+
});
4840+
```
4841+
4842+
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
47644843
[TAP]: https://testanything.org/
47654844
[Test tags]: #test-tags
47664845
[`'test:complete'`]: #event-testcomplete
@@ -4770,6 +4849,7 @@ test.describe('my suite', (suite) => {
47704849
[`'test:enqueue'`]: #event-testenqueue
47714850
[`'test:fail'`]: #event-testfail
47724851
[`'test:interrupted'`]: #event-testinterrupted
4852+
[`'test:log'`]: #event-testlog
47734853
[`'test:pass'`]: #event-testpass
47744854
[`'test:plan'`]: #event-testplan
47754855
[`'test:start'`]: #event-teststart
@@ -4805,6 +4885,7 @@ test.describe('my suite', (suite) => {
48054885
[`TracingChannel`]: diagnostics_channel.md#class-tracingchannel
48064886
[`assert.throws`]: assert.md#assertthrowsfn-error-message
48074887
[`context.diagnostic`]: #contextdiagnosticmessage
4888+
[`context.log`]: #contextlogmessage-data
48084889
[`context.skip`]: #contextskipmessage
48094890
[`context.tags`]: #contexttags
48104891
[`context.todo`]: #contexttodomessage

lib/internal/test_runner/reporter/junit.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ module.exports = async function* junitReporter(source) {
154154
}
155155
break;
156156
}
157-
case 'test:diagnostic': {
157+
case 'test:diagnostic':
158+
case 'test:log': {
158159
const parent = currentSuite?.children ?? roots;
159160
ArrayPrototypePush(parent, {
160161
__proto__: null, nesting: event.data.nesting, comment: event.data.message,

lib/internal/test_runner/reporter/spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ class SpecReporter extends Transform {
9191
case 'test:stderr':
9292
case 'test:stdout':
9393
return data.message;
94-
case 'test:diagnostic':{
95-
const diagnosticColor = reporterColorMap[data.level] || reporterColorMap['test:diagnostic'];
94+
case 'test:diagnostic':
95+
case 'test:log': {
96+
const diagnosticColor = reporterColorMap[data.level] || reporterColorMap[type];
9697
return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`;
9798
}
9899
case 'test:coverage':

lib/internal/test_runner/reporter/tap.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ async function * tapReporter(source) {
5656
}
5757
break;
5858
} case 'test:diagnostic':
59+
case 'test:log':
5960
yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`;
6061
break;
6162
case 'test:coverage':

lib/internal/test_runner/reporter/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const reporterUnicodeSymbolMap = {
2121
'test:fail': '\u2716 ',
2222
'test:pass': '\u2714 ',
2323
'test:diagnostic': '\u2139 ',
24+
'test:log': '\u2139 ',
2425
'test:coverage': '\u2139 ',
2526
'arrow:right': '\u25B6 ',
2627
'hyphen:minus': '\uFE63 ',
@@ -38,6 +39,9 @@ const reporterColorMap = {
3839
get 'test:diagnostic'() {
3940
return colors.blue;
4041
},
42+
get 'test:log'() {
43+
return colors.blue;
44+
},
4145
get 'info'() {
4246
return colors.blue;
4347
},

lib/internal/test_runner/runner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const kCanceledTests = new SafeSet()
130130
// Execution-ordered events are forwarded immediately, bypassing the
131131
// per-file declaration-order buffer.
132132
const kExecutionOrderedEvents = new SafeSet()
133-
.add('test:enqueue').add('test:dequeue').add('test:complete');
133+
.add('test:enqueue').add('test:dequeue').add('test:complete').add('test:log');
134134

135135
let kResistStopPropagation;
136136

lib/internal/test_runner/test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const {
8080
validateNumber,
8181
validateObject,
8282
validateOneOf,
83+
validateString,
8384
validateUint32,
8485
} = require('internal/validators');
8586
const {
@@ -315,6 +316,10 @@ class TestContext {
315316
this.#test.diagnostic(message);
316317
}
317318

319+
log(message, data) {
320+
this.#test.log(message, data);
321+
}
322+
318323
plan(count, options = kEmptyObject) {
319324
if (this.#test.plan !== null) {
320325
throw new ERR_TEST_FAILURE(
@@ -523,6 +528,10 @@ class SuiteContext {
523528
diagnostic(message) {
524529
this.#suite.diagnostic(message);
525530
}
531+
532+
log(message, data) {
533+
this.#suite.log(message, data);
534+
}
526535
}
527536

528537
function parseExpectFailure(expectFailure) {
@@ -1198,6 +1207,12 @@ class Test extends AsyncResource {
11981207
ArrayPrototypePush(this.diagnostics, message);
11991208
}
12001209

1210+
log(message, data) {
1211+
validateString(message, 'message');
1212+
this.reporter.log(this.nesting, this.loc, message, data,
1213+
this.name, this.testId, this.parent?.testId);
1214+
}
1215+
12011216
start() {
12021217
this.applyFilters();
12031218

lib/internal/test_runner/tests_stream.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ class TestsStream extends Readable {
139139
});
140140
}
141141

142+
log(nesting, loc, message, data, name, testId, parentId) {
143+
this[kEmitMessage]('test:log', {
144+
__proto__: null,
145+
name,
146+
nesting,
147+
testId,
148+
parentId,
149+
message,
150+
data,
151+
...loc,
152+
});
153+
}
154+
142155
diagnostic(nesting, loc, message, level = 'info') {
143156
this[kEmitMessage]('test:diagnostic', {
144157
__proto__: null,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test } from 'node:test';
22
import assert from 'node:assert';
33

4-
test('fast-fail', () => {
4+
test('fast-fail', (t) => {
5+
t.log('live');
56
assert.fail('fast');
67
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { suite, test } from 'node:test';
2+
import { setTimeout } from 'node:timers/promises';
3+
4+
suite('my suite', (s) => {
5+
s.log('suite message');
6+
test('in suite', () => {});
7+
});
8+
9+
test('parent', { concurrency: 2 }, async (t) => {
10+
await Promise.all([
11+
t.test('slow', async () => {
12+
await setTimeout(200);
13+
}),
14+
t.test('logger', (t) => {
15+
t.log('hello', { foo: 1 });
16+
t.log('warned', { level: 'warn', attempt: 2 });
17+
}),
18+
]);
19+
});

0 commit comments

Comments
 (0)