Ctrl K
ComunidadGuías…

lib/failure-excerpt.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// lib/failure-excerpt.mjs — choose which lines of a failed child suite's output
// are worth printing (#4017).
//
// test-all.mjs runs every node:test suite in a child process and, on failure,
// printed the last twelve non-empty lines of its output. node's runner prints
// the error message ABOVE the stack frames, so that window reliably kept the
// frames and dropped the message — and the message is the only place an
// assertion's interpolated value appears. A failed `assert.ok(elapsed >= 400,
// \`gave up after \${elapsed}ms\`)` showed `actual: false, expected: true`,
// which is true of every failed assert.ok in the repository.
//
// Keep the headers the window cuts off, and keep the window. The frames locate
// the assertion; the message explains it.

/**
 * Lines that open an error block: `AssertionError [ERR_ASSERTION]: ...`,
 * `TypeError: ...`, `Error: ...`. Anchored so an `at ...` frame mentioning a
 * path with "Error" in it is not mistaken for a header.
 */
const ERROR_HEADER = /^\s*(?:[A-Za-z_$][\w$]*)?(?:Error|Exception)\b[^\n]*:/;

/**
 * The same information in node's TAP reporter, which is the default on node 20
 * and 22 whenever stdout is not a terminal (node 23 made spec the default). TAP
 * prints no `AssertionError [ERR_ASSERTION]: ...` line at all: the message sits
 * under `error:` in a YAML block, either on that line or indented below a `|-`,
 * and the error code has a line of its own. Those lines sit about twenty lines
 * from the end, above the frames and the summary, so the window missed them and
 * the excerpt on node 22 was frames only.
 */
const TAP_ERROR = /^(\s*)error: /;
const TAP_CODE = /^\s*code: '[A-Z][A-Z0-9_]*'$/;
const TAP_STACK = /^(\s*)stack: \|-$/;

const indentOf = (line) => line.length - line.trimStart().length;

/**
 * Indexes of TAP error lines: each `error:` line, the indented lines of its
 * block message, each `code: '...'` line, and the first frame under `stack: |-`
 * (the one that locates the assertion; the window keeps the frames below it
 * on the spec reporter, but on TAP the summary pushes all of them out).
 *
 * @param {string[]} lines Non-empty output lines.
 * @returns {Set<number>}
 */
function tapErrorLines(lines) {
  const found = new Set();
  for (let i = 0; i < lines.length; i++) {
    if (TAP_CODE.test(lines[i])) { found.add(i); continue; }
    const stack = TAP_STACK.exec(lines[i]);
    if (stack) {
      if (i + 1 < lines.length && indentOf(lines[i + 1]) > stack[1].length) found.add(i + 1);
      continue;
    }
    const m = TAP_ERROR.exec(lines[i]);
    if (!m) continue;
    found.add(i);
    // A block message continues while the lines are indented deeper than the
    // `error:` key; the next key at the same depth ends it.
    const depth = m[1].length;
    while (i + 1 < lines.length && indentOf(lines[i + 1]) > depth) {
      found.add(++i);
    }
  }
  return found;
}

/**
 * Pick the lines worth printing from a failed suite's stdout/stderr.
 *
 * @param {string|null|undefined} text Raw child output.
 * @param {object} [opts]
 * @param {number} [opts.tailLines=12] Trailing lines to keep, the previous behaviour.
 * @param {number} [opts.maxHeaders=5] Ceiling on recovered headers, so a suite
 *   failing in bulk cannot flood a CI log. Truncation is reported, never silent.
 * @returns {string[]} Lines in source order, each already trimmed of nothing —
 *   callers add their own indentation.
 */
export function failureExcerpt(text, { tailLines = 12, maxHeaders = 5 } = {}) {
  const lines = String(text ?? '').split('\n').filter((l) => l.trim());
  if (lines.length === 0) return [];

  const firstTail = Math.max(0, lines.length - tailLines);
  // Index-based, not text-based: two identical frames are two real frames, and
  // selecting by index keeps each line once without collapsing genuine repeats.
  const keep = new Set();
  for (let i = firstTail; i < lines.length; i++) keep.add(i);

  const tap = tapErrorLines(lines);
  let dropped = 0;
  for (let i = 0; i < firstTail; i++) {
    if (!ERROR_HEADER.test(lines[i]) && !tap.has(i)) continue;
    if (keep.size - tailLines >= maxHeaders) { dropped++; continue; }
    keep.add(i);
  }

  const out = [...keep].sort((a, b) => a - b).map((i) => lines[i]);
  if (dropped > 0) out.push(`... ${dropped} more error line(s) not shown`);
  return out;
}

/**
 * The excerpt for a failed child run, read from both of its streams.
 *
 * node's runner writes its failure report to stdout, and warnings or a crash
 * land on stderr. Taking whichever stream is non-empty printed a warning and
 * dropped the assertion. stderr goes first, so the tail window stays on
 * stdout's frames, and an error header on stderr is still recovered. One pass
 * over the joined text keeps the same bound as a single stream.
 *
 * @param {{ stdout?: string, stderr?: string }|null|undefined} detail
 * @param {object} [opts] Passed through to failureExcerpt.
 * @returns {string[]}
 */
export function childFailureExcerpt(detail, opts) {
  const streams = [detail?.stderr, detail?.stdout].filter((s) => s && String(s).trim());
  return failureExcerpt(streams.join('\n'), opts);
}