Skip to content

Commit 23aa038

Browse files
ryanslademrnugget
andauthored
Display a progress bar in verbose mode (#209)
* Display a progress bar when running in verbose mode * Cleanups after code review Display patches and failed steps instead of jobs Be more explicit about when we log to the repo loggers Move more display logic into the logger Don't hardcode number of spaces when clearing progress * Split progress and writer progress now keeps track of progress state. progressWriter is a writer that uses current progress to append a progress bar. * Update cmd/src/actions_exec_logger.go Co-authored-by: Thorsten Ball <[email protected]> * Update cmd/src/actions_exec_logger.go Co-authored-by: Thorsten Ball <[email protected]> * Move progress methods * Tweak progress bar labels * Remove dependency between actionLogger and progressWriter Co-authored-by: Thorsten Ball <[email protected]>
1 parent 0cecdc8 commit 23aa038

File tree

3 files changed

+164
-43
lines changed

3 files changed

+164
-43
lines changed

cmd/src/actions_exec.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ Format of the action JSON files:
237237
}
238238
logger.Infof("Use 'src actions scope-query' for help with scoping.\n")
239239

240-
logger.Start()
240+
totalSteps := len(repos) * len(action.Steps)
241+
logger.Start(totalSteps)
241242

242243
executor := newActionExecutor(action, *parallelismFlag, logger, opts)
243244
for _, repo := range repos {

cmd/src/actions_exec_backend_runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (x *actionExecutor) do(ctx context.Context, repo ActionRepo) (err error) {
4646
} else if ok {
4747
status := ActionRepoStatus{Cached: true, Patch: result}
4848
x.updateRepoStatus(repo, status)
49-
x.logger.RepoCacheHit(repo, status.Patch != PatchInput{})
49+
x.logger.RepoCacheHit(repo, len(x.action.Steps), status.Patch != PatchInput{})
5050
return nil
5151
}
5252
}

cmd/src/actions_exec_logger.go

Lines changed: 161 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package main
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
67
"io/ioutil"
78
"os"
89
"strings"
910
"sync"
11+
"sync/atomic"
1012
"time"
1113

1214
"github.com/fatih/color"
@@ -31,35 +33,49 @@ type actionLogger struct {
3133

3234
highlight func(a ...interface{}) string
3335

36+
progress *progress
37+
out io.Writer
38+
39+
mu sync.Mutex
3440
logFiles map[string]*os.File
3541
logWriters map[string]io.Writer
36-
mu sync.Mutex
3742
}
3843

3944
func newActionLogger(verbose, keepLogs bool) *actionLogger {
4045
useColor := isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())
4146
if useColor {
4247
color.NoColor = false
4348
}
49+
50+
progress := new(progress)
51+
4452
return &actionLogger{
45-
verbose: verbose,
46-
keepLogs: keepLogs,
47-
highlight: color.New(color.Bold, color.FgGreen).SprintFunc(),
53+
verbose: verbose,
54+
keepLogs: keepLogs,
55+
highlight: color.New(color.Bold, color.FgGreen).SprintFunc(),
56+
progress: progress,
57+
out: &progressWriter{
58+
p: progress,
59+
w: os.Stderr,
60+
},
4861
logFiles: map[string]*os.File{},
4962
logWriters: map[string]io.Writer{},
5063
}
5164
}
5265

53-
func (a *actionLogger) Start() {
66+
func (a *actionLogger) Start(totalSteps int) {
5467
if a.verbose {
68+
a.progress.SetTotalSteps(int64(totalSteps))
5569
fmt.Fprintln(os.Stderr)
5670
}
5771
}
5872

73+
func (a *actionLogger) Infof(format string, args ...interface{}) {
74+
a.log("", grey, format, args...)
75+
}
76+
5977
func (a *actionLogger) Warnf(format string, args ...interface{}) {
60-
if a.verbose {
61-
yellow.Fprintf(os.Stderr, "WARNING: "+format, args...)
62-
}
78+
a.log("", yellow, "WARNING: "+format, args...)
6379
}
6480

6581
func (a *actionLogger) ActionFailed(err error, patches []PatchInput) {
@@ -89,31 +105,25 @@ func (a *actionLogger) ActionFailed(err error, patches []PatchInput) {
89105
}
90106

91107
func (a *actionLogger) ActionSuccess(patches []PatchInput, newLines bool) {
92-
if a.verbose {
93-
fmt.Fprintln(os.Stderr)
94-
format := "✔ Action produced %d patches."
95-
if newLines {
96-
format = format + "\n\n"
97-
}
98-
hiGreen.Fprintf(os.Stderr, format, len(patches))
108+
if !a.verbose {
109+
return
99110
}
100-
}
101-
102-
func (a *actionLogger) Infof(format string, args ...interface{}) {
103-
if a.verbose {
104-
grey.Fprintf(os.Stderr, format, args...)
111+
fmt.Fprintln(os.Stderr)
112+
format := "✔ Action produced %d patches."
113+
if newLines {
114+
format = format + "\n\n"
105115
}
116+
hiGreen.Fprintf(os.Stderr, format, len(patches))
106117
}
107118

108-
func (a *actionLogger) RepoCacheHit(repo ActionRepo, patchProduced bool) {
109-
if a.verbose {
110-
if patchProduced {
111-
fmt.Fprintf(os.Stderr, "%s -> Cached result found: using cached diff.\n", boldGreen.Sprint(repo.Name))
112-
return
113-
}
114-
115-
fmt.Fprintf(os.Stderr, "%s -> Cached result found: no diff produced for this repository.\n", grey.Sprint(repo.Name))
119+
func (a *actionLogger) RepoCacheHit(repo ActionRepo, stepCount int, patchProduced bool) {
120+
a.progress.IncStepsComplete(int64(stepCount))
121+
if patchProduced {
122+
a.progress.IncPatchCount()
123+
a.log(repo.Name, boldGreen, "Cached result found: using cached diff.\n")
124+
return
116125
}
126+
a.log(repo.Name, grey, "Cached result found: no diff produced for this repository.\n")
117127
}
118128

119129
func (a *actionLogger) AddRepo(repo ActionRepo) (string, error) {
@@ -151,25 +161,14 @@ func (a *actionLogger) RepoStdoutStderr(repoName string) (io.Writer, io.Writer,
151161
}
152162

153163
stderrPrefix := fmt.Sprintf("%s -> [STDERR]: ", yellow.Sprint(repoName))
154-
stderr := textio.NewPrefixWriter(os.Stderr, stderrPrefix)
164+
stderr := textio.NewPrefixWriter(a.out, stderrPrefix)
155165

156166
stdoutPrefix := fmt.Sprintf("%s -> [STDOUT]: ", yellow.Sprint(repoName))
157-
stdout := textio.NewPrefixWriter(os.Stderr, stdoutPrefix)
167+
stdout := textio.NewPrefixWriter(a.out, stdoutPrefix)
158168

159169
return io.MultiWriter(stdout, w), io.MultiWriter(stderr, w), ok
160170
}
161171

162-
func (a *actionLogger) write(repoName string, c *color.Color, format string, args ...interface{}) {
163-
if w, ok := a.RepoWriter(repoName); ok {
164-
fmt.Fprintf(w, format, args...)
165-
}
166-
167-
if a.verbose {
168-
format = fmt.Sprintf("%s -> %s", c.Sprint(repoName), format)
169-
fmt.Fprintf(os.Stderr, format, args...)
170-
}
171-
}
172-
173172
func (a *actionLogger) RepoFinished(repoName string, patchProduced bool, actionErr error) error {
174173
a.mu.Lock()
175174
f, ok := a.logFiles[repoName]
@@ -186,6 +185,7 @@ func (a *actionLogger) RepoFinished(repoName string, patchProduced bool, actionE
186185
a.write(repoName, boldRed, "Action failed: %q\n", actionErr)
187186
}
188187
} else if patchProduced {
188+
a.progress.IncPatchCount()
189189
a.write(repoName, boldGreen, "Finished. Patch produced.\n")
190190
} else {
191191
a.write(repoName, grey, "Finished. No patch produced.\n")
@@ -215,10 +215,13 @@ func (a *actionLogger) CommandStepStarted(repoName string, step int, args []stri
215215
}
216216

217217
func (a *actionLogger) CommandStepErrored(repoName string, step int, err error) {
218+
a.progress.IncStepsComplete(1)
219+
a.progress.IncStepsFailed()
218220
a.write(repoName, boldRed, "%s %s.\n", boldBlack.Sprintf("[Step %d]", step), err)
219221
}
220222

221223
func (a *actionLogger) CommandStepDone(repoName string, step int) {
224+
a.progress.IncStepsComplete(1)
222225
a.write(repoName, yellow, "%s Done.\n", boldBlack.Sprintf("[Step %d]", step))
223226
}
224227

@@ -227,9 +230,126 @@ func (a *actionLogger) DockerStepStarted(repoName string, step int, image string
227230
}
228231

229232
func (a *actionLogger) DockerStepErrored(repoName string, step int, err error, elapsed time.Duration) {
233+
a.progress.IncStepsComplete(1)
234+
a.progress.IncStepsFailed()
230235
a.write(repoName, boldRed, "%s %s. (%s)\n", boldBlack.Sprintf("[Step %d]", step), err, elapsed)
231236
}
232237

233238
func (a *actionLogger) DockerStepDone(repoName string, step int, elapsed time.Duration) {
239+
a.progress.IncStepsComplete(1)
234240
a.write(repoName, yellow, "%s Done. (%s)\n", boldBlack.Sprintf("[Step %d]", step), elapsed)
235241
}
242+
243+
// write writes to the RepoWriter associated with the given repoName and logs the message using the log method.
244+
func (a *actionLogger) write(repoName string, c *color.Color, format string, args ...interface{}) {
245+
if w, ok := a.RepoWriter(repoName); ok {
246+
fmt.Fprintf(w, format, args...)
247+
}
248+
a.log(repoName, c, format, args...)
249+
}
250+
251+
// log logs only to stderr, it does not log to our repoWriters. When not in verbose mode, it's a noop.
252+
func (a *actionLogger) log(repoName string, c *color.Color, format string, args ...interface{}) {
253+
if !a.verbose {
254+
return
255+
}
256+
if len(repoName) > 0 {
257+
format = fmt.Sprintf("%s -> %s", c.Sprint(repoName), format)
258+
}
259+
fmt.Fprintf(a.out, format, args...)
260+
}
261+
262+
type progress struct {
263+
patchCount int64
264+
265+
totalSteps int64
266+
stepsComplete int64
267+
stepsFailed int64
268+
}
269+
270+
func (p *progress) SetTotalSteps(n int64) {
271+
atomic.StoreInt64(&p.totalSteps, n)
272+
}
273+
274+
func (p *progress) TotalSteps() int64 {
275+
return atomic.LoadInt64(&p.totalSteps)
276+
}
277+
278+
func (p *progress) StepsComplete() int64 {
279+
return atomic.LoadInt64(&p.stepsComplete)
280+
}
281+
282+
func (p *progress) IncStepsComplete(delta int64) {
283+
atomic.AddInt64(&p.stepsComplete, delta)
284+
}
285+
286+
func (p *progress) TotalStepsFailed() int64 {
287+
return atomic.LoadInt64(&p.stepsFailed)
288+
}
289+
290+
func (p *progress) IncStepsFailed() {
291+
atomic.AddInt64(&p.stepsFailed, 1)
292+
}
293+
294+
func (p *progress) PatchCount() int64 {
295+
return atomic.LoadInt64(&p.patchCount)
296+
}
297+
298+
func (p *progress) IncPatchCount() {
299+
atomic.AddInt64(&p.patchCount, 1)
300+
}
301+
302+
type progressWriter struct {
303+
p *progress
304+
305+
mu sync.Mutex
306+
w io.Writer
307+
shouldClear bool
308+
progressLogLength int
309+
}
310+
311+
func (w *progressWriter) Write(data []byte) (int, error) {
312+
w.mu.Lock()
313+
defer w.mu.Unlock()
314+
315+
if w.shouldClear {
316+
// Clear current progress
317+
fmt.Fprintf(w.w, "\r")
318+
fmt.Fprintf(w.w, strings.Repeat(" ", w.progressLogLength))
319+
fmt.Fprintf(w.w, "\r")
320+
}
321+
322+
if w.p.TotalSteps() == 0 {
323+
// Don't display bar until we know number of steps
324+
w.shouldClear = false
325+
return w.w.Write(data)
326+
}
327+
328+
if !bytes.HasSuffix(data, []byte("\n")) {
329+
w.shouldClear = false
330+
return w.w.Write(data)
331+
}
332+
333+
n, err := w.w.Write(data)
334+
if err != nil {
335+
return n, err
336+
}
337+
total := w.p.TotalSteps()
338+
done := w.p.StepsComplete()
339+
var pctDone float64
340+
if total > 0 {
341+
pctDone = float64(done) / float64(total)
342+
}
343+
344+
maxLength := 50
345+
bar := strings.Repeat("=", int(float64(maxLength)*pctDone))
346+
if len(bar) < maxLength {
347+
bar += ">"
348+
}
349+
bar += strings.Repeat(" ", maxLength-len(bar))
350+
progessText := fmt.Sprintf("[%s] Steps: %d/%d (%s, %s)", bar, w.p.StepsComplete(), w.p.TotalSteps(), boldRed.Sprintf("%d failed", w.p.TotalStepsFailed()), hiGreen.Sprintf("%d patches", w.p.PatchCount()))
351+
fmt.Fprintf(w.w, progessText)
352+
w.shouldClear = true
353+
w.progressLogLength = len(progessText)
354+
return n, err
355+
}

0 commit comments

Comments
 (0)