104 lines
2.7 KiB
TypeScript
104 lines
2.7 KiB
TypeScript
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
|
|
}
|