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 <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
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

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
@@ -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

Lines 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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy