Skip to content

Commit 64d343a

Browse files
MoLowrichardlau
authored andcommitted
test_runner: support using --inspect with --test
PR-URL: #44520 Backport-PR-URL: #44873 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 12 additions & 1 deletion
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

Lines changed: 49 additions & 10 deletions
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

Lines changed: 2 additions & 3 deletions
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

Lines changed: 42 additions & 0 deletions
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

Lines changed: 0 additions & 3 deletions
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

Lines changed: 3 additions & 0 deletions
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
};
Lines changed: 40 additions & 0 deletions
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+
}
Lines changed: 19 additions & 0 deletions
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+
}

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