Skip to content

Commit dda28df

Browse files
committed
test_runner: support programmatically running --test
PR-URL: nodejs/node#44241 Fixes: nodejs/node#44023 Fixes: nodejs/node#43675 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> (cherry picked from commit 59527de13d39327eb3dfa8dedc92241eb40066d5)
1 parent 54cbb36 commit dda28df

File tree

14 files changed

+479
-240
lines changed

14 files changed

+479
-240
lines changed

README.md

+74
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,35 @@ Otherwise, the test is considered to be a failure. Test files must be
324324
executable by Node.js, but are not required to use the `node:test` module
325325
internally.
326326

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

329358
- `name` {string} The name of the test, which is displayed when reporting test
@@ -541,6 +570,47 @@ describe('tests', async () => {
541570
});
542571
```
543572

573+
## Class: `TapStream`
574+
575+
<!-- YAML
576+
added: REPLACEME
577+
-->
578+
579+
* Extends {ReadableStream}
580+
581+
A successful call to [`run()`][] method will return a new {TapStream}
582+
object, streaming a [TAP][] output
583+
`TapStream` will emit events, in the order of the tests definition
584+
585+
### Event: `'test:diagnostic'`
586+
587+
* `message` {string} The diagnostic message.
588+
589+
Emitted when [`context.diagnostic`][] is called.
590+
591+
### Event: `'test:fail'`
592+
593+
* `data` {Object}
594+
* `duration` {number} The test duration.
595+
* `error` {Error} The failure casing test to fail.
596+
* `name` {string} The test name.
597+
* `testNumber` {number} The ordinal number of the test.
598+
* `todo` {string|undefined} Present if [`context.todo`][] is called
599+
* `skip` {string|undefined} Present if [`context.skip`][] is called
600+
601+
Emitted when a test fails.
602+
603+
### Event: `'test:pass'`
604+
605+
* `data` {Object}
606+
* `duration` {number} The test duration.
607+
* `name` {string} The test name.
608+
* `testNumber` {number} The ordinal number of the test.
609+
* `todo` {string|undefined} Present if [`context.todo`][] is called
610+
* `skip` {string|undefined} Present if [`context.skip`][] is called
611+
612+
Emitted when a test passes.
613+
544614
## Class: `TestContext`
545615

546616
An instance of `TestContext` is passed to each test function in order to
@@ -712,6 +782,10 @@ The name of the suite.
712782
[TAP]: https://testanything.org/
713783
[`SuiteContext`]: #class-suitecontext
714784
[`TestContext`]: #class-testcontext
785+
[`context.diagnostic`]: #contextdiagnosticmessage
786+
[`context.skip`]: #contextskipmessage
787+
[`context.todo`]: #contexttodomessage
788+
[`run()`]: #runoptions
715789
[`test()`]: #testname-options-fn
716790
[describe options]: #describename-options-fn
717791
[it options]: #testname-options-fn

lib/internal/main/test_runner.js

+8-141
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,15 @@
1-
// https://github.com/nodejs/node/blob/2fd4c013c221653da2a7921d08fe1aa96aaba504/lib/internal/main/test_runner.js
1+
// https://github.com/nodejs/node/blob/59527de13d39327eb3dfa8dedc92241eb40066d5/lib/internal/main/test_runner.js
22
'use strict'
3-
const {
4-
ArrayFrom,
5-
ArrayPrototypeFilter,
6-
ArrayPrototypeIncludes,
7-
ArrayPrototypeJoin,
8-
ArrayPrototypePush,
9-
ArrayPrototypeSlice,
10-
ArrayPrototypeSort,
11-
SafePromiseAll,
12-
SafeSet
13-
} = require('#internal/per_context/primordials')
143
const {
154
prepareMainThreadExecution
16-
} = require('#internal/bootstrap/pre_execution')
17-
const { spawn } = require('child_process')
18-
const { readdirSync, statSync } = require('fs')
19-
const {
20-
codes: {
21-
ERR_TEST_FAILURE
22-
}
23-
} = require('#internal/errors')
24-
const { toArray } = require('#internal/streams/operators').promiseReturningOperators
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']
5+
} = require('#internal/process/pre_execution')
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)
80-
: [cwd]
81-
const testFiles = new SafeSet()
82-
83-
try {
84-
for (let i = 0; i < testPaths.length; i++) {
85-
const absolutePath = resolve(testPaths[i])
86-
87-
processPath(absolutePath, testFiles, { userSupplied: true })
88-
}
89-
} catch (err) {
90-
if (err?.code === 'ENOENT') {
91-
console.error(`Could not find '${err.path}'`)
92-
process.exit(1)
93-
}
94-
95-
throw err
96-
}
97-
98-
return ArrayPrototypeSort(ArrayFrom(testFiles))
99-
}
100-
101-
function filterExecArgv (arg) {
102-
return !ArrayPrototypeIncludes(kFilterArgs, arg)
103-
}
104-
105-
function runTestFile (path) {
106-
return test(path, async (t) => {
107-
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv)
108-
ArrayPrototypePush(args, path)
109-
110-
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' })
111-
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
112-
// instead of just displaying it all if the child fails.
113-
let err
114-
115-
child.on('error', (error) => {
116-
err = error
117-
})
118-
119-
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
120-
once(child, 'exit', { signal: t.signal }),
121-
toArray.call(child.stdout, { signal: t.signal }),
122-
toArray.call(child.stderr, { signal: t.signal })
123-
])
124-
125-
if (code !== 0 || signal !== null) {
126-
if (!err) {
127-
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed)
128-
err.exitCode = code
129-
err.signal = signal
130-
err.stdout = ArrayPrototypeJoin(stdout, '')
131-
err.stderr = ArrayPrototypeJoin(stderr, '')
132-
// The stack will not be useful since the failures came from tests
133-
// in a child process.
134-
err.stack = undefined
135-
}
136-
137-
throw err
138-
}
139-
})
140-
}
141-
142-
;(async function main () {
143-
const testFiles = createTestFileList()
144-
145-
for (let i = 0; i < testFiles.length; i++) {
146-
runTestFile(testFiles[i])
147-
}
148-
})()
11+
const tapStream = run()
12+
tapStream.pipe(process.stdout)
13+
tapStream.once('test:fail', () => {
14+
process.exitCode = 1
15+
})

lib/internal/per_context/primordials.js

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
const replaceAll = require('string.prototype.replaceall')
44

55
exports.ArrayFrom = (it, mapFn) => Array.from(it, mapFn)
6+
exports.ArrayIsArray = Array.isArray
7+
exports.ArrayPrototypeConcat = (arr, ...el) => arr.concat(...el)
68
exports.ArrayPrototypeFilter = (arr, fn) => arr.filter(fn)
79
exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg)
810
exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex)
@@ -20,6 +22,7 @@ exports.FunctionPrototype = Function.prototype
2022
exports.FunctionPrototypeBind = (fn, obj, ...args) => fn.bind(obj, ...args)
2123
exports.MathMax = (...args) => Math.max(...args)
2224
exports.Number = Number
25+
exports.ObjectAssign = (target, ...sources) => Object.assign(target, ...sources)
2326
exports.ObjectCreate = obj => Object.create(obj)
2427
exports.ObjectDefineProperties = (obj, props) => Object.defineProperties(obj, props)
2528
exports.ObjectDefineProperty = (obj, key, descr) => Object.defineProperty(obj, key, descr)
@@ -41,6 +44,7 @@ exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn)
4144
exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array)
4245
exports.SafeSet = Set
4346
exports.SafeWeakMap = WeakMap
47+
exports.SafeWeakSet = WeakSet
4448
exports.StringPrototypeIncludes = (str, needle) => str.includes(needle)
4549
exports.StringPrototypeMatch = (str, reg) => str.match(reg)
4650
exports.StringPrototypeReplace = (str, search, replacement) =>

0 commit comments

Comments
 (0)