diff --git a/match.go b/match.go index 6f61fd0..0f33d07 100644 --- a/match.go +++ b/match.go @@ -20,10 +20,27 @@ func matchLine(pattern, 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) { return true } @@ -36,7 +53,7 @@ func doMatch(expected []string, ei int, actual []string, ai int) bool { // Multi-line wildcard if exp == "..." { for skip := ai; skip <= len(actual); skip++ { - if doMatch(expected, ei+1, actual, skip) { + if doMatch(expected, ei+1, actual, skip, memo) { return true } } @@ -48,7 +65,7 @@ func doMatch(expected []string, ei int, actual []string, ai int) bool { } if matchLine(exp, actual[ai]) { - return doMatch(expected, ei+1, actual, ai+1) + return doMatch(expected, ei+1, actual, ai+1, memo) } return false diff --git a/match_test.go b/match_test.go index ef4a4ce..d79ee7e 100644 --- a/match_test.go +++ b/match_test.go @@ -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) { d := diff([]string{"hello"}, []string{"world"}) if len(d) != 2 { diff --git a/run.go b/run.go index bf01c17..540b671 100644 --- a/run.go +++ b/run.go @@ -186,8 +186,29 @@ func runFile(file ShoutFile, opts RunOptions) FileResult { stdin.Close() var output []byte - select { - case r := <-ch: + if totalTimeout > 0 { + select { + case 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 + case <-time.After(totalTimeout): + if cmd.Process != nil { + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + <-ch // drain the reader goroutine so pipe FDs are released + _ = cmd.Wait() + waited = true + 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) @@ -197,14 +218,6 @@ func runFile(file ShoutFile, opts RunOptions) FileResult { return FileResult{File: file, TmpDir: tmpDir, Error: r.err.Error()} } output = r.data - case <-time.After(totalTimeout): - if cmd.Process != nil { - _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - } - <-ch // drain the reader goroutine so pipe FDs are released - _ = cmd.Wait() - waited = true - return FileResult{File: file, TmpDir: tmpDir, Error: "Timeout reading output"} } _ = cmd.Wait()