turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

run_test.go · 259 lines · 6.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 16h ago1package tools
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9)
10
11// runToEnd starts a command and waits for it to finish.
12func 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.
24func 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
40func 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
49func 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
60func 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
82func 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
94func 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
107func 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
116func 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
136func 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
150func 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.
174func 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
192func 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
203func 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
216func 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
228func 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
245func 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
253func 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}