Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 37e9092

Browse filesBrowse 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 <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 5a776d4 commit 37e9092
Copy full SHA for 37e9092

File tree

Expand file treeCollapse file tree

10 files changed

+424
-230
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

10 files changed

+424
-230
lines changed
Open diff view settings
Collapse file

‎doc/api/test.md‎

Copy file name to clipboardExpand all lines: doc/api/test.md
+74Lines changed: 74 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/internal/main/test_runner.js‎

Copy file name to clipboard
+6-138Lines changed: 6 additions & 138 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.