GeezerVison is showing github.com — test_runner: support using `--inspect` with `--test` · nodejs/node@64d343a
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +5Lines changed: 5 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -338,6 +338,11 @@ added: REPLACEME | 338 | 338 | fail after. | 339 | 339 | If unspecified, subtests inherit this value from their parent. | 340 | 340 | **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`. | 341 | 346 | * Returns: {TapStream} | 342 | 347 |
| 343 | 348 | ```js | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/cluster/primary.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -120,6 +120,7 @@ function createWorkerProcess(id, env) { | 120 | 120 | const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/; | 121 | 121 | const nodeOptions = process.env.NODE_OPTIONS || ''; | 122 | 122 |
| | 123 | + // TODO(MoLow): Use getInspectPort from internal/util/inspector | 123 | 124 | if (ArrayPrototypeSome(execArgv, | 124 | 125 | (arg) => RegExpPrototypeExec(debugArgRegex, arg) !== null) || | 125 | 126 | RegExpPrototypeExec(debugArgRegex, nodeOptions) !== null) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/test_runner.js +12-1Lines changed: 12 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 | const { | 3 | 3 | prepareMainThreadExecution, | 4 | 4 | } = require('internal/bootstrap/pre_execution'); | | 5 | +const { isUsingInspector } = require('internal/util/inspector'); | 5 | 6 | const { run } = require('internal/test_runner/runner'); | 6 | 7 |
| 7 | 8 | prepareMainThreadExecution(false); | 8 | 9 | markBootstrapComplete(); | 9 | 10 |
| 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 }); | 11 | 22 | tapStream.pipe(process.stdout); | 12 | 23 | tapStream.once('test:fail', () => { | 13 | 24 | process.exitCode = 1; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/runner.js +49-10Lines changed: 49 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 | const { | 3 | 3 | ArrayFrom, | 4 | | - ArrayPrototypeConcat, | 5 | 4 | ArrayPrototypeFilter, | 6 | 5 | ArrayPrototypeIncludes, | 7 | 6 | ArrayPrototypeJoin, | | 7 | + ArrayPrototypePop, | | 8 | + ArrayPrototypePush, | 8 | 9 | ArrayPrototypeSlice, | 9 | 10 | ArrayPrototypeSort, | 10 | 11 | ObjectAssign, | 11 | 12 | PromisePrototypeThen, | | 13 | + RegExpPrototypeSymbolSplit, | 12 | 14 | SafePromiseAll, | 13 | 15 | SafeSet, | | 16 | + StringPrototypeEndsWith, | 14 | 17 | } = primordials; | 15 | 18 |
| | 19 | +const { Buffer } = require('buffer'); | 16 | 20 | const { spawn } = require('child_process'); | 17 | 21 | const { readdirSync, statSync } = require('fs'); | 18 | 22 | const console = require('internal/console/global'); | @@ -22,6 +26,7 @@ const { | 22 | 26 | }, | 23 | 27 | } = require('internal/errors'); | 24 | 28 | const { validateArray } = require('internal/validators'); | | 29 | +const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); | 25 | 30 | const { kEmptyObject } = require('internal/util'); | 26 | 31 | const { createTestTree } = require('internal/test_runner/harness'); | 27 | 32 | const { kSubtestsFailed, Test } = require('internal/test_runner/test'); | @@ -100,25 +105,59 @@ function filterExecArgv(arg) { | 100 | 105 | return !ArrayPrototypeIncludes(kFilterArgs, arg); | 101 | 106 | } | 102 | 107 |
| 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) { | 104 | 141 | 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 }); | 108 | 143 |
| 109 | 144 | const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' }); | 110 | 145 | // TODO(cjihrig): Implement a TAP parser to read the child's stdout | 111 | 146 | // instead of just displaying it all if the child fails. | 112 | 147 | let err; | | 148 | + let stderr = ''; | 113 | 149 |
| 114 | 150 | child.on('error', (error) => { | 115 | 151 | err = error; | 116 | 152 | }); | 117 | 153 |
| 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([ | 119 | 159 | once(child, 'exit', { signal: t.signal }), | 120 | 160 | child.stdout.toArray({ signal: t.signal }), | 121 | | - child.stderr.toArray({ signal: t.signal }), | 122 | 161 | ]); | 123 | 162 |
| 124 | 163 | if (code !== 0 || signal !== null) { | @@ -128,7 +167,7 @@ function runTestFile(path, root) { | 128 | 167 | exitCode: code, | 129 | 168 | signal: signal, | 130 | 169 | stdout: ArrayPrototypeJoin(stdout, ''), | 131 | | - stderr: ArrayPrototypeJoin(stderr, ''), | | 170 | + stderr, | 132 | 171 | // The stack will not be useful since the failures came from tests | 133 | 172 | // in a child process. | 134 | 173 | stack: undefined, | @@ -145,7 +184,7 @@ function run(options) { | 145 | 184 | if (options === null || typeof options !== 'object') { | 146 | 185 | options = kEmptyObject; | 147 | 186 | } | 148 | | - const { concurrency, timeout, signal, files } = options; | | 187 | + const { concurrency, timeout, signal, files, inspectPort } = options; | 149 | 188 |
| 150 | 189 | if (files != null) { | 151 | 190 | validateArray(files, 'options.files'); | @@ -154,7 +193,7 @@ function run(options) { | 154 | 193 | const root = createTestTree({ concurrency, timeout, signal }); | 155 | 194 | const testFiles = files ?? createTestFileList(); | 156 | 195 |
| 157 | | - PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root)), | | 196 | + PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root, inspectPort)), | 158 | 197 | () => root.postRun()); | 159 | 198 |
| 160 | 199 | return root.reporter; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js +2-3Lines changed: 2 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -58,8 +58,6 @@ const kDefaultTimeout = null; | 58 | 58 | const noop = FunctionPrototype; | 59 | 59 | const isTestRunner = getOptionValue('--test'); | 60 | 60 | 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; | 63 | 61 | const kShouldAbort = Symbol('kShouldAbort'); | 64 | 62 | const kRunHook = Symbol('kRunHook'); | 65 | 63 | const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); | @@ -150,7 +148,7 @@ class Test extends AsyncResource { | 150 | 148 | } | 151 | 149 |
| 152 | 150 | if (parent === null) { | 153 | | - this.concurrency = rootConcurrency; | | 151 | + this.concurrency = 1; | 154 | 152 | this.indent = ''; | 155 | 153 | this.indentString = kDefaultIndent; | 156 | 154 | this.only = testOnlyFlag; | @@ -180,6 +178,7 @@ class Test extends AsyncResource { | 180 | 178 |
| 181 | 179 | case 'boolean': | 182 | 180 | if (concurrency) { | | 181 | + // TODO(cjihrig): Use uv_available_parallelism() once it lands. | 183 | 182 | this.concurrency = parent === null ? MathMax(cpus().length - 1, 1) : Infinity; | 184 | 183 | } else { | 185 | 184 | this.concurrency = 1; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/util/inspector.js +42Lines changed: 42 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | const { | 4 | 4 | ArrayPrototypeConcat, | | 5 | + ArrayPrototypeSome, | 5 | 6 | FunctionPrototypeBind, | 6 | 7 | ObjectDefineProperty, | 7 | 8 | ObjectKeys, | 8 | 9 | ObjectPrototypeHasOwnProperty, | | 10 | + RegExpPrototypeExec, | 9 | 11 | } = primordials; | 10 | 12 |
| | 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 | + | 11 | 46 | let session; | 12 | 47 | function sendInspectorCommand(cb, onError) { | 13 | 48 | const { hasInspector } = internalBinding('config'); | @@ -22,6 +57,10 @@ function sendInspectorCommand(cb, onError) { | 22 | 57 | } | 23 | 58 | } | 24 | 59 |
| | 60 | +function isInspectorMessage(string) { | | 61 | + return isUsingInspector() && RegExpPrototypeExec(kInspectMsgRegex, string) !== null; | | 62 | +} | | 63 | + | 25 | 64 | // Create a special require function for the inspector command line API | 26 | 65 | function installConsoleExtensions(commandLineApi) { | 27 | 66 | if (commandLineApi.require) { return; } | @@ -65,7 +104,10 @@ function wrapConsole(consoleFromNode, consoleFromVM) { | 65 | 104 | // Stores the console from VM, should be set during bootstrap. | 66 | 105 | let consoleFromVM; | 67 | 106 | module.exports = { | | 107 | + getInspectPort, | 68 | 108 | installConsoleExtensions, | | 109 | + isInspectorMessage, | | 110 | + isUsingInspector, | 69 | 111 | sendInspectorCommand, | 70 | 112 | wrapConsole, | 71 | 113 | get consoleFromVM() { | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc -3Lines changed: 0 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -161,9 +161,6 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) { | 161 | 161 | errors->push_back("either --test or --watch can be used, not both"); | 162 | 162 | } | 163 | 163 |
| 164 | | - if (debug_options_.inspector_enabled) { | 165 | | - errors->push_back("the inspector cannot be used with --test"); | 166 | | - } | 167 | 164 | #ifndef ALLOW_ATTACHING_DEBUGGER_IN_TEST_RUNNER | 168 | 165 | debug_options_.allow_attaching_debugger = false; | 169 | 166 | #endif | |
Collapse file Copy file name to clipboardExpand all lines: test/common/index.mjs +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -52,6 +52,8 @@ const { | 52 | 52 | spawnPromisified, | 53 | 53 | } = common; | 54 | 54 |
| | 55 | +const getPort = () => common.PORT; | | 56 | + | 55 | 57 | export { | 56 | 58 | isMainThread, | 57 | 59 | isWindows, | @@ -100,4 +102,5 @@ export { | 100 | 102 | runWithInvalidFD, | 101 | 103 | createRequire, | 102 | 104 | spawnPromisified, | | 105 | + getPort, | 103 | 106 | }; |
Collapse file Copy file name to clipboard +40Lines changed: 40 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
Collapse file Copy file name to clipboard +19Lines changed: 19 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|