Skip to content

Commit 59527de

Browse files
committed
test_runner: support programmatically running --test
PR-URL: #44241 Fixes: #44023 Fixes: #43675 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent cfdc713 commit 59527de

File tree

10 files changed

+424
-230
lines changed

10 files changed

+424
-230
lines changed

doc/api/test.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,35 @@ Otherwise, the test is considered to be a failure. Test files must be
316316
executable by Node.js, but are not required to use the `node:test` module
317317
internally.
318318

319+
## `run([options])`
320+
321+
<!-- YAML
322+
added: REPLACEME
323+
-->
324+
325+
* `options` {Object} Configuration options for running tests. The following
326+
properties are supported:
327+
* `concurrency` {number|boolean} If a number is provided,
328+
then that many files would run in parallel.
329+
If truthy, it would run (number of cpu cores - 1)
330+
files in parallel.
331+
If falsy, it would only run one file at a time.
332+
If unspecified, subtests inherit this value from their parent.
333+
**Default:** `true`.
334+
* `files`: {Array} An array containing the list of files to run.
335+
**Default** matching files from [test runner execution model][].
336+
* `signal` {AbortSignal} Allows aborting an in-progress test execution.
337+
* `timeout` {number} A number of milliseconds the test execution will
338+
fail after.
339+
If unspecified, subtests inherit this value from their parent.
340+
**Default:** `Infinity`.
341+
* Returns: {TapStream}
342+
343+
```js
344+
run({ files: [path.resolve('./tests/test.js')] })
345+
.pipe(process.stdout);
346+
```
347+
319348
## `test([name][, options][, fn])`
320349

321350
<!-- YAML
@@ -564,6 +593,47 @@ describe('tests', async () => {
564593
});
565594
```
566595

596+
## Class: `TapStream`
597+
598+
<!-- YAML
599+
added: REPLACEME
600+
-->
601+
602+
* Extends {ReadableStream}
603+
604+
A successful call to [`run()`][] method will return a new {TapStream}
605+
object, streaming a [TAP][] output
606+
`TapStream` will emit events, in the order of the tests definition
607+
608+
### Event: `'test:diagnostic'`
609+
610+
* `message` {string} The diagnostic message.
611+
612+
Emitted when [`context.diagnostic`][] is called.
613+
614+
### Event: `'test:fail'`
615+
616+
* `data` {Object}
617+
* `duration` {number} The test duration.
618+
* `error` {Error} The failure casing test to fail.
619+
* `name` {string} The test name.
620+
* `testNumber` {number} The ordinal number of the test.
621+
* `todo` {string|undefined} Present if [`context.todo`][] is called
622+
* `skip` {string|undefined} Present if [`context.skip`][] is called
623+
624+
Emitted when a test fails.
625+
626+
### Event: `'test:pass'`
627+
628+
* `data` {Object}
629+
* `duration` {number} The test duration.
630+
* `name` {string} The test name.
631+
* `testNumber` {number} The ordinal number of the test.
632+
* `todo` {string|undefined} Present if [`context.todo`][] is called
633+
* `skip` {string|undefined} Present if [`context.skip`][] is called
634+
635+
Emitted when a test passes.
636+
567637
## Class: `TestContext`
568638

569639
<!-- YAML
@@ -849,6 +919,10 @@ added:
849919
[`--test`]: cli.md#--test
850920
[`SuiteContext`]: #class-suitecontext
851921
[`TestContext`]: #class-testcontext
922+
[`context.diagnostic`]: #contextdiagnosticmessage
923+
[`context.skip`]: #contextskipmessage
924+
[`context.todo`]: #contexttodomessage
925+
[`run()`]: #runoptions
852926
[`test()`]: #testname-options-fn
853927
[describe options]: #describename-options-fn
854928
[it options]: #testname-options-fn

lib/internal/main/test_runner.js

Lines changed: 6 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,15 @@
11
'use strict';
2-
const {
3-
ArrayFrom,
4-
ArrayPrototypeFilter,
5-
ArrayPrototypeIncludes,
6-
ArrayPrototypeJoin,
7-
ArrayPrototypePush,
8-
ArrayPrototypeSlice,
9-
ArrayPrototypeSort,
10-
SafePromiseAll,
11-
SafeSet,
12-
} = primordials;
132
const {
143
prepareMainThreadExecution,
154
markBootstrapComplete
165
} = require('internal/process/pre_execution');
17-
const { spawn } = require('child_process');
18-
const { readdirSync, statSync } = require('fs');
19-
const console = require('internal/console/global');
20-
const {
21-
codes: {
22-
ERR_TEST_FAILURE,
23-
},
24-
} = require('internal/errors');
25-
const { test } = require('internal/test_runner/harness');
26-
const { kSubtestsFailed } = require('internal/test_runner/test');
27-
const {
28-
isSupportedFileType,
29-
doesPathMatchFilter,
30-
} = require('internal/test_runner/utils');
31-
const { basename, join, resolve } = require('path');
32-
const { once } = require('events');
33-
const kFilterArgs = ['--test'];
6+
const { run } = require('internal/test_runner/runner');
347

358
prepareMainThreadExecution(false);
369
markBootstrapComplete();
3710

38-
// TODO(cjihrig): Replace this with recursive readdir once it lands.
39-
function processPath(path, testFiles, options) {
40-
const stats = statSync(path);
41-
42-
if (stats.isFile()) {
43-
if (options.userSupplied ||
44-
(options.underTestDir && isSupportedFileType(path)) ||
45-
doesPathMatchFilter(path)) {
46-
testFiles.add(path);
47-
}
48-
} else if (stats.isDirectory()) {
49-
const name = basename(path);
50-
51-
if (!options.userSupplied && name === 'node_modules') {
52-
return;
53-
}
54-
55-
// 'test' directories get special treatment. Recursively add all .js,
56-
// .cjs, and .mjs files in the 'test' directory.
57-
const isTestDir = name === 'test';
58-
const { underTestDir } = options;
59-
const entries = readdirSync(path);
60-
61-
if (isTestDir) {
62-
options.underTestDir = true;
63-
}
64-
65-
options.userSupplied = false;
66-
67-
for (let i = 0; i < entries.length; i++) {
68-
processPath(join(path, entries[i]), testFiles, options);
69-
}
70-
71-
options.underTestDir = underTestDir;
72-
}
73-
}
74-
75-
function createTestFileList() {
76-
const cwd = process.cwd();
77-
const hasUserSuppliedPaths = process.argv.length > 1;
78-
const testPaths = hasUserSuppliedPaths ?
79-
ArrayPrototypeSlice(process.argv, 1) : [cwd];
80-
const testFiles = new SafeSet();
81-
82-
try {
83-
for (let i = 0; i < testPaths.length; i++) {
84-
const absolutePath = resolve(testPaths[i]);
85-
86-
processPath(absolutePath, testFiles, { userSupplied: true });
87-
}
88-
} catch (err) {
89-
if (err?.code === 'ENOENT') {
90-
console.error(`Could not find '${err.path}'`);
91-
process.exit(1);
92-
}
93-
94-
throw err;
95-
}
96-
97-
return ArrayPrototypeSort(ArrayFrom(testFiles));
98-
}
99-
100-
function filterExecArgv(arg) {
101-
return !ArrayPrototypeIncludes(kFilterArgs, arg);
102-
}
103-
104-
function runTestFile(path) {
105-
return test(path, async (t) => {
106-
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
107-
ArrayPrototypePush(args, path);
108-
109-
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
110-
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
111-
// instead of just displaying it all if the child fails.
112-
let err;
113-
114-
child.on('error', (error) => {
115-
err = error;
116-
});
117-
118-
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
119-
once(child, 'exit', { signal: t.signal }),
120-
child.stdout.toArray({ signal: t.signal }),
121-
child.stderr.toArray({ signal: t.signal }),
122-
]);
123-
124-
if (code !== 0 || signal !== null) {
125-
if (!err) {
126-
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
127-
err.exitCode = code;
128-
err.signal = signal;
129-
err.stdout = ArrayPrototypeJoin(stdout, '');
130-
err.stderr = ArrayPrototypeJoin(stderr, '');
131-
// The stack will not be useful since the failures came from tests
132-
// in a child process.
133-
err.stack = undefined;
134-
}
135-
136-
throw err;
137-
}
138-
});
139-
}
140-
141-
(async function main() {
142-
const testFiles = createTestFileList();
143-
144-
for (let i = 0; i < testFiles.length; i++) {
145-
runTestFile(testFiles[i]);
146-
}
147-
})();
11+
const tapStream = run();
12+
tapStream.pipe(process.stdout);
13+
tapStream.once('test:fail', () => {
14+
process.exitCode = 1;
15+
});

0 commit comments

Comments
 (0)