import ansis from "ansis" import type { CommandResult } from "./run.ts" import type { DiffLine } from "./match.ts" import { diff, matchOutput } from "./match.ts" export type TestResult = { path: string passed: boolean commandCount: number failures: FailedCommand[] error?: string } type FailedCommand = { result: CommandResult diffLines: DiffLine[] exitCodeMismatch: boolean } export function evaluateFile( path: string, results: CommandResult[], error?: string, ): TestResult { if (error) { return { path, passed: false, commandCount: results.length, failures: [], error } } const failures: FailedCommand[] = [] for (const result of results) { const { command, actual, exitCode } = result const outputMatches = matchOutput(command.expected, actual) let exitCodeMismatch = false if (command.exitCode === null) { // Expect exit code 0 exitCodeMismatch = exitCode !== 0 } else if (command.exitCode === "*") { // Expect any non-zero exitCodeMismatch = exitCode === 0 } else { // Expect specific code exitCodeMismatch = exitCode !== command.exitCode } if (!outputMatches || exitCodeMismatch) { failures.push({ result, diffLines: outputMatches ? [] : diff(command.expected, actual), exitCodeMismatch, }) } } return { path, passed: failures.length === 0, commandCount: results.length, failures } } export function formatFailure(test: TestResult): string { const lines: string[] = [] lines.push(ansis.red(`FAIL ${test.path}`)) if (test.error) { lines.push(` ${ansis.red(test.error)}`) return lines.join("\n") } for (const failure of test.failures) { lines.push("") lines.push(` ${ansis.dim("$")} ${failure.result.command.command}`) if (failure.diffLines.length > 0) { const expectedLines: string[] = [] const actualLines: string[] = [] for (const dl of failure.diffLines) { const text = dl.kind === "context" ? ansis.dim(dl.text) : dl.text if (dl.kind === "expected" || dl.kind === "equal" || dl.kind === "context") { const prefix = dl.kind === "expected" ? ansis.green(" > ") : " " expectedLines.push(`${prefix}${text}`) } if (dl.kind === "actual" || dl.kind === "equal" || dl.kind === "context") { const prefix = dl.kind === "actual" ? ansis.red(" > ") : " " actualLines.push(`${prefix}${text}`) } } lines.push(ansis.green(" expected:"), ...expectedLines) lines.push(ansis.red(" actual:"), ...actualLines) } if (failure.exitCodeMismatch) { const expected = failure.result.command.exitCode ?? 0 const actual = failure.result.exitCode lines.push( ansis.green(` expected exit code: ${expected === "*" ? "non-zero" : expected}`), ) lines.push(ansis.red(` actual exit code: ${actual}`)) } } return lines.join("\n") } export function formatSummary( results: TestResult[], elapsed: number, singleFile?: string, ): string { const totalCommands = results.reduce((n, r) => n + r.commandCount, 0) const failedCommands = results.reduce((n, r) => n + r.failures.length, 0) const passedCommands = totalCommands - failedCommands const parts: string[] = [] if (passedCommands > 0) parts.push(ansis.green(`${passedCommands} passed`)) if (failedCommands > 0) parts.push(ansis.red(`${failedCommands} failed`)) const time = elapsed < 1000 ? `${Math.round(elapsed)}ms` : `${(elapsed / 1000).toFixed(1)}s` const label = singleFile ? ` in ${singleFile}` : "" return `${parts.join(", ")}${label} ${ansis.dim(`[${time}]`)}` }