current commit
260709.4 · test: normalize Windows crash in debugger test normalization
'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 crash while tearing down. For now normalize the crash
// 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
// The crash shows up as with exit code 3221225477 on Windows, or signal
// SIGSEGV on other platforms.
const probeTargetExitSignal = 'SIGSEGV';
// 0xC0000005 STATUS_ACCESS_VIOLATION on Windows
const probeTargetExitCode = 3221225477;

function isProbeSegvTeardown(result) {
  if (result?.event !== 'error') { return false; }
  const error = result.error;
  if (error?.signal !== probeTargetExitSignal && error?.exitCode !== probeTargetExitCode) { return false; }
  return error.code === 'probe_target_exit' || error.code === 'probe_failure';
}

function findProbeSegvTeardownLine(output) {
  const signalPrefix = `Target exited with signal ${probeTargetExitSignal}`;
  const codePrefix = `Target exited with code ${probeTargetExitCode}`;
  for (const prefix of [signalPrefix, codePrefix]) {
    if (output.startsWith(prefix)) { return 0; }
    const idx = output.indexOf(`\n${prefix}`);
    if (idx !== -1) { return idx + 1; }
  }
  return -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,
};