export function parseTest()

in packages/bun-internal-test/runners/bun/runner.ts [275:552]


export function parseTest(stderr: string, options: ParseTestOptions = {}): ParseTestResult {
  const { cwd, knownPaths } = options;
  const linesAnsi = stderr.split("\n");
  const lines = linesAnsi.map(stripAnsi);
  let info: TestInfo | undefined;
  const parseInfo = (line: string): TestInfo | undefined => {
    const match = /^(bun (?:wip)?test) v([0-9\.]+) \(([0-9a-z]+)\)$/.exec(line);
    if (!match) {
      return undefined;
    }
    const [, name, version, sha] = match;
    return {
      name,
      version,
      revision: sha,
    };
  };
  let files: TestFile[] = [];
  let file: TestFile | undefined;
  const parseFile = (line: string): TestFile | undefined => {
    let file = line.slice(0, -1);
    if (!isJavaScript(file) || !line.endsWith(":")) {
      return undefined;
    }
    for (const path of knownPaths ?? []) {
      if (path.endsWith(file)) {
        file = path;
        break;
      }
    }
    return {
      file,
      tests: [],
      status: "pass",
      summary: {
        files: 1,
        tests: 0,
        pass: 0,
        fail: 0,
        skip: 0,
        todo: 0,
        duration: 0,
      },
    };
  };
  const parseTestLine = (line: string): Test | undefined => {
    const match = /^(✓|‚úì|✗|‚úó|»|-|✎) (.*)$/.exec(line);
    if (!match) {
      return undefined;
    }
    const [, icon, name] = match;
    let status: TestStatus = "fail";
    switch (icon) {
      case "✓":
      case "‚úì":
        status = "pass";
        break;
      case "✗":
      case "‚úó":
        status = "fail";
        break;
      case "»":
      case "-":
        status = "skip";
        break;
      case "✎":
        status = "todo";
        break;
    }
    const match2 = /^(.*) \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(name);
    if (!match2) {
      return {
        name,
        status,
        duration: 0,
      };
    }
    const [, title, duration, unit] = match2;
    return {
      name: title,
      status,
      duration: parseFloat(duration ?? "0") * (unit === "ms" ? 1000 : 1) || 0,
    };
  };
  let errors: TestError[] = [];
  let error: TestError | undefined;
  const parseError = (line: string): TestError | undefined => {
    const match = /^(.*error|timeout)\: (.*)$/i.exec(line);
    if (!match) {
      return undefined;
    }
    const [, name, message] = match;
    return {
      name: name === "error" ? "Error" : name,
      message,
    };
  };
  const parseErrorStack = (line: string): TestErrorStack | undefined => {
    let match = /^\s*at (.*) \((.*)\:([0-9]+)\:([0-9]+)\)$/.exec(line);
    if (!match) {
      match = /^\s*at (.*)\:([0-9]+)\:([0-9]+)$/.exec(line);
      if (!match) {
        return undefined;
      }
    }
    const [columnNo, lineNo, path, func] = match.reverse();
    let file = path;
    if (cwd && path.startsWith(cwd)) {
      file = path.slice(cwd.length);
      if (file.startsWith("/")) {
        file = file.slice(1);
      }
    }
    return {
      file,
      function: func !== line ? func : undefined,
      line: parseInt(lineNo),
      column: parseInt(columnNo),
    };
  };
  const parseErrorPreview = (line: string): string | undefined => {
    if (line.endsWith("^") || /^[0-9]+ \| /.test(line)) {
      return line;
    }
    return undefined;
  };
  let summary: TestSummary | undefined;
  const parseSummary = (line: string): TestSummary | undefined => {
    const match = /^Ran ([0-9]+) tests across ([0-9]+) files\. .* \[([0-9]+\.[0-9]+)(m?s)\]$/.exec(line);
    if (!match) {
      return undefined;
    }
    const [, tests, files, duration, unit] = match;
    return {
      pass: 0,
      fail: 0,
      skip: 0,
      todo: 0,
      tests: parseInt(tests),
      files: parseInt(files),
      duration: parseFloat(duration) * (unit === "s" ? 1000 : 1),
    };
  };
  const createSummary = (files: TestFile[]): TestSummary => {
    const summary = {
      pass: 0,
      fail: 0,
      skip: 0,
      todo: 0,
      tests: 0,
      files: 0,
      duration: 0,
    };
    for (const file of files) {
      summary.files++;
      summary.duration += file.summary.duration;
      for (const test of file.tests) {
        summary.tests++;
        summary[test.status]++;
      }
      if (file.errors?.length) {
        summary.fail++;
      }
    }
    return summary;
  };
  const parseSkip = (line: string): number => {
    const match = /^([0-9]+) tests (?:skipped|failed|todo)\:$/.exec(line);
    if (match) {
      return parseInt(match[1]);
    }
    return 0;
  };
  const endOfFile = (file?: TestFile): void => {
    if (file && !file.tests.length && errors.length) {
      file.errors = errors;
      errors = [];
    }
  };
  let errorStart = 0;
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (!info && !(info = parseInfo(line))) {
      continue;
    }
    const newFile = parseFile(line);
    if (newFile) {
      endOfFile(file);
      files.push((file = newFile));
      continue;
    }
    const newError = parseError(line);
    if (newError) {
      errorStart = i;
      errors.push((error = newError));
      for (let j = 1; j < 8 && i - j >= 0; j++) {
        const line = lines[i - j];
        const preview = parseErrorPreview(line);
        if (!preview) {
          break;
        }
        if (error.preview) {
          error.preview = preview + "\n" + error.preview;
        } else {
          error.preview = preview;
        }
      }
      continue;
    }
    const newStack = parseErrorStack(line);
    if (newStack) {
      if (error) {
        error.stack ||= [];
        error.stack.push(newStack);
        for (let j = errorStart + 1; j < i && error.stack.length === 1; j++) {
          error.message += "\n" + lines[j];
        }
      } else {
        // TODO: newStack and !error
      }
      continue;
    }
    const newTest = parseTestLine(line);
    if (newTest) {
      if (error && newTest.status === "skip") {
        continue; // Likely a false positive from error message
      }
      if (error) {
        for (let j = errorStart + 1; j < i - 1 && !error.stack?.length; j++) {
          error.message += "\n" + lines[j];
        }
        error = undefined;
      }
      if (errors.length) {
        newTest.errors = errors;
        errors = [];
      }
      file!.tests.push(newTest);
      continue;
    }
    const newSummary = parseSummary(line);
    if (newSummary) {
      summary = newSummary;
      break;
    }
    i += parseSkip(line);
  }
  endOfFile(file);
  if (!info) {
    throw new Error("No tests found; did the test runner crash?");
  }
  summary ||= createSummary(files);
  const count = (status: TestStatus): number => {
    return files.reduce((n, file) => n + file.tests.filter(test => test.status === status).length, 0);
  };
  summary.pass ||= count("pass");
  summary.fail ||= count("fail");
  summary.skip ||= count("skip");
  summary.todo ||= count("todo");
  const getStatus = (summary: TestSummary) => {
    return summary.fail ? "fail" : !summary.pass && summary.skip ? "skip" : "pass";
  };
  if (files.length === 1) {
    files[0].summary = { ...summary };
    files[0].status = getStatus(summary);
  } else {
    for (const file of files) {
      const summary = createSummary([file]);
      file.summary = summary;
      file.status = getStatus(summary);
    }
  }
  return {
    info,
    files,
    summary,
  };
}