blob: e032c1f1e7eabf529276820626145653a21441d1 [file] [log] [blame]
Colin Crossdde49cb2019-06-08 21:59:42 -07001// 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 "testing"
21
22 "android/soong/ui/status"
23)
24
25func TestStatusOutput(t *testing.T) {
26 tests := []struct {
27 name string
28 calls func(stat status.StatusOutput)
29 smart string
30 dumb string
31 }{
32 {
33 name: "two actions",
34 calls: twoActions,
35 smart: "\r[ 0% 0/2] action1\x1b[K\r[ 50% 1/2] action1\x1b[K\r[ 50% 1/2] action2\x1b[K\r[100% 2/2] action2\x1b[K\n",
36 dumb: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
37 },
38 {
39 name: "two parallel actions",
40 calls: twoParallelActions,
41 smart: "\r[ 0% 0/2] action1\x1b[K\r[ 0% 0/2] action2\x1b[K\r[ 50% 1/2] action1\x1b[K\r[100% 2/2] action2\x1b[K\n",
42 dumb: "[ 50% 1/2] action1\n[100% 2/2] action2\n",
43 },
44 {
45 name: "action with output",
46 calls: actionsWithOutput,
47 smart: "\r[ 0% 0/3] action1\x1b[K\r[ 33% 1/3] action1\x1b[K\r[ 33% 1/3] action2\x1b[K\r[ 66% 2/3] action2\x1b[K\noutput1\noutput2\n\r[ 66% 2/3] action3\x1b[K\r[100% 3/3] action3\x1b[K\n",
48 dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
49 },
50 {
51 name: "action with output without newline",
52 calls: actionsWithOutputWithoutNewline,
53 smart: "\r[ 0% 0/3] action1\x1b[K\r[ 33% 1/3] action1\x1b[K\r[ 33% 1/3] action2\x1b[K\r[ 66% 2/3] action2\x1b[K\noutput1\noutput2\n\r[ 66% 2/3] action3\x1b[K\r[100% 3/3] action3\x1b[K\n",
54 dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n",
55 },
56 {
57 name: "action with error",
58 calls: actionsWithError,
59 smart: "\r[ 0% 0/3] action1\x1b[K\r[ 33% 1/3] action1\x1b[K\r[ 33% 1/3] action2\x1b[K\r[ 66% 2/3] action2\x1b[K\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n\r[ 66% 2/3] action3\x1b[K\r[100% 3/3] action3\x1b[K\n",
60 dumb: "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n",
61 },
62 {
63 name: "action with empty description",
64 calls: actionWithEmptyDescription,
65 smart: "\r[ 0% 0/1] command1\x1b[K\r[100% 1/1] command1\x1b[K\n",
66 dumb: "[100% 1/1] command1\n",
67 },
68 {
69 name: "messages",
70 calls: actionsWithMessages,
71 smart: "\r[ 0% 0/2] action1\x1b[K\r[ 50% 1/2] action1\x1b[K\rstatus\x1b[K\r\x1b[Kprint\nFAILED: error\n\r[ 50% 1/2] action2\x1b[K\r[100% 2/2] action2\x1b[K\n",
72 dumb: "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n",
73 },
74 {
75 name: "action with long description",
76 calls: actionWithLongDescription,
77 smart: "\r[ 0% 0/2] action with very long descrip\x1b[K\r[ 50% 1/2] action with very long descrip\x1b[K\n",
78 dumb: "[ 50% 1/2] action with very long description to test eliding\n",
79 },
80 {
81 name: "action with output with ansi codes",
82 calls: actionWithOuptutWithAnsiCodes,
83 smart: "\r[ 0% 0/1] action1\x1b[K\r[100% 1/1] action1\x1b[K\n\x1b[31mcolor\x1b[0m\n",
84 dumb: "[100% 1/1] action1\ncolor\n",
85 },
86 }
87
88 for _, tt := range tests {
89 t.Run(tt.name, func(t *testing.T) {
90 t.Run("smart", func(t *testing.T) {
91 smart := &fakeSmartTerminal{termWidth: 40}
92 stdio := customStdio{
93 stdin: nil,
94 stdout: smart,
95 stderr: nil,
96 }
97
98 writer := NewWriter(stdio)
99 stat := NewStatusOutput(writer, "", false)
100 tt.calls(stat)
101 stat.Flush()
102 writer.Finish()
103
104 if g, w := smart.String(), tt.smart; g != w {
105 t.Errorf("want:\n%q\ngot:\n%q", w, g)
106 }
107 })
108
109 t.Run("dumb", func(t *testing.T) {
110 dumb := &bytes.Buffer{}
111 stdio := customStdio{
112 stdin: nil,
113 stdout: dumb,
114 stderr: nil,
115 }
116
117 writer := NewWriter(stdio)
118 stat := NewStatusOutput(writer, "", false)
119 tt.calls(stat)
120 stat.Flush()
121 writer.Finish()
122
123 if g, w := dumb.String(), tt.dumb; 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 actionWithOuptutWithAnsiCodes(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
268func TestSmartStatusOutputWidthChange(t *testing.T) {
269 smart := &fakeSmartTerminal{termWidth: 40}
270
271 stdio := customStdio{
272 stdin: nil,
273 stdout: smart,
274 stderr: nil,
275 }
276
277 writer := NewWriter(stdio)
278 stat := NewStatusOutput(writer, "", false)
279
280 runner := newRunner(stat, 2)
281
282 action := &status.Action{Description: "action with very long description to test eliding"}
283 result := status.ActionResult{Action: action}
284
285 runner.startAction(action)
286 smart.termWidth = 30
287 runner.finishAction(result)
288
289 stat.Flush()
290 writer.Finish()
291
292 w := "\r[ 0% 0/2] action with very long descrip\x1b[K\r[ 50% 1/2] action with very lo\x1b[K\n"
293
294 if g := smart.String(); g != w {
295 t.Errorf("want:\n%q\ngot:\n%q", w, g)
296 }
297}