node/test/common/debugger-probe.js
Joyee Cheung 8c4ec8a674
debugger: add --max-hit option to probe mode
This option provides a new way of terminating the probing by
specifying a limit for the per-probe hit evaluation. When any
probe reaches the limit, the session removes the V8 breakpoints
and finishes with a `completed` terminal event rather than waiting
for the target to exit or timeout. This would be useful when we
provide a way to attach to and probe a running process in the future.

Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63704
Refs: https://github.com/nodejs/node/issues/63646
Reviewed-By: Jan Martin <jan.krems@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2026-06-13 20:50:15 +00:00

96 lines
3.3 KiB
JavaScript

'use strict';
const assert = require('assert');
const { spawnSyncAndExit } = require('./child_process');
// Work around a pre-existing inspector issue: if the debuggee exits too quickly
// the inspector can segfault while tearing down. For now normalize the segfault
// back to the expected terminal event (e.g. "completed" or "miss")
// until the upstream bug is fixed.
// See https://github.com/nodejs/node/issues/62765
// https://github.com/nodejs/node/issues/58245
const probeTargetExitSignal = 'SIGSEGV';
function isProbeSegvTeardown(result) {
if (result?.event !== 'error') { return false; }
const error = result.error;
if (error?.signal !== probeTargetExitSignal) { return false; }
return error.code === 'probe_target_exit' || error.code === 'probe_failure';
}
function findProbeSegvTeardownLine(output) {
const signalPrefix = `Target exited with signal ${probeTargetExitSignal}`;
if (output.startsWith(signalPrefix)) { return 0; }
const idx = output.indexOf(`\n${signalPrefix}`);
return idx === -1 ? -1 : idx + 1;
}
// Replace volatile fields in a probe report (stack frames, Node.js version,
// scriptId, callFrames) with stable placeholders for deepStrictEqual.
function normalizeProbeReport(value) {
if (typeof value === 'string') {
return value
.replace(/(?:\n[ \t]+at\s[^\n]*)+/g, '\n<stack>')
.replace(/\nNode\.js v[^\n]+/g, '\nNode.js <version>');
}
if (Array.isArray(value)) {
return value.map(normalizeProbeReport);
}
if (value !== null && typeof value === 'object') {
const result = {};
for (const key of Object.keys(value)) {
if (key === 'scriptId') {
result[key] = '<scriptId>';
} else if (key === 'callFrames') {
result[key] = '<callFrames>';
} else {
result[key] = normalizeProbeReport(value[key]);
}
}
return result;
}
return value;
}
function assertProbeJson(output, expected) {
const normalized = typeof output === 'string' ? JSON.parse(output) : output;
const lastResult = normalized.results?.[normalized.results.length - 1];
if (isProbeSegvTeardown(lastResult)) {
// Log to facilitate debugging if this normalization is occurring.
console.log('Normalizing trailing SIGSEGV in JSON probe output');
normalized.results[normalized.results.length - 1] = expected.results.at(-1);
}
assert.deepStrictEqual(normalizeProbeReport(normalized), normalizeProbeReport(expected));
}
function assertProbeText(output, expected) {
const lineStart = findProbeSegvTeardownLine(output);
let normalized;
if (lineStart !== -1) {
// Log to facilitate debugging if this normalization is occurring.
console.log('Normalizing trailing SIGSEGV in text probe output');
const expectedLineStart = expected.lastIndexOf('\n');
const terminalLine = expectedLineStart === -1 ? expected : expected.slice(expectedLineStart + 1);
normalized = lineStart === 0 ? terminalLine : `${output.slice(0, lineStart - 1)}\n${terminalLine}`;
} else {
normalized = output;
}
assert.strictEqual(normalized, expected);
}
function assertProbeCliError(inspectArgs, expectedStderr, { cwd } = {}) {
spawnSyncAndExit(process.execPath, ['inspect', ...inspectArgs], { cwd }, {
signal: null,
status: 9,
stderr: expectedStderr,
trim: true,
});
}
module.exports = {
assertProbeJson,
assertProbeCliError,
assertProbeText,
};