Skip to content

Commit 6755536

Browse files
committed
feat: support using --inspect with --test
PR-URL: nodejs/node#44520 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> (cherry picked from commit a165193c5c8e4bcfbd12b2c3f6e55a81a251c258)
1 parent 27241c3 commit 6755536

File tree

7 files changed

+126
-27
lines changed

7 files changed

+126
-27
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,12 @@ added: REPLACEME
346346
fail after.
347347
If unspecified, subtests inherit this value from their parent.
348348
**Default:** `Infinity`.
349+
* `inspectPort` {number|Function} Sets inspector port of test child process.
350+
This can be a number, or a function that takes no arguments and returns a
351+
number. If a nullish value is provided, each process gets its own port,
352+
incremented from the primary's `process.debugPort`.
353+
**Default:** `undefined`.
354+
349355
* Returns: {TapStream}
350356

351357
```js

lib/internal/main/test_runner.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
// https://github.com/nodejs/node/blob/59527de13d39327eb3dfa8dedc92241eb40066d5/lib/internal/main/test_runner.js
1+
// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/lib/internal/main/test_runner.js
22
'use strict'
33
const {
44
prepareMainThreadExecution
55
} = require('#internal/process/pre_execution')
6+
const { isUsingInspector } = require('#internal/util/inspector')
67
const { run } = require('#internal/test_runner/runner')
78

89
prepareMainThreadExecution(false)
910
// markBootstrapComplete();
1011

11-
const tapStream = run()
12+
let concurrency = true
13+
let inspectPort
14+
15+
if (isUsingInspector()) {
16+
process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' +
17+
'Use the inspectPort option to run with concurrency')
18+
concurrency = 1
19+
inspectPort = process.debugPort
20+
}
21+
22+
const tapStream = run({ concurrency, inspectPort })
1223
tapStream.pipe(process.stdout)
1324
tapStream.once('test:fail', () => {
1425
process.exitCode = 1

lib/internal/per_context/primordials.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ exports.ArrayPrototypeForEach = (arr, fn, thisArg) => arr.forEach(fn, thisArg)
1010
exports.ArrayPrototypeIncludes = (arr, el, fromIndex) => arr.includes(el, fromIndex)
1111
exports.ArrayPrototypeJoin = (arr, str) => arr.join(str)
1212
exports.ArrayPrototypeMap = (arr, mapFn) => arr.map(mapFn)
13+
exports.ArrayPrototypePop = arr => arr.pop()
1314
exports.ArrayPrototypePush = (arr, ...el) => arr.push(...el)
1415
exports.ArrayPrototypeReduce = (arr, fn, originalVal) => arr.reduce(fn, originalVal)
1516
exports.ArrayPrototypeShift = arr => arr.shift()
1617
exports.ArrayPrototypeSlice = (arr, offset) => arr.slice(offset)
18+
exports.ArrayPrototypeSome = (arr, fn) => arr.some(fn)
1719
exports.ArrayPrototypeSort = (arr, fn) => arr.sort(fn)
1820
exports.ArrayPrototypeUnshift = (arr, ...el) => arr.unshift(...el)
1921
exports.Error = Error
@@ -38,13 +40,15 @@ exports.PromiseAll = iterator => Promise.all(iterator)
3840
exports.PromisePrototypeThen = (promise, thenFn, catchFn) => promise.then(thenFn, catchFn)
3941
exports.PromiseResolve = val => Promise.resolve(val)
4042
exports.PromiseRace = val => Promise.race(val)
43+
exports.RegExpPrototypeSymbolSplit = (reg, str) => reg[Symbol.split](str)
4144
exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }}
4245
exports.SafeMap = Map
4346
exports.SafePromiseAll = (array, mapFn) => Promise.all(mapFn ? array.map(mapFn) : array)
4447
exports.SafePromiseRace = (array, mapFn) => Promise.race(mapFn ? array.map(mapFn) : array)
4548
exports.SafeSet = Set
4649
exports.SafeWeakMap = WeakMap
4750
exports.SafeWeakSet = WeakSet
51+
exports.StringPrototypeEndsWith = (haystack, needle, index) => haystack.endsWith(needle, index)
4852
exports.StringPrototypeIncludes = (str, needle) => str.includes(needle)
4953
exports.StringPrototypeMatch = (str, reg) => str.match(reg)
5054
exports.StringPrototypeReplace = (str, search, replacement) =>

lib/internal/test_runner/runner.js

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
// https://github.com/nodejs/node/blob/59527de13d39327eb3dfa8dedc92241eb40066d5/lib/internal/test_runner/runner.js
1+
// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/lib/internal/test_runner/runner.js
22
'use strict'
33
const {
44
ArrayFrom,
5-
ArrayPrototypeConcat,
65
ArrayPrototypeFilter,
76
ArrayPrototypeIncludes,
87
ArrayPrototypeJoin,
8+
ArrayPrototypePop,
9+
ArrayPrototypePush,
910
ArrayPrototypeSlice,
1011
ArrayPrototypeSort,
1112
ObjectAssign,
1213
PromisePrototypeThen,
14+
RegExpPrototypeSymbolSplit,
1315
SafePromiseAll,
14-
SafeSet
16+
SafeSet,
17+
StringPrototypeEndsWith
1518
} = require('#internal/per_context/primordials')
1619

20+
const { Buffer } = require('buffer')
1721
const { spawn } = require('child_process')
1822
const { readdirSync, statSync } = require('fs')
1923
const {
@@ -23,6 +27,7 @@ const {
2327
} = require('#internal/errors')
2428
const { toArray } = require('#internal/streams/operators').promiseReturningOperators
2529
const { validateArray } = require('#internal/validators')
30+
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('#internal/util/inspector')
2631
const { kEmptyObject } = require('#internal/util')
2732
const { createTestTree } = require('#internal/test_runner/harness')
2833
const { kSubtestsFailed, Test } = require('#internal/test_runner/test')
@@ -102,25 +107,59 @@ function filterExecArgv (arg) {
102107
return !ArrayPrototypeIncludes(kFilterArgs, arg)
103108
}
104109

105-
function runTestFile (path, root) {
110+
function getRunArgs ({ path, inspectPort }) {
111+
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv)
112+
if (isUsingInspector()) {
113+
ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`)
114+
}
115+
ArrayPrototypePush(argv, path)
116+
return argv
117+
}
118+
119+
function makeStderrCallback (callback) {
120+
if (!isUsingInspector()) {
121+
return callback
122+
}
123+
let buffer = Buffer.alloc(0)
124+
return (data) => {
125+
callback(data)
126+
const newData = Buffer.concat([buffer, data])
127+
const str = newData.toString('utf8')
128+
let lines = str
129+
if (StringPrototypeEndsWith(lines, '\n')) {
130+
buffer = Buffer.alloc(0)
131+
} else {
132+
lines = RegExpPrototypeSymbolSplit(/\r?\n/, str)
133+
buffer = Buffer.from(ArrayPrototypePop(lines), 'utf8')
134+
lines = ArrayPrototypeJoin(lines, '\n')
135+
}
136+
if (isInspectorMessage(lines)) {
137+
process.stderr.write(lines)
138+
}
139+
}
140+
}
141+
142+
function runTestFile (path, root, inspectPort) {
106143
const subtest = root.createSubtest(Test, path, async (t) => {
107-
const args = ArrayPrototypeConcat(
108-
ArrayPrototypeFilter(process.execArgv, filterExecArgv),
109-
path)
144+
const args = getRunArgs({ path, inspectPort })
110145

111146
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' })
112147
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
113148
// instead of just displaying it all if the child fails.
114149
let err
150+
let stderr = ''
115151

116152
child.on('error', (error) => {
117153
err = error
118154
})
119155

120-
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
156+
child.stderr.on('data', makeStderrCallback((data) => {
157+
stderr += data
158+
}))
159+
160+
const { 0: { 0: code, 1: signal }, 1: stdout } = await SafePromiseAll([
121161
once(child, 'exit', { signal: t.signal }),
122-
toArray.call(child.stdout, { signal: t.signal }),
123-
toArray.call(child.stderr, { signal: t.signal })
162+
toArray.call(child.stdout, { signal: t.signal })
124163
])
125164

126165
if (code !== 0 || signal !== null) {
@@ -130,7 +169,7 @@ function runTestFile (path, root) {
130169
exitCode: code,
131170
signal,
132171
stdout: ArrayPrototypeJoin(stdout, ''),
133-
stderr: ArrayPrototypeJoin(stderr, ''),
172+
stderr,
134173
// The stack will not be useful since the failures came from tests
135174
// in a child process.
136175
stack: undefined
@@ -147,7 +186,7 @@ function run (options) {
147186
if (options === null || typeof options !== 'object') {
148187
options = kEmptyObject
149188
}
150-
const { concurrency, timeout, signal, files } = options
189+
const { concurrency, timeout, signal, files, inspectPort } = options
151190

152191
if (files != null) {
153192
validateArray(files, 'options.files')
@@ -156,7 +195,7 @@ function run (options) {
156195
const root = createTestTree({ concurrency, timeout, signal })
157196
const testFiles = files ?? createTestFileList()
158197

159-
PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root)),
198+
PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)),
160199
() => root.postRun())
161200

162201
return root.reporter

lib/internal/test_runner/test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// https://github.com/nodejs/node/blob/6ee1f3444f8c1cf005153f936ffc74221d55658b/lib/internal/test_runner/test.js
1+
// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/lib/internal/test_runner/test.js
22

33
'use strict'
44

@@ -60,8 +60,6 @@ const kDefaultTimeout = null
6060
const noop = FunctionPrototype
6161
const isTestRunner = getOptionValue('--test')
6262
const testOnlyFlag = !isTestRunner && getOptionValue('--test-only')
63-
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
64-
const rootConcurrency = isTestRunner ? MathMax(cpus().length - 1, 1) : 1
6563
const kShouldAbort = Symbol('kShouldAbort')
6664
const kRunHook = Symbol('kRunHook')
6765
const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach'])
@@ -148,7 +146,7 @@ class Test extends AsyncResource {
148146
}
149147

150148
if (parent === null) {
151-
this.concurrency = rootConcurrency
149+
this.concurrency = 1
152150
this.indent = ''
153151
this.indentString = kDefaultIndent
154152
this.only = testOnlyFlag

lib/internal/util/inspector.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/lib/internal/util/inspector.js
2+
const {
3+
ArrayPrototypeSome,
4+
RegExpPrototypeExec
5+
} = require('#internal/per_context/primordials')
6+
7+
const { validatePort } = require('#internal/validators')
8+
9+
const kMinPort = 1024
10+
const kMaxPort = 65535
11+
const kInspectArgRegex = /--inspect(?:-brk|-port)?|--debug-port/
12+
const kInspectMsgRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\/|Debugger attached|Waiting for the debugger to disconnect\.\.\./
13+
14+
let _isUsingInspector
15+
function isUsingInspector () {
16+
// Node.js 14.x does not support Logical_nullish_assignment operator
17+
_isUsingInspector = _isUsingInspector ??
18+
(ArrayPrototypeSome(process.execArgv, (arg) => RegExpPrototypeExec(kInspectArgRegex, arg) !== null) ||
19+
RegExpPrototypeExec(kInspectArgRegex, process.env.NODE_OPTIONS) !== null)
20+
return _isUsingInspector
21+
}
22+
23+
let debugPortOffset = 1
24+
function getInspectPort (inspectPort) {
25+
if (!isUsingInspector()) {
26+
return null
27+
}
28+
if (typeof inspectPort === 'function') {
29+
inspectPort = inspectPort()
30+
} else if (inspectPort == null) {
31+
inspectPort = process.debugPort + debugPortOffset
32+
if (inspectPort > kMaxPort) { inspectPort = inspectPort - kMaxPort + kMinPort - 1 }
33+
debugPortOffset++
34+
}
35+
validatePort(inspectPort)
36+
37+
return inspectPort
38+
}
39+
40+
function isInspectorMessage (string) {
41+
return isUsingInspector() && RegExpPrototypeExec(kInspectMsgRegex, string) !== null
42+
}
43+
44+
module.exports = {
45+
isUsingInspector,
46+
getInspectPort,
47+
isInspectorMessage
48+
}

test/parallel/test-runner-cli.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// https://github.com/nodejs/node/blob/1aab13cad9c800f4121c1d35b554b78c1b17bdbd/test/parallel/test-runner-cli.js
1+
// https://github.com/nodejs/node/blob/a165193c5c8e4bcfbd12b2c3f6e55a81a251c258/test/parallel/test-runner-cli.js
22
'use strict'
33
require('../common')
44
const assert = require('assert')
@@ -106,13 +106,6 @@ const testFixtures = fixtures.path('test-runner')
106106
// ['--print', 'console.log("should not print")', '--test']
107107
// ]
108108

109-
// if (process.features.inspector) {
110-
// flags.push(
111-
// // ['--inspect', '--test'],
112-
// // ['--inspect-brk', '--test']
113-
// )
114-
// }
115-
116109
// flags.forEach((args) => {
117110
// const child = spawnSync(process.execPath, args)
118111

0 commit comments

Comments
 (0)