xref: /aosp_15_r20/build/soong/ui/terminal/status_test.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2018 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package terminal
16
17import (
18	"bytes"
19	"fmt"
20	"os"
21	"syscall"
22	"testing"
23
24	"android/soong/ui/status"
25)
26
27func TestStatusOutput(t *testing.T) {
28	tests := []struct {
29		name   string
30		calls  func(stat status.StatusOutput)
31		smart  string
32		simple string
33	}{
34		{
35			name:   "two actions",
36			calls:  twoActions,
37			smart:  "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
38			simple: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
39		},
40		{
41			name:   "two parallel actions",
42			calls:  twoParallelActions,
43			smart:  "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
44			simple: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
45		},
46		{
47			name:   "action with output",
48			calls:  actionsWithOutput,
49			smart:  "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
50			simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
51		},
52		{
53			name:   "action with output without newline",
54			calls:  actionsWithOutputWithoutNewline,
55			smart:  "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\noutput1\noutput2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
56			simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
57		},
58		{
59			name:   "action with error",
60			calls:  actionsWithError,
61			smart:  "\r\x1b[1m[  0% 0/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action1\x1b[0m\x1b[K\r\x1b[1m[ 33% 1/3] action2\x1b[0m\x1b[K\r\x1b[1m[ 66% 2/3] action2\x1b[0m\x1b[K\n\x1b[31m\x1b[1mFAILED:\x1b[0m f1 f2\ntouch f1 f2\nerror1\nerror2\n\r\x1b[1m[ 66% 2/3] action3\x1b[0m\x1b[K\r\x1b[1m[100% 3/3] action3\x1b[0m\x1b[K\n",
62			simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n",
63		},
64		{
65			name:   "action with empty description",
66			calls:  actionWithEmptyDescription,
67			smart:  "\r\x1b[1m[  0% 0/1] command1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] command1\x1b[0m\x1b[K\n",
68			simple: "[100% 1/1] command1\n",
69		},
70		{
71			name:   "messages",
72			calls:  actionsWithMessages,
73			smart:  "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\r\x1b[1mstatus\x1b[0m\x1b[K\r\x1b[Kprint\n\x1b[31m\x1b[1mFAILED:\x1b[0m error\n\r\x1b[1m[ 50% 1/2] action2\x1b[0m\x1b[K\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n",
74			simple: "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n",
75		},
76		{
77			name:   "action with long description",
78			calls:  actionWithLongDescription,
79			smart:  "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very long descrip\x1b[0m\x1b[K\n",
80			simple: "[ 50% 1/2] action with very long description to test eliding\n",
81		},
82		{
83			name:   "action with output with ansi codes",
84			calls:  actionWithOutputWithAnsiCodes,
85			smart:  "\r\x1b[1m[  0% 0/1] action1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] action1\x1b[0m\x1b[K\n\x1b[31mcolor\x1b[0m\n\x1b[31mcolor message\x1b[0m\n",
86			simple: "[100% 1/1] action1\ncolor\ncolor message\n",
87		},
88	}
89
90	os.Setenv(tableHeightEnVar, "")
91
92	for _, tt := range tests {
93		t.Run(tt.name, func(t *testing.T) {
94
95			t.Run("smart", func(t *testing.T) {
96				smart := &fakeSmartTerminal{termWidth: 40}
97				stat := NewStatusOutput(smart, "", false, false, false)
98				tt.calls(stat)
99				stat.Flush()
100
101				if g, w := smart.String(), tt.smart; g != w {
102					t.Errorf("want:\n%q\ngot:\n%q", w, g)
103				}
104			})
105
106			t.Run("simple", func(t *testing.T) {
107				simple := &bytes.Buffer{}
108				stat := NewStatusOutput(simple, "", false, false, false)
109				tt.calls(stat)
110				stat.Flush()
111
112				if g, w := simple.String(), tt.simple; g != w {
113					t.Errorf("want:\n%q\ngot:\n%q", w, g)
114				}
115			})
116
117			t.Run("force simple", func(t *testing.T) {
118				smart := &fakeSmartTerminal{termWidth: 40}
119				stat := NewStatusOutput(smart, "", true, false, false)
120				tt.calls(stat)
121				stat.Flush()
122
123				if g, w := smart.String(), tt.simple; g != w {
124					t.Errorf("want:\n%q\ngot:\n%q", w, g)
125				}
126			})
127		})
128	}
129}
130
131type runner struct {
132	counts status.Counts
133	stat   status.StatusOutput
134}
135
136func newRunner(stat status.StatusOutput, totalActions int) *runner {
137	return &runner{
138		counts: status.Counts{TotalActions: totalActions},
139		stat:   stat,
140	}
141}
142
143func (r *runner) startAction(action *status.Action) {
144	r.counts.StartedActions++
145	r.counts.RunningActions++
146	r.stat.StartAction(action, r.counts)
147}
148
149func (r *runner) finishAction(result status.ActionResult) {
150	r.counts.FinishedActions++
151	r.counts.RunningActions--
152	r.stat.FinishAction(result, r.counts)
153}
154
155func (r *runner) finishAndStartAction(result status.ActionResult, action *status.Action) {
156	r.counts.FinishedActions++
157	r.stat.FinishAction(result, r.counts)
158
159	r.counts.StartedActions++
160	r.stat.StartAction(action, r.counts)
161}
162
163var (
164	action1 = &status.Action{Description: "action1"}
165	result1 = status.ActionResult{Action: action1}
166	action2 = &status.Action{Description: "action2"}
167	result2 = status.ActionResult{Action: action2}
168	action3 = &status.Action{Description: "action3"}
169	result3 = status.ActionResult{Action: action3}
170)
171
172func twoActions(stat status.StatusOutput) {
173	runner := newRunner(stat, 2)
174	runner.startAction(action1)
175	runner.finishAction(result1)
176	runner.startAction(action2)
177	runner.finishAction(result2)
178}
179
180func twoParallelActions(stat status.StatusOutput) {
181	runner := newRunner(stat, 2)
182	runner.startAction(action1)
183	runner.startAction(action2)
184	runner.finishAction(result1)
185	runner.finishAction(result2)
186}
187
188func actionsWithOutput(stat status.StatusOutput) {
189	result2WithOutput := status.ActionResult{Action: action2, Output: "output1\noutput2\n"}
190
191	runner := newRunner(stat, 3)
192	runner.startAction(action1)
193	runner.finishAction(result1)
194	runner.startAction(action2)
195	runner.finishAction(result2WithOutput)
196	runner.startAction(action3)
197	runner.finishAction(result3)
198}
199
200func actionsWithOutputWithoutNewline(stat status.StatusOutput) {
201	result2WithOutputWithoutNewline := status.ActionResult{Action: action2, Output: "output1\noutput2"}
202
203	runner := newRunner(stat, 3)
204	runner.startAction(action1)
205	runner.finishAction(result1)
206	runner.startAction(action2)
207	runner.finishAction(result2WithOutputWithoutNewline)
208	runner.startAction(action3)
209	runner.finishAction(result3)
210}
211
212func actionsWithError(stat status.StatusOutput) {
213	action2WithError := &status.Action{Description: "action2", Outputs: []string{"f1", "f2"}, Command: "touch f1 f2"}
214	result2WithError := status.ActionResult{Action: action2WithError, Output: "error1\nerror2\n", Error: fmt.Errorf("error1")}
215
216	runner := newRunner(stat, 3)
217	runner.startAction(action1)
218	runner.finishAction(result1)
219	runner.startAction(action2WithError)
220	runner.finishAction(result2WithError)
221	runner.startAction(action3)
222	runner.finishAction(result3)
223}
224
225func actionWithEmptyDescription(stat status.StatusOutput) {
226	action1 := &status.Action{Command: "command1"}
227	result1 := status.ActionResult{Action: action1}
228
229	runner := newRunner(stat, 1)
230	runner.startAction(action1)
231	runner.finishAction(result1)
232}
233
234func actionsWithMessages(stat status.StatusOutput) {
235	runner := newRunner(stat, 2)
236
237	runner.startAction(action1)
238	runner.finishAction(result1)
239
240	stat.Message(status.VerboseLvl, "verbose")
241	stat.Message(status.StatusLvl, "status")
242	stat.Message(status.PrintLvl, "print")
243	stat.Message(status.ErrorLvl, "error")
244
245	runner.startAction(action2)
246	runner.finishAction(result2)
247}
248
249func actionWithLongDescription(stat status.StatusOutput) {
250	action1 := &status.Action{Description: "action with very long description to test eliding"}
251	result1 := status.ActionResult{Action: action1}
252
253	runner := newRunner(stat, 2)
254
255	runner.startAction(action1)
256
257	runner.finishAction(result1)
258}
259
260func actionWithOutputWithAnsiCodes(stat status.StatusOutput) {
261	result1WithOutputWithAnsiCodes := status.ActionResult{Action: action1, Output: "\x1b[31mcolor\x1b[0m"}
262
263	runner := newRunner(stat, 1)
264	runner.startAction(action1)
265	runner.finishAction(result1WithOutputWithAnsiCodes)
266
267	stat.Message(status.PrintLvl, "\x1b[31mcolor message\x1b[0m")
268}
269
270func TestSmartStatusOutputWidthChange(t *testing.T) {
271	os.Setenv(tableHeightEnVar, "")
272
273	smart := &fakeSmartTerminal{termWidth: 40}
274	stat := NewStatusOutput(smart, "", false, false, false)
275	smartStat := stat.(*smartStatusOutput)
276	smartStat.sigwinchHandled = make(chan bool)
277
278	runner := newRunner(stat, 2)
279
280	action := &status.Action{Description: "action with very long description to test eliding"}
281	result := status.ActionResult{Action: action}
282
283	runner.startAction(action)
284	smart.termWidth = 30
285	// Fake a SIGWINCH
286	smartStat.sigwinch <- syscall.SIGWINCH
287	<-smartStat.sigwinchHandled
288	runner.finishAction(result)
289
290	stat.Flush()
291
292	w := "\r\x1b[1m[  0% 0/2] action with very long descrip\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action with very lo\x1b[0m\x1b[K\n"
293
294	if g := smart.String(); g != w {
295		t.Errorf("want:\n%q\ngot:\n%q", w, g)
296	}
297}
298
299func TestSmartStatusDoesntHideAfterSucecss(t *testing.T) {
300	os.Setenv(tableHeightEnVar, "")
301
302	smart := &fakeSmartTerminal{termWidth: 40}
303	stat := NewStatusOutput(smart, "", false, false, false)
304	smartStat := stat.(*smartStatusOutput)
305	smartStat.sigwinchHandled = make(chan bool)
306
307	runner := newRunner(stat, 2)
308
309	action1 := &status.Action{Description: "action1"}
310	result1 := status.ActionResult{
311		Action: action1,
312		Output: "Output1",
313	}
314
315	action2 := &status.Action{Description: "action2"}
316	result2 := status.ActionResult{
317		Action: action2,
318		Output: "Output2",
319	}
320
321	runner.startAction(action1)
322	runner.startAction(action2)
323	runner.finishAction(result1)
324	runner.finishAction(result2)
325
326	stat.Flush()
327
328	w := "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\nOutput1\n\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\nOutput2\n"
329
330	if g := smart.String(); g != w {
331		t.Errorf("want:\n%q\ngot:\n%q", w, g)
332	}
333}
334
335func TestSmartStatusHideAfterFailure(t *testing.T) {
336	os.Setenv(tableHeightEnVar, "")
337
338	smart := &fakeSmartTerminal{termWidth: 40}
339	stat := NewStatusOutput(smart, "", false, false, false)
340	smartStat := stat.(*smartStatusOutput)
341	smartStat.sigwinchHandled = make(chan bool)
342
343	runner := newRunner(stat, 2)
344
345	action1 := &status.Action{Description: "action1"}
346	result1 := status.ActionResult{
347		Action: action1,
348		Output: "Output1",
349		Error:  fmt.Errorf("Error1"),
350	}
351
352	action2 := &status.Action{Description: "action2"}
353	result2 := status.ActionResult{
354		Action: action2,
355		Output: "Output2",
356	}
357
358	runner.startAction(action1)
359	runner.startAction(action2)
360	runner.finishAction(result1)
361	runner.finishAction(result2)
362
363	stat.Flush()
364
365	w := "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\n\x1b[31m\x1b[1mFAILED:\x1b[0m \nOutput1\n\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\nThere was 1 action that completed after the action that failed. See verbose.log.gz for its output.\n"
366
367	if g := smart.String(); g != w {
368		t.Errorf("want:\n%q\ngot:\n%q", w, g)
369	}
370}
371
372func TestSmartStatusHideAfterFailurePlural(t *testing.T) {
373	os.Setenv(tableHeightEnVar, "")
374
375	smart := &fakeSmartTerminal{termWidth: 40}
376	stat := NewStatusOutput(smart, "", false, false, false)
377	smartStat := stat.(*smartStatusOutput)
378	smartStat.sigwinchHandled = make(chan bool)
379
380	runner := newRunner(stat, 2)
381
382	action1 := &status.Action{Description: "action1"}
383	result1 := status.ActionResult{
384		Action: action1,
385		Output: "Output1",
386		Error:  fmt.Errorf("Error1"),
387	}
388
389	action2 := &status.Action{Description: "action2"}
390	result2 := status.ActionResult{
391		Action: action2,
392		Output: "Output2",
393	}
394
395	action3 := &status.Action{Description: "action3"}
396	result3 := status.ActionResult{
397		Action: action3,
398		Output: "Output3",
399	}
400
401	runner.startAction(action1)
402	runner.startAction(action2)
403	runner.startAction(action3)
404	runner.finishAction(result1)
405	runner.finishAction(result2)
406	runner.finishAction(result3)
407
408	stat.Flush()
409
410	w := "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action3\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\n\x1b[31m\x1b[1mFAILED:\x1b[0m \nOutput1\n\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\r\x1b[1m[150% 3/2] action3\x1b[0m\x1b[K\nThere were 2 actions that completed after the action that failed. See verbose.log.gz for their output.\n"
411
412	if g := smart.String(); g != w {
413		t.Errorf("want:\n%q\ngot:\n%q", w, g)
414	}
415}
416
417func TestSmartStatusDontHideErrorAfterFailure(t *testing.T) {
418	os.Setenv(tableHeightEnVar, "")
419
420	smart := &fakeSmartTerminal{termWidth: 40}
421	stat := NewStatusOutput(smart, "", false, false, false)
422	smartStat := stat.(*smartStatusOutput)
423	smartStat.sigwinchHandled = make(chan bool)
424
425	runner := newRunner(stat, 2)
426
427	action1 := &status.Action{Description: "action1"}
428	result1 := status.ActionResult{
429		Action: action1,
430		Output: "Output1",
431		Error:  fmt.Errorf("Error1"),
432	}
433
434	action2 := &status.Action{Description: "action2"}
435	result2 := status.ActionResult{
436		Action: action2,
437		Output: "Output2",
438		Error:  fmt.Errorf("Error1"),
439	}
440
441	runner.startAction(action1)
442	runner.startAction(action2)
443	runner.finishAction(result1)
444	runner.finishAction(result2)
445
446	stat.Flush()
447
448	w := "\r\x1b[1m[  0% 0/2] action1\x1b[0m\x1b[K\r\x1b[1m[  0% 0/2] action2\x1b[0m\x1b[K\r\x1b[1m[ 50% 1/2] action1\x1b[0m\x1b[K\n\x1b[31m\x1b[1mFAILED:\x1b[0m \nOutput1\n\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\n\x1b[31m\x1b[1mFAILED:\x1b[0m \nOutput2\n"
449
450	if g := smart.String(); g != w {
451		t.Errorf("want:\n%q\ngot:\n%q", w, g)
452	}
453}
454