Add memoization to match and fix zero-timeout

This commit is contained in:
Chris Wanstrath 2026-03-10 15:06:23 -07:00
parent 6df26b90ac
commit 6b23930908
3 changed files with 57 additions and 14 deletions

View File

@ -20,10 +20,27 @@ func matchLine(pattern, actual string) bool {
} }
func matchOutput(expected, actual []string) bool { func matchOutput(expected, actual []string) bool {
return doMatch(expected, 0, actual, 0) memo := make(map[[2]int]int8) // 0=unknown, 1=true, -1=false
return doMatch(expected, 0, actual, 0, memo)
} }
func doMatch(expected []string, ei int, actual []string, ai int) bool { func doMatch(expected []string, ei int, actual []string, ai int, memo map[[2]int]int8) bool {
key := [2]int{ei, ai}
if v := memo[key]; v != 0 {
return v == 1
}
result := doMatchInner(expected, ei, actual, ai, memo)
if result {
memo[key] = 1
} else {
memo[key] = -1
}
return result
}
func doMatchInner(expected []string, ei int, actual []string, ai int, memo map[[2]int]int8) bool {
if ei == len(expected) && ai == len(actual) { if ei == len(expected) && ai == len(actual) {
return true return true
} }
@ -36,7 +53,7 @@ func doMatch(expected []string, ei int, actual []string, ai int) bool {
// Multi-line wildcard // Multi-line wildcard
if exp == "..." { if exp == "..." {
for skip := ai; skip <= len(actual); skip++ { for skip := ai; skip <= len(actual); skip++ {
if doMatch(expected, ei+1, actual, skip) { if doMatch(expected, ei+1, actual, skip, memo) {
return true return true
} }
} }
@ -48,7 +65,7 @@ func doMatch(expected []string, ei int, actual []string, ai int) bool {
} }
if matchLine(exp, actual[ai]) { if matchLine(exp, actual[ai]) {
return doMatch(expected, ei+1, actual, ai+1) return doMatch(expected, ei+1, actual, ai+1, memo)
} }
return false return false

View File

@ -99,6 +99,19 @@ func TestMatchOutputInlineAndMultiline(t *testing.T) {
} }
} }
func TestMatchOutputConsecutiveWildcards(t *testing.T) {
// This would hang before memoization (exponential backtracking)
expected := []string{"...", "...", "...", "...", "end"}
actual := make([]string, 200)
for i := range actual {
actual[i] = "line"
}
// "end" is not in actual, so this should return false — quickly.
if matchOutput(expected, actual) {
t.Error("should not match when final line is missing")
}
}
func TestDiffBasic(t *testing.T) { func TestDiffBasic(t *testing.T) {
d := diff([]string{"hello"}, []string{"world"}) d := diff([]string{"hello"}, []string{"world"})
if len(d) != 2 { if len(d) != 2 {

13
run.go
View File

@ -186,6 +186,7 @@ func runFile(file ShoutFile, opts RunOptions) FileResult {
stdin.Close() stdin.Close()
var output []byte var output []byte
if totalTimeout > 0 {
select { select {
case r := <-ch: case r := <-ch:
if r.err != nil { if r.err != nil {
@ -206,6 +207,18 @@ func runFile(file ShoutFile, opts RunOptions) FileResult {
waited = true waited = true
return FileResult{File: file, TmpDir: tmpDir, Error: "Timeout reading output"} return FileResult{File: file, TmpDir: tmpDir, Error: "Timeout reading output"}
} }
} else {
r := <-ch
if r.err != nil {
if cmd.Process != nil {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
}
_ = cmd.Wait()
waited = true
return FileResult{File: file, TmpDir: tmpDir, Error: r.err.Error()}
}
output = r.data
}
_ = cmd.Wait() _ = cmd.Wait()
waited = true waited = true