import { escapeRegex } from "./utils.ts" export function matchLine(pattern: string, actual: string): boolean { if (!pattern.includes("...")) return pattern === actual // Convert inline ... to regex const parts = pattern.split("...") const escaped = parts.map(p => escapeRegex(p)) const regex = new RegExp("^" + escaped.join(".*") + "$") return regex.test(actual) } export function matchOutput( expected: string[], actual: string[], ): boolean { return doMatch(expected, 0, actual, 0) } function doMatch( expected: string[], ei: number, actual: string[], ai: number, ): boolean { // Both exhausted — match if (ei === expected.length && ai === actual.length) return true // Expected exhausted but actual remains — no match if (ei === expected.length) return false const exp = expected[ei]! // Multi-line wildcard if (exp === "...") { // Try matching zero or more actual lines for (let skip = ai; skip <= actual.length; skip++) { if (doMatch(expected, ei + 1, actual, skip)) return true } return false } // Actual exhausted but expected remains — no match if (ai === actual.length) return false // Line-level match (with possible inline wildcards) if (matchLine(exp, actual[ai]!)) { return doMatch(expected, ei + 1, actual, ai + 1) } return false } export type DiffLine = { kind: "equal" | "expected" | "actual" | "context" text: string } export function diff(expected: string[], actual: string[]): DiffLine[] { const result: DiffLine[] = [] let ei = 0 let ai = 0 while (ei < expected.length || ai < actual.length) { if (ei < expected.length && expected[ei] === "...") { // Find where the wildcard ends by looking at next expected line const nextExp = ei + 1 < expected.length ? expected[ei + 1] : null if (nextExp === null) { // ... at end matches everything remaining result.push({ kind: "context", text: "..." }) break } // Skip actual lines until we find the next expected match result.push({ kind: "context", text: "..." }) ei++ while (ai < actual.length && !matchLine(nextExp!, actual[ai]!)) { ai++ } continue } if (ei < expected.length && ai < actual.length) { if (matchLine(expected[ei]!, actual[ai]!)) { result.push({ kind: "equal", text: actual[ai]! }) ei++ ai++ } else { result.push({ kind: "expected", text: expected[ei]! }) result.push({ kind: "actual", text: actual[ai]! }) ei++ ai++ } } else if (ei < expected.length) { result.push({ kind: "expected", text: expected[ei]! }) ei++ } else { result.push({ kind: "actual", text: actual[ai]! }) ai++ } } return result }