Skip to content

Commit 37e9092

Browse files
MoLowrichardlau
authored andcommitted
test_runner: support programmatically running --test
PR-URL: #44241 Backport-PR-URL: #44873 Fixes: #44023 Fixes: #43675 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 5a776d4 commit 37e9092

File tree

10 files changed

+424
-230
lines changed

10 files changed

+424
-230
lines changed

doc/api/test.md

+74
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
@@ -560,6 +589,47 @@ describe('tests', async () => {
560589
});
561590
```
562591

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

565635
<!-- YAML
@@ -825,6 +895,10 @@ added: v16.17.0
825895
[`--test`]: cli.md#--test
826896
[`SuiteContext`]: #class-suitecontext
827897
[`TestContext`]: #class-testcontext
898+
[`context.diagnostic`]: #contextdiagnosticmessage
899+
[`context.skip`]: #contextskipmessage
900+
[`context.todo`]: #contexttodomessage
901+
[`run()`]: #runoptions
828902
[`test()`]: #testname-options-fn
829903
[describe options]: #describename-options-fn
830904
[it options]: #testname-options-fn

lib/internal/main/test_runner.js

+6-138
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,14 @@
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
} = require('internal/bootstrap/pre_execution');
16-
const { spawn } = require('child_process');
17-
const { readdirSync, statSync } = require('fs');
18-
const console = require('internal/console/global');
19-
const {
20-
codes: {
21-
ERR_TEST_FAILURE,
22-
},
23-
} = require('internal/errors');
24-
const { test } = require('internal/test_runner/harness');
25-
const { kSubtestsFailed } = require('internal/test_runner/test');
26-
const {
27-
isSupportedFileType,
28-
doesPathMatchFilter,
29-
} = require('internal/test_runner/utils');
30-
const { basename, join, resolve } = require('path');
31-
const { once } = require('events');
32-
const kFilterArgs = ['--test'];
5+
const { run } = require('internal/test_runner/runner');
336

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

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

0 commit comments

Comments
 (0)