package tools import ( "os" "path/filepath" "strings" "testing" "time" ) // runToEnd starts a command and waits for it to finish. func runToEnd(t *testing.T, command, dir string) *Run { t.Helper() run, err := Start(command, dir, nil) if err != nil { t.Fatalf("Start(%q) error = %v", command, err) } waitUntilDone(t, run) return run } // waitUntilDone waits for a command to end, failing the test if it never does. func waitUntilDone(t *testing.T, run *Run) { t.Helper() deadline := time.After(20 * time.Second) for { if finished, _ := run.Done(); finished { return } select { case <-deadline: t.Fatalf("%q never finished", run.Command()) case <-time.After(5 * time.Millisecond): } } } func TestStartCollectsWhatACommandPrints(t *testing.T) { run := runToEnd(t, "echo one; echo two", t.TempDir()) got := run.Lines() if len(got) != 2 || got[0] != "one" || got[1] != "two" { t.Errorf("Lines() = %v, want [one two]", got) } } func TestStandardErrorIsMergedInOrder(t *testing.T) { // A compiler's errors and its progress belong together, in the order it // wrote them. run := runToEnd(t, "echo out; echo err >&2; echo out2", t.TempDir()) got := strings.Join(run.Lines(), "|") if got != "out|err|out2" { t.Errorf("Lines() = %q, want the two streams interleaved as written", got) } } func TestTheExitCodeIsReported(t *testing.T) { for command, want := range map[string]int{ "true": 0, "exit 3": 3, "exit 1": 1, "false": 1, "nosuchcm": 127, } { t.Run(command, func(t *testing.T) { run := runToEnd(t, command, t.TempDir()) finished, code := run.Done() if !finished { t.Fatal("Done() reported unfinished after waiting") } if code != want { t.Errorf("exit code = %d, want %d", code, want) } }) } } func TestACommandIsNotDoneWhileItRuns(t *testing.T) { run, err := Start("sleep 5", t.TempDir(), nil) if err != nil { t.Fatalf("Start() error = %v", err) } t.Cleanup(run.Stop) if finished, _ := run.Done(); finished { t.Error("a command that has just started reports itself finished") } } func TestTheCommandRunsInTheDirectoryGiven(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "marker-is-here"), nil, 0o644); err != nil { t.Fatalf("creating the marker: %v", err) } run := runToEnd(t, "ls", dir) if got := strings.Join(run.Lines(), "\n"); !strings.Contains(got, "marker-is-here") { t.Errorf("the command ran somewhere else; ls gave %q", got) } } func TestACommandLineWithPipesAndAndsWorks(t *testing.T) { // The whole reason it goes through a shell: one entry can be a sequence. run := runToEnd(t, "echo b; echo a | sort && echo done", t.TempDir()) if got := strings.Join(run.Lines(), "|"); got != "b|a|done" { t.Errorf("Lines() = %q", got) } } func TestOnLineIsCalledAsOutputArrivesAndAtTheEnd(t *testing.T) { calls := make(chan struct{}, 64) run, err := Start("echo one; echo two", t.TempDir(), func() { select { case calls <- struct{}{}: default: } }) if err != nil { t.Fatalf("Start() error = %v", err) } waitUntilDone(t, run) // Two lines and one ending: at least three, and never zero, which is what // would leave a dialog blank for ever. if len(calls) < 3 { t.Errorf("onLine was called %d times, want one per line plus the end", len(calls)) } } func TestStopEndsACommandThatWouldNotStopItself(t *testing.T) { run, err := Start("sleep 60", t.TempDir(), nil) if err != nil { t.Fatalf("Start() error = %v", err) } run.Stop() waitUntilDone(t, run) if _, code := run.Done(); code == 0 { t.Error("a killed command reported success") } } func TestStopEndsWhatTheCommandStartedToo(t *testing.T) { // Killing only the shell leaves a grandchild holding the output pipe, so // the reading goroutine blocks until *it* ends — which for `go test ./...` // means every test binary it spawned. Stopping has to take the group. // The echo is what proves the background child exists before we stop // anything: killing the shell before it has forked would make this pass // for the wrong reason. run, err := Start("(sleep 30) & echo forked; wait", t.TempDir(), nil) if err != nil { t.Fatalf("Start() error = %v", err) } waitUntilPrinted(t, run, "forked") start := time.Now() run.Stop() waitUntilDone(t, run) if elapsed := time.Since(start); elapsed > 5*time.Second { t.Errorf("Done() took %s after Stop(); something is still holding the pipe", elapsed) } } // waitUntilPrinted waits for a command to have printed a line, which is the // only honest evidence that it has got as far as doing something. func waitUntilPrinted(t *testing.T, run *Run, want string) { t.Helper() deadline := time.After(10 * time.Second) for { for _, line := range run.Lines() { if line == want { return } } select { case <-deadline: t.Fatalf("the command never printed %q; it printed %v", want, run.Lines()) case <-time.After(5 * time.Millisecond): } } } func TestStoppingAFinishedCommandIsSafe(t *testing.T) { run := runToEnd(t, "true", t.TempDir()) run.Stop() run.Stop() if _, code := run.Done(); code != 0 { t.Errorf("stopping a finished command changed its exit code to %d", code) } } func TestACommandThatPrintsNothingGivesNoLines(t *testing.T) { // go build ./... succeeding is silent, and the caller has to be able to // tell that from a command that has not started yet. run := runToEnd(t, "true", t.TempDir()) if got := run.Lines(); len(got) != 0 { t.Errorf("Lines() = %v, want nothing", got) } if finished, code := run.Done(); !finished || code != 0 { t.Errorf("Done() = %v, %d; want finished and successful", finished, code) } } func TestLinesIsACopy(t *testing.T) { // A caller holds it while the command goes on writing. run := runToEnd(t, "echo one", t.TempDir()) lines := run.Lines() lines[0] = "tampered" if got := run.Lines(); got[0] != "one" { t.Errorf("Lines() = %v; the caller's slice is the run's own", got) } } func TestOutputPastTheLimitDropsTheOldestAndSaysSo(t *testing.T) { // The tail of a failing build is the part that matters, so it is the head // that goes — and the caller is told, rather than quietly shown less. run := runToEnd(t, "seq 1 10500", t.TempDir()) lines := run.Lines() if len(lines) != maxLines { t.Fatalf("Lines() kept %d lines, want the cap of %d", len(lines), maxLines) } if got := lines[len(lines)-1]; got != "10500" { t.Errorf("the last line is %q, want the newest output", got) } if run.Dropped() != 500 { t.Errorf("Dropped() = %d, want 500", run.Dropped()) } } func TestStartFailsWhenTheShellIsNotThere(t *testing.T) { // Not a real risk on a Unix, but the error path exists and returning nil, // nil from it would be a nil dereference in the caller. if _, err := Start("true", "/nonexistent-directory-for-a-test", nil); err == nil { t.Error("Start() accepted a directory that does not exist") } } func TestCommandReportsWhatIsBeingRun(t *testing.T) { run := runToEnd(t, "echo x", t.TempDir()) if got := run.Command(); got != "echo x" { t.Errorf("Command() = %q", got) } }