Skip to content

Commit 8175c65

Browse files
MoLowRafaelGSS
authored andcommitted
test_runner: support programmatically running --test
PR-URL: #44241 Fixes: #44023 Fixes: #43675 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 57da3db commit 8175c65

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: v18.7.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,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)