Skip to content

Commit 64d343a

Browse files
MoLowrichardlau
authored andcommittedDec 7, 2022
test_runner: support using --inspect with --test
PR-URL: #44520 Backport-PR-URL: #44873 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent 99ee5e4 commit 64d343a

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: REPLACEME
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
@@ -2,12 +2,23 @@
22
const {
33
prepareMainThreadExecution,
44
} = require('internal/bootstrap/pre_execution');
5+
const { isUsingInspector } = require('internal/util/inspector');
56
const { run } = require('internal/test_runner/runner');
67

78
prepareMainThreadExecution(false);
89
markBootstrapComplete();
910

10-
const tapStream = run();
11+
let concurrency = true;
12+
let inspectPort;
13+
14+
if (isUsingInspector()) {
15+
process.emitWarning('Using the inspector with --test forces running at a concurrency of 1. ' +
16+
'Use the inspectPort option to run with concurrency');
17+
concurrency = 1;
18+
inspectPort = process.debugPort;
19+
}
20+
21+
const tapStream = run({ concurrency, inspectPort });
1122
tapStream.pipe(process.stdout);
1223
tapStream.once('test:fail', () => {
1324
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; }
@@ -65,7 +104,10 @@ function wrapConsole(consoleFromNode, consoleFromVM) {
65104
// Stores the console from VM, should be set during bootstrap.
66105
let consoleFromVM;
67106
module.exports = {
107+
getInspectPort,
68108
installConsoleExtensions,
109+
isInspectorMessage,
110+
isUsingInspector,
69111
sendInspectorCommand,
70112
wrapConsole,
71113
get consoleFromVM() {

‎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);

‎test/parallel/test-runner-inspect.mjs

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as common from '../common/index.mjs';
2+
import * as tmpdir from '../common/tmpdir.js';
3+
import * as fixtures from '../common/fixtures.mjs';
4+
import assert from 'node:assert';
5+
import { NodeInstance } from '../common/inspector-helper.js';
6+
7+
8+
common.skipIfInspectorDisabled();
9+
tmpdir.refresh();
10+
11+
{
12+
const child = new NodeInstance(['--test', '--inspect-brk=0'], undefined, fixtures.path('test-runner/index.test.js'));
13+
14+
let stdout = '';
15+
let stderr = '';
16+
child.on('stdout', (line) => stdout += line);
17+
child.on('stderr', (line) => stderr += line);
18+
19+
const session = await child.connectInspectorSession();
20+
21+
await session.send([
22+
{ method: 'Runtime.enable' },
23+
{ method: 'Runtime.runIfWaitingForDebugger' }]);
24+
25+
session.disconnect();
26+
assert.match(stderr,
27+
/Warning: Using the inspector with --test forces running at a concurrency of 1\. Use the inspectPort option to run with concurrency/);
28+
}
29+
30+
31+
{
32+
const args = ['--test', '--inspect=0', fixtures.path('test-runner/index.js')];
33+
const { stderr, stdout, code, signal } = await common.spawnPromisified(process.execPath, args);
34+
35+
assert.match(stderr,
36+
/Warning: Using the inspector with --test forces running at a concurrency of 1\. Use the inspectPort option to run with concurrency/);
37+
assert.match(stdout, /not ok 1 - .+index\.js/);
38+
assert.match(stdout, /stderr: \|-\r?\n\s+Debugger listening on/);
39+
assert.strictEqual(code, 1);
40+
assert.strictEqual(signal, null);
41+
}
42+
43+
44+
{
45+
// File not found.
46+
const args = ['--test', '--inspect=0', 'a-random-file-that-does-not-exist.js'];
47+
const { stderr, stdout, code, signal } = await common.spawnPromisified(process.execPath, args);
48+
49+
assert.strictEqual(stdout, '');
50+
assert.match(stderr, /^Could not find/);
51+
assert.doesNotMatch(stderr, /Warning: Using the inspector with --test forces running at a concurrency of 1\. Use the inspectPort option to run with concurrency/);
52+
assert.strictEqual(code, 1);
53+
assert.strictEqual(signal, null);
54+
}
+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import * as common from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import assert from 'node:assert';
4+
5+
common.skipIfInspectorDisabled();
6+
7+
8+
const debuggerPort = common.getPort();
9+
10+
async function spawnRunner({ execArgv, expectedPort, expectedHost, expectedInitialPort, inspectPort }) {
11+
const { code, signal } = await common.spawnPromisified(
12+
process.execPath,
13+
['--expose-internals', '--no-warnings', ...execArgv, fixtures.path('test-runner/run_inspect.js')], {
14+
env: { ...process.env,
15+
expectedPort,
16+
inspectPort,
17+
expectedHost,
18+
expectedInitialPort }
19+
});
20+
assert.strictEqual(code, 0);
21+
assert.strictEqual(signal, null);
22+
}
23+
24+
let offset = 0;
25+
26+
const defaultPortCase = spawnRunner({
27+
execArgv: ['--inspect'],
28+
expectedPort: 9230,
29+
});
30+
31+
await spawnRunner({
32+
execArgv: ['--inspect=65535'],
33+
expectedPort: 1024,
34+
});
35+
36+
let port = debuggerPort + offset++ * 5;
37+
38+
await spawnRunner({
39+
execArgv: [`--inspect=${port}`],
40+
expectedPort: port + 1,
41+
});
42+
43+
port = debuggerPort + offset++ * 5;
44+
45+
await spawnRunner({
46+
execArgv: ['--inspect', `--inspect-port=${port}`],
47+
expectedPort: port + 1,
48+
});
49+
50+
port = debuggerPort + offset++ * 5;
51+
52+
await spawnRunner({
53+
execArgv: ['--inspect', `--debug-port=${port}`],
54+
expectedPort: port + 1,
55+
});
56+
57+
port = debuggerPort + offset++ * 5;
58+
59+
await spawnRunner({
60+
execArgv: [`--inspect=0.0.0.0:${port}`],
61+
expectedPort: port + 1, expectedHost: '0.0.0.0',
62+
});
63+
64+
port = debuggerPort + offset++ * 5;
65+
66+
await spawnRunner({
67+
execArgv: [`--inspect=127.0.0.1:${port}`],
68+
expectedPort: port + 1, expectedHost: '127.0.0.1'
69+
});
70+
71+
if (common.hasIPv6) {
72+
port = debuggerPort + offset++ * 5;
73+
74+
await spawnRunner({
75+
execArgv: [`--inspect=[::]:${port}`],
76+
expectedPort: port + 1, expectedHost: '::'
77+
});
78+
79+
port = debuggerPort + offset++ * 5;
80+
81+
await spawnRunner({
82+
execArgv: [`--inspect=[::1]:${port}`],
83+
expectedPort: port + 1, expectedHost: '::1'
84+
});
85+
}
86+
87+
// These tests check that setting inspectPort in run
88+
// would take effect and override port incrementing behavior
89+
90+
port = debuggerPort + offset++ * 5;
91+
92+
await spawnRunner({
93+
execArgv: [`--inspect=${port}`],
94+
inspectPort: port + 2,
95+
expectedPort: port + 2,
96+
});
97+
98+
port = debuggerPort + offset++ * 5;
99+
100+
await spawnRunner({
101+
execArgv: [`--inspect=${port}`],
102+
inspectPort: 'addTwo',
103+
expectedPort: port + 2,
104+
});
105+
106+
port = debuggerPort + offset++ * 5;
107+
108+
await spawnRunner({
109+
execArgv: [`--inspect=${port}`],
110+
inspectPort: 'string',
111+
});
112+
113+
port = debuggerPort + offset++ * 5;
114+
115+
await spawnRunner({
116+
execArgv: [`--inspect=${port}`],
117+
inspectPort: 'null',
118+
expectedPort: port + 1,
119+
});
120+
121+
port = debuggerPort + offset++ * 5;
122+
123+
await spawnRunner({
124+
execArgv: [`--inspect=${port}`],
125+
inspectPort: 'bignumber',
126+
});
127+
128+
port = debuggerPort + offset++ * 5;
129+
130+
await spawnRunner({
131+
execArgv: [`--inspect=${port}`],
132+
inspectPort: 'negativenumber',
133+
});
134+
135+
port = debuggerPort + offset++ * 5;
136+
137+
await spawnRunner({
138+
execArgv: [`--inspect=${port}`],
139+
inspectPort: 'bignumberfunc'
140+
});
141+
142+
port = debuggerPort + offset++ * 5;
143+
144+
await spawnRunner({
145+
execArgv: [`--inspect=${port}`],
146+
inspectPort: 'strfunc',
147+
});
148+
149+
port = debuggerPort + offset++ * 5;
150+
151+
await spawnRunner({
152+
execArgv: [`--inspect=${port}`],
153+
inspectPort: 0,
154+
expectedInitialPort: 0,
155+
});
156+
157+
await defaultPortCase;
158+
159+
port = debuggerPort + offset++ * 5;
160+
await spawnRunner({
161+
execArgv: ['--inspect'],
162+
inspectPort: port + 2,
163+
expectedInitialPort: port + 2,
164+
});

0 commit comments

Comments
 (0)
Please sign in to comment.