// Copyright 2018 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package terminal import ( "bytes" "fmt" "os" "syscall" "testing" "android/soong/ui/status" ) func TestStatusOutput(t *testing.T) { tests := []struct { name string calls func(stat status.StatusOutput) smart string simple string }{ { name: "two actions", calls: twoActions, 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", simple: "[ 50% 1/2] action1\n[100% 2/2] action2\n", }, { name: "two parallel actions", calls: twoParallelActions, 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", simple: "[ 50% 1/2] action1\n[100% 2/2] action2\n", }, { name: "action with output", calls: actionsWithOutput, 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", simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n", }, { name: "action with output without newline", calls: actionsWithOutputWithoutNewline, 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", simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\noutput1\noutput2\n[100% 3/3] action3\n", }, { name: "action with error", calls: actionsWithError, 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\nFAILED: 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", simple: "[ 33% 1/3] action1\n[ 66% 2/3] action2\nFAILED: f1 f2\ntouch f1 f2\nerror1\nerror2\n[100% 3/3] action3\n", }, { name: "action with empty description", calls: actionWithEmptyDescription, smart: "\r\x1b[1m[ 0% 0/1] command1\x1b[0m\x1b[K\r\x1b[1m[100% 1/1] command1\x1b[0m\x1b[K\n", simple: "[100% 1/1] command1\n", }, { name: "messages", calls: actionsWithMessages, 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\nFAILED: 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", simple: "[ 50% 1/2] action1\nstatus\nprint\nFAILED: error\n[100% 2/2] action2\n", }, { name: "action with long description", calls: actionWithLongDescription, 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", simple: "[ 50% 1/2] action with very long description to test eliding\n", }, { name: "action with output with ansi codes", calls: actionWithOutputWithAnsiCodes, 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", simple: "[100% 1/1] action1\ncolor\ncolor message\n", }, } os.Setenv(tableHeightEnVar, "") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Run("smart", func(t *testing.T) { smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) tt.calls(stat) stat.Flush() if g, w := smart.String(), tt.smart; g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } }) t.Run("simple", func(t *testing.T) { simple := &bytes.Buffer{} stat := NewStatusOutput(simple, "", false, false, false) tt.calls(stat) stat.Flush() if g, w := simple.String(), tt.simple; g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } }) t.Run("force simple", func(t *testing.T) { smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", true, false, false) tt.calls(stat) stat.Flush() if g, w := smart.String(), tt.simple; g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } }) }) } } type runner struct { counts status.Counts stat status.StatusOutput } func newRunner(stat status.StatusOutput, totalActions int) *runner { return &runner{ counts: status.Counts{TotalActions: totalActions}, stat: stat, } } func (r *runner) startAction(action *status.Action) { r.counts.StartedActions++ r.counts.RunningActions++ r.stat.StartAction(action, r.counts) } func (r *runner) finishAction(result status.ActionResult) { r.counts.FinishedActions++ r.counts.RunningActions-- r.stat.FinishAction(result, r.counts) } func (r *runner) finishAndStartAction(result status.ActionResult, action *status.Action) { r.counts.FinishedActions++ r.stat.FinishAction(result, r.counts) r.counts.StartedActions++ r.stat.StartAction(action, r.counts) } var ( action1 = &status.Action{Description: "action1"} result1 = status.ActionResult{Action: action1} action2 = &status.Action{Description: "action2"} result2 = status.ActionResult{Action: action2} action3 = &status.Action{Description: "action3"} result3 = status.ActionResult{Action: action3} ) func twoActions(stat status.StatusOutput) { runner := newRunner(stat, 2) runner.startAction(action1) runner.finishAction(result1) runner.startAction(action2) runner.finishAction(result2) } func twoParallelActions(stat status.StatusOutput) { runner := newRunner(stat, 2) runner.startAction(action1) runner.startAction(action2) runner.finishAction(result1) runner.finishAction(result2) } func actionsWithOutput(stat status.StatusOutput) { result2WithOutput := status.ActionResult{Action: action2, Output: "output1\noutput2\n"} runner := newRunner(stat, 3) runner.startAction(action1) runner.finishAction(result1) runner.startAction(action2) runner.finishAction(result2WithOutput) runner.startAction(action3) runner.finishAction(result3) } func actionsWithOutputWithoutNewline(stat status.StatusOutput) { result2WithOutputWithoutNewline := status.ActionResult{Action: action2, Output: "output1\noutput2"} runner := newRunner(stat, 3) runner.startAction(action1) runner.finishAction(result1) runner.startAction(action2) runner.finishAction(result2WithOutputWithoutNewline) runner.startAction(action3) runner.finishAction(result3) } func actionsWithError(stat status.StatusOutput) { action2WithError := &status.Action{Description: "action2", Outputs: []string{"f1", "f2"}, Command: "touch f1 f2"} result2WithError := status.ActionResult{Action: action2WithError, Output: "error1\nerror2\n", Error: fmt.Errorf("error1")} runner := newRunner(stat, 3) runner.startAction(action1) runner.finishAction(result1) runner.startAction(action2WithError) runner.finishAction(result2WithError) runner.startAction(action3) runner.finishAction(result3) } func actionWithEmptyDescription(stat status.StatusOutput) { action1 := &status.Action{Command: "command1"} result1 := status.ActionResult{Action: action1} runner := newRunner(stat, 1) runner.startAction(action1) runner.finishAction(result1) } func actionsWithMessages(stat status.StatusOutput) { runner := newRunner(stat, 2) runner.startAction(action1) runner.finishAction(result1) stat.Message(status.VerboseLvl, "verbose") stat.Message(status.StatusLvl, "status") stat.Message(status.PrintLvl, "print") stat.Message(status.ErrorLvl, "error") runner.startAction(action2) runner.finishAction(result2) } func actionWithLongDescription(stat status.StatusOutput) { action1 := &status.Action{Description: "action with very long description to test eliding"} result1 := status.ActionResult{Action: action1} runner := newRunner(stat, 2) runner.startAction(action1) runner.finishAction(result1) } func actionWithOutputWithAnsiCodes(stat status.StatusOutput) { result1WithOutputWithAnsiCodes := status.ActionResult{Action: action1, Output: "\x1b[31mcolor\x1b[0m"} runner := newRunner(stat, 1) runner.startAction(action1) runner.finishAction(result1WithOutputWithAnsiCodes) stat.Message(status.PrintLvl, "\x1b[31mcolor message\x1b[0m") } func TestSmartStatusOutputWidthChange(t *testing.T) { os.Setenv(tableHeightEnVar, "") smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) smartStat := stat.(*smartStatusOutput) smartStat.sigwinchHandled = make(chan bool) runner := newRunner(stat, 2) action := &status.Action{Description: "action with very long description to test eliding"} result := status.ActionResult{Action: action} runner.startAction(action) smart.termWidth = 30 // Fake a SIGWINCH smartStat.sigwinch <- syscall.SIGWINCH <-smartStat.sigwinchHandled runner.finishAction(result) stat.Flush() 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" if g := smart.String(); g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } } func TestSmartStatusDoesntHideAfterSucecss(t *testing.T) { os.Setenv(tableHeightEnVar, "") smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) smartStat := stat.(*smartStatusOutput) smartStat.sigwinchHandled = make(chan bool) runner := newRunner(stat, 2) action1 := &status.Action{Description: "action1"} result1 := status.ActionResult{ Action: action1, Output: "Output1", } action2 := &status.Action{Description: "action2"} result2 := status.ActionResult{ Action: action2, Output: "Output2", } runner.startAction(action1) runner.startAction(action2) runner.finishAction(result1) runner.finishAction(result2) stat.Flush() 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" if g := smart.String(); g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } } func TestSmartStatusHideAfterFailure(t *testing.T) { os.Setenv(tableHeightEnVar, "") smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) smartStat := stat.(*smartStatusOutput) smartStat.sigwinchHandled = make(chan bool) runner := newRunner(stat, 2) action1 := &status.Action{Description: "action1"} result1 := status.ActionResult{ Action: action1, Output: "Output1", Error: fmt.Errorf("Error1"), } action2 := &status.Action{Description: "action2"} result2 := status.ActionResult{ Action: action2, Output: "Output2", } runner.startAction(action1) runner.startAction(action2) runner.finishAction(result1) runner.finishAction(result2) stat.Flush() 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\nFAILED: \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" if g := smart.String(); g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } } func TestSmartStatusHideAfterFailurePlural(t *testing.T) { os.Setenv(tableHeightEnVar, "") smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) smartStat := stat.(*smartStatusOutput) smartStat.sigwinchHandled = make(chan bool) runner := newRunner(stat, 2) action1 := &status.Action{Description: "action1"} result1 := status.ActionResult{ Action: action1, Output: "Output1", Error: fmt.Errorf("Error1"), } action2 := &status.Action{Description: "action2"} result2 := status.ActionResult{ Action: action2, Output: "Output2", } action3 := &status.Action{Description: "action3"} result3 := status.ActionResult{ Action: action3, Output: "Output3", } runner.startAction(action1) runner.startAction(action2) runner.startAction(action3) runner.finishAction(result1) runner.finishAction(result2) runner.finishAction(result3) stat.Flush() 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\nFAILED: \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" if g := smart.String(); g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } } func TestSmartStatusDontHideErrorAfterFailure(t *testing.T) { os.Setenv(tableHeightEnVar, "") smart := &fakeSmartTerminal{termWidth: 40} stat := NewStatusOutput(smart, "", false, false, false) smartStat := stat.(*smartStatusOutput) smartStat.sigwinchHandled = make(chan bool) runner := newRunner(stat, 2) action1 := &status.Action{Description: "action1"} result1 := status.ActionResult{ Action: action1, Output: "Output1", Error: fmt.Errorf("Error1"), } action2 := &status.Action{Description: "action2"} result2 := status.ActionResult{ Action: action2, Output: "Output2", Error: fmt.Errorf("Error1"), } runner.startAction(action1) runner.startAction(action2) runner.finishAction(result1) runner.finishAction(result2) stat.Flush() 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\nFAILED: \nOutput1\n\r\x1b[1m[100% 2/2] action2\x1b[0m\x1b[K\nFAILED: \nOutput2\n" if g := smart.String(); g != w { t.Errorf("want:\n%q\ngot:\n%q", w, g) } }