| 🛟 Updated. 28d5985 k33g 6h ago | 1 | package tools |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | // runToEnd starts a command and waits for it to finish. |
| 12 | func runToEnd(t *testing.T, command, dir string) *Run { |
| 13 | t.Helper() |
| 14 | |
| 15 | run, err := Start(command, dir, nil) |
| 16 | if err != nil { |
| 17 | t.Fatalf("Start(%q) error = %v", command, err) |
| 18 | } |
| 19 | waitUntilDone(t, run) |
| 20 | return run |
| 21 | } |
| 22 | |
| 23 | // waitUntilDone waits for a command to end, failing the test if it never does. |
| 24 | func waitUntilDone(t *testing.T, run *Run) { |
| 25 | t.Helper() |
| 26 | |
| 27 | deadline := time.After(20 * time.Second) |
| 28 | for { |
| 29 | if finished, _ := run.Done(); finished { |
| 30 | return |
| 31 | } |
| 32 | select { |
| 33 | case <-deadline: |
| 34 | t.Fatalf("%q never finished", run.Command()) |
| 35 | case <-time.After(5 * time.Millisecond): |
| 36 | } |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | func TestStartCollectsWhatACommandPrints(t *testing.T) { |
| 41 | run := runToEnd(t, "echo one; echo two", t.TempDir()) |
| 42 | |
| 43 | got := run.Lines() |
| 44 | if len(got) != 2 || got[0] != "one" || got[1] != "two" { |
| 45 | t.Errorf("Lines() = %v, want [one two]", got) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestStandardErrorIsMergedInOrder(t *testing.T) { |
| 50 | // A compiler's errors and its progress belong together, in the order it |
| 51 | // wrote them. |
| 52 | run := runToEnd(t, "echo out; echo err >&2; echo out2", t.TempDir()) |
| 53 | |
| 54 | got := strings.Join(run.Lines(), "|") |
| 55 | if got != "out|err|out2" { |
| 56 | t.Errorf("Lines() = %q, want the two streams interleaved as written", got) |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | func TestTheExitCodeIsReported(t *testing.T) { |
| 61 | for command, want := range map[string]int{ |
| 62 | "true": 0, |
| 63 | "exit 3": 3, |
| 64 | "exit 1": 1, |
| 65 | "false": 1, |
| 66 | "nosuchcm": 127, |
| 67 | } { |
| 68 | t.Run(command, func(t *testing.T) { |
| 69 | run := runToEnd(t, command, t.TempDir()) |
| 70 | |
| 71 | finished, code := run.Done() |
| 72 | if !finished { |
| 73 | t.Fatal("Done() reported unfinished after waiting") |
| 74 | } |
| 75 | if code != want { |
| 76 | t.Errorf("exit code = %d, want %d", code, want) |
| 77 | } |
| 78 | }) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | func TestACommandIsNotDoneWhileItRuns(t *testing.T) { |
| 83 | run, err := Start("sleep 5", t.TempDir(), nil) |
| 84 | if err != nil { |
| 85 | t.Fatalf("Start() error = %v", err) |
| 86 | } |
| 87 | t.Cleanup(run.Stop) |
| 88 | |
| 89 | if finished, _ := run.Done(); finished { |
| 90 | t.Error("a command that has just started reports itself finished") |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | func TestTheCommandRunsInTheDirectoryGiven(t *testing.T) { |
| 95 | dir := t.TempDir() |
| 96 | if err := os.WriteFile(filepath.Join(dir, "marker-is-here"), nil, 0o644); err != nil { |
| 97 | t.Fatalf("creating the marker: %v", err) |
| 98 | } |
| 99 | |
| 100 | run := runToEnd(t, "ls", dir) |
| 101 | |
| 102 | if got := strings.Join(run.Lines(), "\n"); !strings.Contains(got, "marker-is-here") { |
| 103 | t.Errorf("the command ran somewhere else; ls gave %q", got) |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | func TestACommandLineWithPipesAndAndsWorks(t *testing.T) { |
| 108 | // The whole reason it goes through a shell: one entry can be a sequence. |
| 109 | run := runToEnd(t, "echo b; echo a | sort && echo done", t.TempDir()) |
| 110 | |
| 111 | if got := strings.Join(run.Lines(), "|"); got != "b|a|done" { |
| 112 | t.Errorf("Lines() = %q", got) |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | func TestOnLineIsCalledAsOutputArrivesAndAtTheEnd(t *testing.T) { |
| 117 | calls := make(chan struct{}, 64) |
| 118 | run, err := Start("echo one; echo two", t.TempDir(), func() { |
| 119 | select { |
| 120 | case calls <- struct{}{}: |
| 121 | default: |
| 122 | } |
| 123 | }) |
| 124 | if err != nil { |
| 125 | t.Fatalf("Start() error = %v", err) |
| 126 | } |
| 127 | waitUntilDone(t, run) |
| 128 | |
| 129 | // Two lines and one ending: at least three, and never zero, which is what |
| 130 | // would leave a dialog blank for ever. |
| 131 | if len(calls) < 3 { |
| 132 | t.Errorf("onLine was called %d times, want one per line plus the end", len(calls)) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func TestStopEndsACommandThatWouldNotStopItself(t *testing.T) { |
| 137 | run, err := Start("sleep 60", t.TempDir(), nil) |
| 138 | if err != nil { |
| 139 | t.Fatalf("Start() error = %v", err) |
| 140 | } |
| 141 | |
| 142 | run.Stop() |
| 143 | |
| 144 | waitUntilDone(t, run) |
| 145 | if _, code := run.Done(); code == 0 { |
| 146 | t.Error("a killed command reported success") |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | func TestStopEndsWhatTheCommandStartedToo(t *testing.T) { |
| 151 | // Killing only the shell leaves a grandchild holding the output pipe, so |
| 152 | // the reading goroutine blocks until *it* ends — which for `go test ./...` |
| 153 | // means every test binary it spawned. Stopping has to take the group. |
| 154 | // The echo is what proves the background child exists before we stop |
| 155 | // anything: killing the shell before it has forked would make this pass |
| 156 | // for the wrong reason. |
| 157 | run, err := Start("(sleep 30) & echo forked; wait", t.TempDir(), nil) |
| 158 | if err != nil { |
| 159 | t.Fatalf("Start() error = %v", err) |
| 160 | } |
| 161 | waitUntilPrinted(t, run, "forked") |
| 162 | |
| 163 | start := time.Now() |
| 164 | run.Stop() |
| 165 | waitUntilDone(t, run) |
| 166 | |
| 167 | if elapsed := time.Since(start); elapsed > 5*time.Second { |
| 168 | t.Errorf("Done() took %s after Stop(); something is still holding the pipe", elapsed) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | // waitUntilPrinted waits for a command to have printed a line, which is the |
| 173 | // only honest evidence that it has got as far as doing something. |
| 174 | func waitUntilPrinted(t *testing.T, run *Run, want string) { |
| 175 | t.Helper() |
| 176 | |
| 177 | deadline := time.After(10 * time.Second) |
| 178 | for { |
| 179 | for _, line := range run.Lines() { |
| 180 | if line == want { |
| 181 | return |
| 182 | } |
| 183 | } |
| 184 | select { |
| 185 | case <-deadline: |
| 186 | t.Fatalf("the command never printed %q; it printed %v", want, run.Lines()) |
| 187 | case <-time.After(5 * time.Millisecond): |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | func TestStoppingAFinishedCommandIsSafe(t *testing.T) { |
| 193 | run := runToEnd(t, "true", t.TempDir()) |
| 194 | |
| 195 | run.Stop() |
| 196 | run.Stop() |
| 197 | |
| 198 | if _, code := run.Done(); code != 0 { |
| 199 | t.Errorf("stopping a finished command changed its exit code to %d", code) |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | func TestACommandThatPrintsNothingGivesNoLines(t *testing.T) { |
| 204 | // go build ./... succeeding is silent, and the caller has to be able to |
| 205 | // tell that from a command that has not started yet. |
| 206 | run := runToEnd(t, "true", t.TempDir()) |
| 207 | |
| 208 | if got := run.Lines(); len(got) != 0 { |
| 209 | t.Errorf("Lines() = %v, want nothing", got) |
| 210 | } |
| 211 | if finished, code := run.Done(); !finished || code != 0 { |
| 212 | t.Errorf("Done() = %v, %d; want finished and successful", finished, code) |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | func TestLinesIsACopy(t *testing.T) { |
| 217 | // A caller holds it while the command goes on writing. |
| 218 | run := runToEnd(t, "echo one", t.TempDir()) |
| 219 | |
| 220 | lines := run.Lines() |
| 221 | lines[0] = "tampered" |
| 222 | |
| 223 | if got := run.Lines(); got[0] != "one" { |
| 224 | t.Errorf("Lines() = %v; the caller's slice is the run's own", got) |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | func TestOutputPastTheLimitDropsTheOldestAndSaysSo(t *testing.T) { |
| 229 | // The tail of a failing build is the part that matters, so it is the head |
| 230 | // that goes — and the caller is told, rather than quietly shown less. |
| 231 | run := runToEnd(t, "seq 1 10500", t.TempDir()) |
| 232 | |
| 233 | lines := run.Lines() |
| 234 | if len(lines) != maxLines { |
| 235 | t.Fatalf("Lines() kept %d lines, want the cap of %d", len(lines), maxLines) |
| 236 | } |
| 237 | if got := lines[len(lines)-1]; got != "10500" { |
| 238 | t.Errorf("the last line is %q, want the newest output", got) |
| 239 | } |
| 240 | if run.Dropped() != 500 { |
| 241 | t.Errorf("Dropped() = %d, want 500", run.Dropped()) |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | func TestStartFailsWhenTheShellIsNotThere(t *testing.T) { |
| 246 | // Not a real risk on a Unix, but the error path exists and returning nil, |
| 247 | // nil from it would be a nil dereference in the caller. |
| 248 | if _, err := Start("true", "/nonexistent-directory-for-a-test", nil); err == nil { |
| 249 | t.Error("Start() accepted a directory that does not exist") |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | func TestCommandReportsWhatIsBeingRun(t *testing.T) { |
| 254 | run := runToEnd(t, "echo x", t.TempDir()) |
| 255 | |
| 256 | if got := run.Command(); got != "echo x" { |
| 257 | t.Errorf("Command() = %q", got) |
| 258 | } |
| 259 | } |