Skip to content

Commit 3c1e9d4

Browse files
MoLowdanielleadams
authored andcommitted
test_runner: support using --inspect with --test
PR-URL: #44520 Backport-PR-URL: #44813 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 36227ed commit 3c1e9d4

File tree

13 files changed

+391
-23
lines changed

13 files changed

+391
-23
lines changed

doc/api/test.md

+5
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,11 @@ added: v18.9.0
338338
fail after.
339339
If unspecified, subtests inherit this value from their parent.
340340
**Default:** `Infinity`.
341+
* `inspectPort` {number|Function} Sets inspector port of test child process.
342+
This can be a number, or a function that takes no arguments and returns a
343+
number. If a nullish value is provided, each process gets its own port,
344+
incremented from the primary's `process.debugPort`.
345+
**Default:** `undefined`.
341346
* Returns: {TapStream}
342347

343348
```js

lib/internal/cluster/primary.js

+1
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ function createWorkerProcess(id, env) {
120120
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
121121
const nodeOptions = process.env.NODE_OPTIONS || '';
122122

123+
// TODO(MoLow): Use getInspectPort from internal/util/inspector
123124
if (ArrayPrototypeSome(execArgv,
124125
(arg) => RegExpPrototypeExec(debugArgRegex, arg) !== null) ||
125126
RegExpPrototypeExec(debugArgRegex, nodeOptions) !== null) {

lib/internal/main/test_runner.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ const {
33
prepareMainThreadExecution,
44
markBootstrapComplete
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/test_runner/runner.js

+49-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
'use strict';
22
const {
33
ArrayFrom,
4-
ArrayPrototypeConcat,
54
ArrayPrototypeFilter,
65
ArrayPrototypeIncludes,
76
ArrayPrototypeJoin,
7+
ArrayPrototypePop,
8+
ArrayPrototypePush,
89
ArrayPrototypeSlice,
910
ArrayPrototypeSort,
1011
ObjectAssign,
1112
PromisePrototypeThen,
13+
RegExpPrototypeSymbolSplit,
1214
SafePromiseAll,
1315
SafeSet,
16+
StringPrototypeEndsWith,
1417
} = primordials;
1518

19+
const { Buffer } = require('buffer');
1620
const { spawn } = require('child_process');
1721
const { readdirSync, statSync } = require('fs');
1822
const console = require('internal/console/global');
@@ -22,6 +26,7 @@ const {
2226
},
2327
} = require('internal/errors');
2428
const { validateArray } = require('internal/validators');
29+
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
2530
const { kEmptyObject } = require('internal/util');
2631
const { createTestTree } = require('internal/test_runner/harness');
2732
const { kSubtestsFailed, Test } = require('internal/test_runner/test');
@@ -100,25 +105,59 @@ function filterExecArgv(arg) {
100105
return !ArrayPrototypeIncludes(kFilterArgs, arg);
101106
}
102107

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

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

114150
child.on('error', (error) => {
115151
err = error;
116152
});
117153

118-
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
154+
child.stderr.on('data', makeStderrCallback((data) => {
155+
stderr += data;
156+
}));
157+
158+
const { 0: { 0: code, 1: signal }, 1: stdout } = await SafePromiseAll([
119159
once(child, 'exit', { signal: t.signal }),
120160
child.stdout.toArray({ signal: t.signal }),
121-
child.stderr.toArray({ signal: t.signal }),
122161
]);
123162

124163
if (code !== 0 || signal !== null) {
@@ -128,7 +167,7 @@ function runTestFile(path, root) {
128167
exitCode: code,
129168
signal: signal,
130169
stdout: ArrayPrototypeJoin(stdout, ''),
131-
stderr: ArrayPrototypeJoin(stderr, ''),
170+
stderr,
132171
// The stack will not be useful since the failures came from tests
133172
// in a child process.
134173
stack: undefined,
@@ -145,7 +184,7 @@ function run(options) {
145184
if (options === null || typeof options !== 'object') {
146185
options = kEmptyObject;
147186
}
148-
const { concurrency, timeout, signal, files } = options;
187+
const { concurrency, timeout, signal, files, inspectPort } = options;
149188

150189
if (files != null) {
151190
validateArray(files, 'options.files');
@@ -154,7 +193,7 @@ function run(options) {
154193
const root = createTestTree({ concurrency, timeout, signal });
155194
const testFiles = files ?? createTestFileList();
156195

157-
PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root)),
196+
PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)),
158197
() => root.postRun());
159198

160199
return root.reporter;

lib/internal/test_runner/test.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ const kDefaultTimeout = null;
5858
const noop = FunctionPrototype;
5959
const isTestRunner = getOptionValue('--test');
6060
const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
61-
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
62-
const rootConcurrency = isTestRunner ? MathMax(cpus().length - 1, 1) : 1;
6361
const kShouldAbort = Symbol('kShouldAbort');
6462
const kRunHook = Symbol('kRunHook');
6563
const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);
@@ -150,7 +148,7 @@ class Test extends AsyncResource {
150148
}
151149

152150
if (parent === null) {
153-
this.concurrency = rootConcurrency;
151+
this.concurrency = 1;
154152
this.indent = '';
155153
this.indentString = kDefaultIndent;
156154
this.only = testOnlyFlag;
@@ -180,6 +178,7 @@ class Test extends AsyncResource {
180178

181179
case 'boolean':
182180
if (concurrency) {
181+
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
183182
this.concurrency = parent === null ? MathMax(cpus().length - 1, 1) : Infinity;
184183
} else {
185184
this.concurrency = 1;

lib/internal/util/inspector.js

+42
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,47 @@
22

33
const {
44
ArrayPrototypeConcat,
5+
ArrayPrototypeSome,
56
FunctionPrototypeBind,
67
ObjectDefineProperty,
78
ObjectKeys,
89
ObjectPrototypeHasOwnProperty,
10+
RegExpPrototypeExec,
911
} = primordials;
1012

13+
const { validatePort } = require('internal/validators');
14+
15+
const kMinPort = 1024;
16+
const kMaxPort = 65535;
17+
const kInspectArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
18+
const kInspectMsgRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\/|Debugger attached|Waiting for the debugger to disconnect\.\.\./;
19+
20+
let _isUsingInspector;
21+
function isUsingInspector() {
22+
_isUsingInspector ??=
23+
ArrayPrototypeSome(process.execArgv, (arg) => RegExpPrototypeExec(kInspectArgRegex, arg) !== null) ||
24+
RegExpPrototypeExec(kInspectArgRegex, process.env.NODE_OPTIONS) !== null;
25+
return _isUsingInspector;
26+
}
27+
28+
let debugPortOffset = 1;
29+
function getInspectPort(inspectPort) {
30+
if (!isUsingInspector()) {
31+
return null;
32+
}
33+
if (typeof inspectPort === 'function') {
34+
inspectPort = inspectPort();
35+
} else if (inspectPort == null) {
36+
inspectPort = process.debugPort + debugPortOffset;
37+
if (inspectPort > kMaxPort)
38+
inspectPort = inspectPort - kMaxPort + kMinPort - 1;
39+
debugPortOffset++;
40+
}
41+
validatePort(inspectPort);
42+
43+
return inspectPort;
44+
}
45+
1146
let session;
1247
function sendInspectorCommand(cb, onError) {
1348
const { hasInspector } = internalBinding('config');
@@ -22,6 +57,10 @@ function sendInspectorCommand(cb, onError) {
2257
}
2358
}
2459

60+
function isInspectorMessage(string) {
61+
return isUsingInspector() && RegExpPrototypeExec(kInspectMsgRegex, string) !== null;
62+
}
63+
2564
// Create a special require function for the inspector command line API
2665
function installConsoleExtensions(commandLineApi) {
2766
if (commandLineApi.require) { return; }
@@ -63,7 +102,10 @@ function wrapConsole(consoleFromNode) {
63102
}
64103

65104
module.exports = {
105+
getInspectPort,
66106
installConsoleExtensions,
107+
isInspectorMessage,
108+
isUsingInspector,
67109
sendInspectorCommand,
68110
wrapConsole,
69111
};

src/node_options.cc

-3
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,6 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
161161
errors->push_back("either --test or --watch can be used, not both");
162162
}
163163

164-
if (debug_options_.inspector_enabled) {
165-
errors->push_back("the inspector cannot be used with --test");
166-
}
167164
#ifndef ALLOW_ATTACHING_DEBUGGER_IN_TEST_RUNNER
168165
debug_options_.allow_attaching_debugger = false;
169166
#endif

test/common/index.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ const {
5252
spawnPromisified,
5353
} = common;
5454

55+
const getPort = () => common.PORT;
56+
5557
export {
5658
isMainThread,
5759
isWindows,
@@ -100,4 +102,5 @@ export {
100102
runWithInvalidFD,
101103
createRequire,
102104
spawnPromisified,
105+
getPort,
103106
};
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const fixtures = require('../../common/fixtures');
5+
const { run } = require('node:test');
6+
const assert = require('node:assert');
7+
8+
const badPortError = { name: 'RangeError', code: 'ERR_SOCKET_BAD_PORT' };
9+
let inspectPort = 'inspectPort' in process.env ? Number(process.env.inspectPort) : undefined;
10+
let expectedError;
11+
12+
if (process.env.inspectPort === 'addTwo') {
13+
inspectPort = common.mustCall(() => { return process.debugPort += 2; });
14+
} else if (process.env.inspectPort === 'string') {
15+
inspectPort = 'string';
16+
expectedError = badPortError;
17+
} else if (process.env.inspectPort === 'null') {
18+
inspectPort = null;
19+
} else if (process.env.inspectPort === 'bignumber') {
20+
inspectPort = 1293812;
21+
expectedError = badPortError;
22+
} else if (process.env.inspectPort === 'negativenumber') {
23+
inspectPort = -9776;
24+
expectedError = badPortError;
25+
} else if (process.env.inspectPort === 'bignumberfunc') {
26+
inspectPort = common.mustCall(() => 123121);
27+
expectedError = badPortError;
28+
} else if (process.env.inspectPort === 'strfunc') {
29+
inspectPort = common.mustCall(() => 'invalidPort');
30+
expectedError = badPortError;
31+
}
32+
33+
const stream = run({ files: [fixtures.path('test-runner/run_inspect_assert.js')], inspectPort });
34+
if (expectedError) {
35+
stream.on('test:fail', common.mustCall(({ error }) => {
36+
assert.deepStrictEqual({ name: error.cause.name, code: error.cause.code }, expectedError);
37+
}));
38+
} else {
39+
stream.on('test:fail', common.mustNotCall());
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
const assert = require('node:assert');
4+
5+
const { expectedPort, expectedInitialPort, expectedHost } = process.env;
6+
const debugOptions =
7+
require('internal/options').getOptionValue('--inspect-port');
8+
9+
if ('expectedPort' in process.env) {
10+
assert.strictEqual(process.debugPort, +expectedPort);
11+
}
12+
13+
if ('expectedInitialPort' in process.env) {
14+
assert.strictEqual(debugOptions.port, +expectedInitialPort);
15+
}
16+
17+
if ('expectedHost' in process.env) {
18+
assert.strictEqual(debugOptions.host, expectedHost);
19+
}

test/parallel/test-runner-cli.js

-6
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,6 @@ const testFixtures = fixtures.path('test-runner');
104104
['--print', 'console.log("should not print")', '--test'],
105105
];
106106

107-
if (process.features.inspector) {
108-
flags.push(
109-
['--inspect', '--test'],
110-
['--inspect-brk', '--test'],
111-
);
112-
}
113107

114108
flags.forEach((args) => {
115109
const child = spawnSync(process.execPath, args);

0 commit comments

Comments
 (0)