shout/src/format.ts

125 lines
3.6 KiB
TypeScript

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}]`)}`
}