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 <[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‎

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
constdebugArgRegex=/--inspect(?:-brk|-port)?|--debug-port/;
121121
constnodeOptions=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-
consttapStream=run();
11+
letconcurrency=true;
12+
letinspectPort;
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+
consttapStream=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
constconsole=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-
functionrunTestFile(path,root){
108+
functiongetRunArgs({ path, inspectPort }){
109+
constargv=ArrayPrototypeFilter(process.execArgv,filterExecArgv);
110+
if(isUsingInspector()){
111+
ArrayPrototypePush(argv,`--inspect-port=${getInspectPort(inspectPort)}`);
112+
}
113+
ArrayPrototypePush(argv,path);
114+
returnargv;
115+
}
116+
117+
functionmakeStderrCallback(callback){
118+
if(!isUsingInspector()){
119+
returncallback;
120+
}
121+
letbuffer=Buffer.alloc(0);
122+
return(data)=>{
123+
callback(data);
124+
constnewData=Buffer.concat([buffer,data]);
125+
conststr=newData.toString('utf8');
126+
letlines=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+
functionrunTestFile(path,root,inspectPort){
104141
constsubtest=root.createSubtest(Test,path,async(t)=>{
105-
constargs=ArrayPrototypeConcat(
106-
ArrayPrototypeFilter(process.execArgv,filterExecArgv),
107-
path);
142+
constargs=getRunArgs({ path, inspectPort });
108143

109144
constchild=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
leterr;
148+
letstderr='';
113149

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

118-
const{0: {0: code,1: signal},1: stdout,2: stderr}=awaitSafePromiseAll([
154+
child.stderr.on('data',makeStderrCallback((data)=>{
155+
stderr+=data;
156+
}));
157+
158+
const{0: {0: code,1: signal},1: stdout}=awaitSafePromiseAll([
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||typeofoptions!=='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
constroot=createTestTree({ concurrency, timeout, signal });
155194
consttestFiles=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
returnroot.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
constnoop=FunctionPrototype;
5959
constisTestRunner=getOptionValue('--test');
6060
consttestOnlyFlag=!isTestRunner&&getOptionValue('--test-only');
61-
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
62-
constrootConcurrency=isTestRunner ? MathMax(cpus().length-1,1) : 1;
6361
constkShouldAbort=Symbol('kShouldAbort');
6462
constkRunHook=Symbol('kRunHook');
6563
constkHookNames=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+
constkMinPort=1024;
16+
constkMaxPort=65535;
17+
constkInspectArgRegex=/--inspect(?:-brk|-port)?|--debug-port/;
18+
constkInspectMsgRegex=/Debuggerlisteningonws:\/\/\[?(.+?)\]?:(\d+)\/|Debuggerattached|Waitingforthedebuggertodisconnect\.\.\./;
19+
20+
let_isUsingInspector;
21+
functionisUsingInspector(){
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+
letdebugPortOffset=1;
29+
functiongetInspectPort(inspectPort){
30+
if(!isUsingInspector()){
31+
returnnull;
32+
}
33+
if(typeofinspectPort==='function'){
34+
inspectPort=inspectPort();
35+
}elseif(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+
returninspectPort;
44+
}
45+
1146
letsession;
1247
functionsendInspectorCommand(cb,onError){
1348
const{ hasInspector }=internalBinding('config');
@@ -22,6 +57,10 @@ function sendInspectorCommand(cb, onError){
2257
}
2358
}
2459

60+
functionisInspectorMessage(string){
61+
returnisUsingInspector()&&RegExpPrototypeExec(kInspectMsgRegex,string)!==null;
62+
}
63+
2564
// Create a special require function for the inspector command line API
2665
functioninstallConsoleExtensions(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
letconsoleFromVM;
67106
module.exports={
107+
getInspectPort,
68108
installConsoleExtensions,
109+
isInspectorMessage,
110+
isUsingInspector,
69111
sendInspectorCommand,
70112
wrapConsole,
71113
getconsoleFromVM(){

‎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+
constgetPort=()=>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+
constcommon=require('../../common');
4+
constfixtures=require('../../common/fixtures');
5+
const{ run }=require('node:test');
6+
constassert=require('node:assert');
7+
8+
constbadPortError={name: 'RangeError',code: 'ERR_SOCKET_BAD_PORT'};
9+
letinspectPort='inspectPort'inprocess.env ? Number(process.env.inspectPort) : undefined;
10+
letexpectedError;
11+
12+
if(process.env.inspectPort==='addTwo'){
13+
inspectPort=common.mustCall(()=>{returnprocess.debugPort+=2;});
14+
}elseif(process.env.inspectPort==='string'){
15+
inspectPort='string';
16+
expectedError=badPortError;
17+
}elseif(process.env.inspectPort==='null'){
18+
inspectPort=null;
19+
}elseif(process.env.inspectPort==='bignumber'){
20+
inspectPort=1293812;
21+
expectedError=badPortError;
22+
}elseif(process.env.inspectPort==='negativenumber'){
23+
inspectPort=-9776;
24+
expectedError=badPortError;
25+
}elseif(process.env.inspectPort==='bignumberfunc'){
26+
inspectPort=common.mustCall(()=>123121);
27+
expectedError=badPortError;
28+
}elseif(process.env.inspectPort==='strfunc'){
29+
inspectPort=common.mustCall(()=>'invalidPort');
30+
expectedError=badPortError;
31+
}
32+
33+
conststream=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+
constassert=require('node:assert');
4+
5+
const{ expectedPort, expectedInitialPort, expectedHost }=process.env;
6+
constdebugOptions=
7+
require('internal/options').getOptionValue('--inspect-port');
8+
9+
if('expectedPort'inprocess.env){
10+
assert.strictEqual(process.debugPort,+expectedPort);
11+
}
12+
13+
if('expectedInitialPort'inprocess.env){
14+
assert.strictEqual(debugOptions.port,+expectedInitialPort);
15+
}
16+
17+
if('expectedHost'inprocess.env){
18+
assert.strictEqual(debugOptions.host,expectedHost);
19+
}

0 commit comments

Comments
(0)