GeezerVison is showing github.com — test_runner: add context.log() and test:log event · nodejs/node@bb51f2c
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +81Lines changed: 81 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3476,6 +3476,10 @@ ordered events, emitted immediately as the tests execute. | 3476 | 3476 | | [`'test:fail'`][] | [`'test:complete'`][] (`details.passed` is `false`) | | 3477 | 3477 | | [`'test:plan'`][] | | | 3478 | 3478 | | [`'test:diagnostic'`][] | | | | 3479 | +| | [`'test:log'`][] | | | 3480 | + | | 3481 | +[`'test:log'`][] is deliberately execution ordered only: it is the live | | 3482 | +counterpart of [`'test:diagnostic'`][]'s buffered reporting. | 3479 | 3483 |
| 3480 | 3484 | File scoped and global events are always emitted immediately, in execution | 3481 | 3485 | order. | @@ -3743,6 +3747,38 @@ When using process isolation (the default), the test name will be the file path | 3743 | 3747 | since the parent runner only knows about file-level tests. When using | 3744 | 3748 | `--test-isolation=none`, the actual test name is shown. | 3745 | 3749 |
| | 3750 | +### Event: `'test:log'` | | 3751 | + | | 3752 | +<!-- YAML | | 3753 | +added: REPLACEME | | 3754 | +--> | | 3755 | + | | 3756 | +* `data` {Object} | | 3757 | + * `column` {number|undefined} The column number where the test is defined, or | | 3758 | + `undefined` if the test was run through the REPL. | | 3759 | + * `data` {any} The structured payload passed to [`context.log`][], or | | 3760 | + `undefined` if none was provided. The test runner does not interpret this | | 3761 | + value. | | 3762 | + * `entryFile` {string|undefined} The path of the test file that was | | 3763 | + executed as the entry point of the child process that emitted this event. | | 3764 | + Only present when tests run with process isolation. May differ from | | 3765 | + `file` when the test is defined in a module imported by the entry file. | | 3766 | + * `file` {string|undefined} The path of the test file, | | 3767 | + `undefined` if test was run through the REPL. | | 3768 | + * `line` {number|undefined} The line number where the test is defined, or | | 3769 | + `undefined` if the test was run through the REPL. | | 3770 | + * `message` {string} The log message. | | 3771 | + * `name` {string} The test name. | | 3772 | + * `nesting` {number} The nesting level of the test. | | 3773 | + * `parentId` {number|undefined} The `testId` of the enclosing test, or | | 3774 | + `undefined` for top-level tests. | | 3775 | + * `testId` {number} A numeric identifier for the test instance that emitted | | 3776 | + the log message. | | 3777 | + | | 3778 | +Emitted when [`context.log`][] is called. Unlike [`'test:diagnostic'`][], | | 3779 | +this event is emitted immediately, in the order that the tests execute, | | 3780 | +making it suitable for reporters that render test output unbuffered. | | 3781 | + | 3746 | 3782 | ### Event: `'test:pass'` | 3747 | 3783 |
| 3748 | 3784 | * `data` {Object} | @@ -4266,6 +4302,29 @@ test('top level test', (t) => { | 4266 | 4302 | }); | 4267 | 4303 | ``` | 4268 | 4304 |
| | 4305 | +### `context.log(message[, data])` | | 4306 | + | | 4307 | +<!-- YAML | | 4308 | +added: REPLACEME | | 4309 | +--> | | 4310 | + | | 4311 | +* `message` {string} Message to be reported. | | 4312 | +* `data` {any} Optional structured payload attached to the message. The test | | 4313 | + runner passes it through untouched. When tests run with process isolation, | | 4314 | + this value must be compatible with the [HTML structured clone algorithm][]. | | 4315 | + | | 4316 | +This function is used to write a log message to the output. Unlike | | 4317 | +[`context.diagnostic`][], the resulting [`'test:log'`][] event is emitted | | 4318 | +immediately, in the order that the tests execute, rather than being buffered | | 4319 | +until the test reports its results. This function does not return a value. | | 4320 | + | | 4321 | +```js | | 4322 | +test('top level test', (t) => { | | 4323 | + t.log('fetched user', { userId: 42 }); | | 4324 | + t.log('retrying flaky endpoint', { attempt: 3 }); | | 4325 | +}); | | 4326 | +``` | | 4327 | + | 4269 | 4328 | ### `context.filePath` | 4270 | 4329 |
| 4271 | 4330 | <!-- YAML | @@ -4747,6 +4806,26 @@ test.describe('my suite', (suite) => { | 4747 | 4806 | }); | 4748 | 4807 | ``` | 4749 | 4808 |
| | 4809 | +### `context.log(message[, data])` | | 4810 | + | | 4811 | +<!-- YAML | | 4812 | +added: REPLACEME | | 4813 | +--> | | 4814 | + | | 4815 | +* `message` {string} Message to be reported. | | 4816 | +* `data` {any} Optional structured payload attached to the message. The test | | 4817 | + runner passes it through untouched. | | 4818 | + | | 4819 | +Write a log message to the output. The resulting [`'test:log'`][] event is | | 4820 | +emitted immediately, in the order that the tests execute. | | 4821 | + | | 4822 | +```js | | 4823 | +test.describe('my suite', (suite) => { | | 4824 | + suite.log('Suite log message'); | | 4825 | +}); | | 4826 | +``` | | 4827 | + | | 4828 | +[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm | 4750 | 4829 | [TAP]: https://testanything.org/ | 4751 | 4830 | [Test tags]: #test-tags | 4752 | 4831 | [`'test:complete'`]: #event-testcomplete | @@ -4756,6 +4835,7 @@ test.describe('my suite', (suite) => { | 4756 | 4835 | [`'test:enqueue'`]: #event-testenqueue | 4757 | 4836 | [`'test:fail'`]: #event-testfail | 4758 | 4837 | [`'test:interrupted'`]: #event-testinterrupted | | 4838 | +[`'test:log'`]: #event-testlog | 4759 | 4839 | [`'test:pass'`]: #event-testpass | 4760 | 4840 | [`'test:plan'`]: #event-testplan | 4761 | 4841 | [`'test:start'`]: #event-teststart | @@ -4791,6 +4871,7 @@ test.describe('my suite', (suite) => { | 4791 | 4871 | [`TracingChannel`]: diagnostics_channel.md#class-tracingchannel | 4792 | 4872 | [`assert.throws`]: assert.md#assertthrowsfn-error-message | 4793 | 4873 | [`context.diagnostic`]: #contextdiagnosticmessage | | 4874 | +[`context.log`]: #contextlogmessage-data | 4794 | 4875 | [`context.skip`]: #contextskipmessage | 4795 | 4876 | [`context.tags`]: #contexttags | 4796 | 4877 | [`context.todo`]: #contexttodomessage | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/junit.js +2-1Lines changed: 2 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -154,7 +154,8 @@ module.exports = async function* junitReporter(source) { | 154 | 154 | } | 155 | 155 | break; | 156 | 156 | } | 157 | | - case 'test:diagnostic': { | | 157 | + case 'test:diagnostic': | | 158 | + case 'test:log': { | 158 | 159 | const parent = currentSuite?.children ?? roots; | 159 | 160 | ArrayPrototypePush(parent, { | 160 | 161 | __proto__: null, nesting: event.data.nesting, comment: event.data.message, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/spec.js +3-2Lines changed: 3 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -91,8 +91,9 @@ class SpecReporter extends Transform { | 91 | 91 | case 'test:stderr': | 92 | 92 | case 'test:stdout': | 93 | 93 | 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]; | 96 | 97 | return `${diagnosticColor}${indent(data.nesting)}${reporterUnicodeSymbolMap[type]}${data.message}${colors.white}\n`; | 97 | 98 | } | 98 | 99 | case 'test:coverage': | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/tap.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -56,6 +56,7 @@ async function * tapReporter(source) { | 56 | 56 | } | 57 | 57 | break; | 58 | 58 | } case 'test:diagnostic': | | 59 | + case 'test:log': | 59 | 60 | yield `${indent(data.nesting)}# ${tapEscape(data.message)}\n`; | 60 | 61 | break; | 61 | 62 | case 'test:coverage': | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/reporter/utils.js +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -21,6 +21,7 @@ const reporterUnicodeSymbolMap = { | 21 | 21 | 'test:fail': '\u2716 ', | 22 | 22 | 'test:pass': '\u2714 ', | 23 | 23 | 'test:diagnostic': '\u2139 ', | | 24 | + 'test:log': '\u2139 ', | 24 | 25 | 'test:coverage': '\u2139 ', | 25 | 26 | 'arrow:right': '\u25B6 ', | 26 | 27 | 'hyphen:minus': '\uFE63 ', | @@ -38,6 +39,9 @@ const reporterColorMap = { | 38 | 39 | get 'test:diagnostic'() { | 39 | 40 | return colors.blue; | 40 | 41 | }, | | 42 | + get 'test:log'() { | | 43 | + return colors.blue; | | 44 | + }, | 41 | 45 | get 'info'() { | 42 | 46 | return colors.blue; | 43 | 47 | }, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -130,7 +130,7 @@ const kCanceledTests = new SafeSet() | 130 | 130 | // Execution-ordered events are forwarded immediately, bypassing the | 131 | 131 | // per-file declaration-order buffer. | 132 | 132 | 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'); | 134 | 134 |
| 135 | 135 | let kResistStopPropagation; | 136 | 136 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js +15Lines changed: 15 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -80,6 +80,7 @@ const { | 80 | 80 | validateNumber, | 81 | 81 | validateObject, | 82 | 82 | validateOneOf, | | 83 | + validateString, | 83 | 84 | validateUint32, | 84 | 85 | } = require('internal/validators'); | 85 | 86 | const { | @@ -315,6 +316,10 @@ class TestContext { | 315 | 316 | this.#test.diagnostic(message); | 316 | 317 | } | 317 | 318 |
| | 319 | + log(message, data) { | | 320 | + this.#test.log(message, data); | | 321 | + } | | 322 | + | 318 | 323 | plan(count, options = kEmptyObject) { | 319 | 324 | if (this.#test.plan !== null) { | 320 | 325 | throw new ERR_TEST_FAILURE( | @@ -523,6 +528,10 @@ class SuiteContext { | 523 | 528 | diagnostic(message) { | 524 | 529 | this.#suite.diagnostic(message); | 525 | 530 | } | | 531 | + | | 532 | + log(message, data) { | | 533 | + this.#suite.log(message, data); | | 534 | + } | 526 | 535 | } | 527 | 536 |
| 528 | 537 | function parseExpectFailure(expectFailure) { | @@ -1198,6 +1207,12 @@ class Test extends AsyncResource { | 1198 | 1207 | ArrayPrototypePush(this.diagnostics, message); | 1199 | 1208 | } | 1200 | 1209 |
| | 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 | + | 1201 | 1216 | start() { | 1202 | 1217 | this.applyFilters(); | 1203 | 1218 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/tests_stream.js +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -139,6 +139,19 @@ class TestsStream extends Readable { | 139 | 139 | }); | 140 | 140 | } | 141 | 141 |
| | 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 | + | 142 | 155 | diagnostic(nesting, loc, message, level = 'info') { | 143 | 156 | this[kEmitMessage]('test:diagnostic', { | 144 | 157 | __proto__: null, | |
Collapse file Copy file name to clipboard +2-1Lines changed: 2 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | import { test } from 'node:test'; | 2 | 2 | import assert from 'node:assert'; | 3 | 3 |
| 4 | | -test('fast-fail', () => { | | 4 | +test('fast-fail', (t) => { | | 5 | + t.log('live'); | 5 | 6 | assert.fail('fast'); | 6 | 7 | }); |
Collapse file Copy file name to clipboard +19Lines changed: 19 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +}); |
|