test(crash): fail when the child exits before the kill and show its output
- commit
- 5e09b48f81c3654292a06712e93998865e5b7fbc
- author
- brice · · 2026-10-10 03:15 UTC
- parent
- 3755228
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +25 -5
M crash_test.go +25 -5
| @@ -4,12 +4,14 @@ package kv | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "bytes" | |
| 7 | + | "errors" | |
| 7 | 8 | "math/rand/v2" | |
| 8 | 9 | "os" | |
| 9 | 10 | "os/exec" | |
| 10 | 11 | "strconv" | |
| 11 | 12 | "strings" | |
| 12 | 13 | "sync" | |
| 14 | + | "syscall" | |
| 13 | 15 | "testing" | |
| 14 | 16 | "time" | |
| 15 | 17 | ) | |
| @@ -58,11 +60,13 @@ func TestCrashChild(t *testing.T) { | |||
| 58 | 60 | } | |
| 59 | 61 | } | |
| 60 | 62 | ||
| 61 | - | // ackReader records the highest seq the child reported. | |
| 63 | + | // ackReader records the highest seq the child reported and keeps every | |
| 64 | + | // other line (test failures are written to stdout). | |
| 62 | 65 | type ackReader struct { | |
| 63 | - | mu sync.Mutex | |
| 64 | - | rest []byte | |
| 65 | - | last int | |
| 66 | + | mu sync.Mutex | |
| 67 | + | rest []byte | |
| 68 | + | last int | |
| 69 | + | other strings.Builder | |
| 66 | 70 | } | |
| 67 | 71 | ||
| 68 | 72 | func (a *ackReader) Write(p []byte) (int, error) { | |
| @@ -76,11 +80,20 @@ func (a *ackReader) Write(p []byte) (int, error) { | |||
| 76 | 80 | } | |
| 77 | 81 | if n, err := strconv.Atoi(string(line)); err == nil { | |
| 78 | 82 | a.last = max(a.last, n) | |
| 83 | + | } else { | |
| 84 | + | a.other.Write(line) | |
| 85 | + | a.other.WriteByte('\n') | |
| 79 | 86 | } | |
| 80 | 87 | a.rest = rest | |
| 81 | 88 | } | |
| 82 | 89 | } | |
| 83 | 90 | ||
| 91 | + | func (a *ackReader) output() string { | |
| 92 | + | a.mu.Lock() | |
| 93 | + | defer a.mu.Unlock() | |
| 94 | + | return a.other.String() | |
| 95 | + | } | |
| 96 | + | ||
| 84 | 97 | func (a *ackReader) lastAcked() int { | |
| 85 | 98 | a.mu.Lock() | |
| 86 | 99 | defer a.mu.Unlock() | |
| @@ -120,7 +133,14 @@ func TestCrash(t *testing.T) { | |||
| 120 | 133 | delay := time.Duration(20+rng.IntN(180)) * time.Millisecond | |
| 121 | 134 | time.Sleep(delay) | |
| 122 | 135 | _ = cmd.Process.Kill() | |
| 123 | - | _ = cmd.Wait() // reports the kill; it also waits for the stdout copy | |
| 136 | + | // Wait also waits for the stdout copy. The child must have died from | |
| 137 | + | // the kill: an earlier exit means the writer itself failed. | |
| 138 | + | werr := cmd.Wait() | |
| 139 | + | var exitErr *exec.ExitError | |
| 140 | + | if !errors.As(werr, &exitErr) || exitErr.Sys().(syscall.WaitStatus).Signal() != syscall.SIGKILL { | |
| 141 | + | t.Fatalf("iteration %d: child did not die from SIGKILL (%v)\nchild output: %s\nchild stderr: %s", | |
| 142 | + | i, werr, acks.output(), stderr.String()) | |
| 143 | + | } | |
| 124 | 144 | acked := acks.lastAcked() | |
| 125 | 145 | ||
| 126 | 146 | db, err := Open(bg, dir, Options{}) | |
| @@ -4,12 +4,14 @@ package kv | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "bytes" | 6 | "bytes" | ||
| 7 | + | "errors" | |||
| 7 | "math/rand/v2" | 8 | "math/rand/v2" | ||
| 8 | "os" | 9 | "os" | ||
| 9 | "os/exec" | 10 | "os/exec" | ||
| 10 | "strconv" | 11 | "strconv" | ||
| 11 | "strings" | 12 | "strings" | ||
| 12 | "sync" | 13 | "sync" | ||
| 14 | + | "syscall" | |||
| 13 | "testing" | 15 | "testing" | ||
| 14 | "time" | 16 | "time" | ||
| 15 | ) | 17 | ) | ||
| @@ -58,11 +60,13 @@ func TestCrashChild(t *testing.T) { | |||||
| 58 | } | 60 | } | ||
| 59 | } | 61 | } | ||
| 60 | 62 | ||||
| 61 | - | // ackReader records the highest seq the child reported. | 63 | + | // ackReader records the highest seq the child reported and keeps every |
| 64 | + | // other line (test failures are written to stdout). | |||
| 62 | type ackReader struct { | 65 | type ackReader struct { | ||
| 63 | - | mu sync.Mutex | 66 | + | mu sync.Mutex |
| 64 | - | rest []byte | 67 | + | rest []byte |
| 65 | - | last int | 68 | + | last int |
| 69 | + | other strings.Builder | |||
| 66 | } | 70 | } | ||
| 67 | 71 | ||||
| 68 | func (a *ackReader) Write(p []byte) (int, error) { | 72 | func (a *ackReader) Write(p []byte) (int, error) { | ||
| @@ -76,11 +80,20 @@ func (a *ackReader) Write(p []byte) (int, error) { | |||||
| 76 | } | 80 | } | ||
| 77 | if n, err := strconv.Atoi(string(line)); err == nil { | 81 | if n, err := strconv.Atoi(string(line)); err == nil { | ||
| 78 | a.last = max(a.last, n) | 82 | a.last = max(a.last, n) | ||
| 83 | + | } else { | |||
| 84 | + | a.other.Write(line) | |||
| 85 | + | a.other.WriteByte('\n') | |||
| 79 | } | 86 | } | ||
| 80 | a.rest = rest | 87 | a.rest = rest | ||
| 81 | } | 88 | } | ||
| 82 | } | 89 | } | ||
| 83 | 90 | ||||
| 91 | + | func (a *ackReader) output() string { | |||
| 92 | + | a.mu.Lock() | |||
| 93 | + | defer a.mu.Unlock() | |||
| 94 | + | return a.other.String() | |||
| 95 | + | } | |||
| 96 | + | ||||
| 84 | func (a *ackReader) lastAcked() int { | 97 | func (a *ackReader) lastAcked() int { | ||
| 85 | a.mu.Lock() | 98 | a.mu.Lock() | ||
| 86 | defer a.mu.Unlock() | 99 | defer a.mu.Unlock() | ||
| @@ -120,7 +133,14 @@ func TestCrash(t *testing.T) { | |||||
| 120 | delay := time.Duration(20+rng.IntN(180)) * time.Millisecond | 133 | delay := time.Duration(20+rng.IntN(180)) * time.Millisecond | ||
| 121 | time.Sleep(delay) | 134 | time.Sleep(delay) | ||
| 122 | _ = cmd.Process.Kill() | 135 | _ = cmd.Process.Kill() | ||
| 123 | - | _ = cmd.Wait() // reports the kill; it also waits for the stdout copy | 136 | + | // Wait also waits for the stdout copy. The child must have died from |
| 137 | + | // the kill: an earlier exit means the writer itself failed. | |||
| 138 | + | werr := cmd.Wait() | |||
| 139 | + | var exitErr *exec.ExitError | |||
| 140 | + | if !errors.As(werr, &exitErr) || exitErr.Sys().(syscall.WaitStatus).Signal() != syscall.SIGKILL { | |||
| 141 | + | t.Fatalf("iteration %d: child did not die from SIGKILL (%v)\nchild output: %s\nchild stderr: %s", | |||
| 142 | + | i, werr, acks.output(), stderr.String()) | |||
| 143 | + | } | |||
| 124 | acked := acks.lastAcked() | 144 | acked := acks.lastAcked() | ||
| 125 | 145 | ||||
| 126 | db, err := Open(bg, dir, Options{}) | 146 | db, err := Open(bg, dir, Options{}) | ||