From 89a3dac882973437e06a806b2c10e6795601b613 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Sun, 14 May 2023 01:41:43 +0000 Subject: [PATCH] [tools] Extract unicode progress bar to library Change-Id: Ia0710777e7a133b3e335de917364858dd887618d Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/132640 Reviewed-by: Antonio Maiorano Kokoro: Kokoro Commit-Queue: Ben Clayton --- tools/src/cmd/run-cts/main.go | 85 +++------- tools/src/progressbar/progressbar.go | 222 +++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 60 deletions(-) create mode 100644 tools/src/progressbar/progressbar.go diff --git a/tools/src/cmd/run-cts/main.go b/tools/src/cmd/run-cts/main.go index 4762ea8be6..f31f846b3b 100644 --- a/tools/src/cmd/run-cts/main.go +++ b/tools/src/cmd/run-cts/main.go @@ -24,7 +24,6 @@ import ( "fmt" "io" "io/ioutil" - "math" "net/http" "os" "os/exec" @@ -42,6 +41,7 @@ import ( "dawn.googlesource.com/dawn/tools/src/cov" "dawn.googlesource.com/dawn/tools/src/fileutils" "dawn.googlesource.com/dawn/tools/src/git" + "dawn.googlesource.com/dawn/tools/src/progressbar" "github.com/mattn/go-colorable" "github.com/mattn/go-isatty" ) @@ -911,7 +911,7 @@ func (r *runner) streamResults(ctx context.Context, wg *sync.WaitGroup, results // Helper function for printing a progress bar. lastStatusUpdate, animFrame := time.Now(), 0 updateProgress := func() { - fmt.Fprint(r.stdout, ansiProgressBar(animFrame, numTests, numByExpectedStatus)) + drawProgressBar(r.stdout, animFrame, numTests, numByExpectedStatus) animFrame++ lastStatusUpdate = time.Now() } @@ -971,7 +971,7 @@ func (r *runner) streamResults(ctx context.Context, wg *sync.WaitGroup, results covTree.Add(SplitCTSQuery(res.testcase), res.coverage) } } - fmt.Fprint(r.stdout, ansiProgressBar(animFrame, numTests, numByExpectedStatus)) + drawProgressBar(r.stdout, animFrame, numTests, numByExpectedStatus) // All done. Print final stats. fmt.Fprintf(r.stdout, "\nCompleted in %v\n", timeTaken) @@ -1101,6 +1101,14 @@ var statusColor = map[status]string{ fail: red, } +var pbStatusColor = map[status]progressbar.Color{ + pass: progressbar.Green, + warn: progressbar.Yellow, + skip: progressbar.Cyan, + timeout: progressbar.Yellow, + fail: progressbar.Red, +} + // expectedStatus is a test status, along with a boolean to indicate whether the // status matches the test expectations type expectedStatus struct { @@ -1274,69 +1282,26 @@ func alignRight(val interface{}, width int) string { return strings.Repeat(" ", padding) + s } -// ansiProgressBar returns a string with an ANSI-colored progress bar, providing -// realtime information about the status of the CTS run. +// drawProgressBar draws an ANSI-colored progress bar, providing realtime +// information about the status of the CTS run. // Note: We'll want to skip this if !isatty or if we're running on windows. -func ansiProgressBar(animFrame int, numTests int, numByExpectedStatus map[expectedStatus]int) string { - const barWidth = 50 - - animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} - blockSymbols := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'} - - numBlocksPrinted := 0 - - buf := &strings.Builder{} - fmt.Fprint(buf, string(animSymbols[animFrame%len(animSymbols)]), " [") - animFrame++ - - numFinished := 0 - +func drawProgressBar(out io.Writer, animFrame int, numTests int, numByExpectedStatus map[expectedStatus]int) { + bar := progressbar.Status{Total: numTests} for _, status := range statuses { for _, expected := range []bool{true, false} { - color := statusColor[status] - if expected { - color += bold + if num := numByExpectedStatus[expectedStatus{status, expected}]; num > 0 { + bar.Segments = append(bar.Segments, + progressbar.Segment{ + Count: num, + Color: pbStatusColor[status], + Bold: expected, + Transparent: expected, + }) } - - num := numByExpectedStatus[expectedStatus{status, expected}] - numFinished += num - statusFrac := float64(num) / float64(numTests) - fNumBlocks := barWidth * statusFrac - fmt.Fprint(buf, color) - numBlocks := int(math.Ceil(fNumBlocks)) - if expected { - if numBlocks > 1 { - fmt.Fprint(buf, strings.Repeat(string("░"), numBlocks)) - } - } else { - if numBlocks > 1 { - fmt.Fprint(buf, strings.Repeat(string("▉"), numBlocks)) - } - if numBlocks > 0 { - frac := fNumBlocks - math.Floor(fNumBlocks) - symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))] - fmt.Fprint(buf, string(symbol)) - } - } - numBlocksPrinted += numBlocks } } - - if barWidth > numBlocksPrinted { - fmt.Fprint(buf, strings.Repeat(string(" "), barWidth-numBlocksPrinted)) - } - fmt.Fprint(buf, ansiReset) - fmt.Fprint(buf, "] ", percentage(numFinished, numTests)) - - if colors { - // move cursor to start of line so the bar is overridden - fmt.Fprint(buf, positionLeft) - } else { - // cannot move cursor, so newline - fmt.Fprintln(buf) - } - - return buf.String() + const width = 50 + bar.Draw(out, width, colors, animFrame) } // testcaseStatus is a pair of testcase name and result status diff --git a/tools/src/progressbar/progressbar.go b/tools/src/progressbar/progressbar.go new file mode 100644 index 0000000000..11c8f169c7 --- /dev/null +++ b/tools/src/progressbar/progressbar.go @@ -0,0 +1,222 @@ +// Copyright 2023 Google LLC +// +// 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 +// +// https://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 progressbar provides functions for drawing unicode progress bars to +// the terminal +package progressbar + +import ( + "bytes" + "fmt" + "io" + "math" + "strings" + "time" +) + +// Defaults for the Config +const ( + DefaultRefreshRate = time.Millisecond * 100 + DefaultWidth = 50 + DefaultANSIColors = true +) + +// Config holds configuration options for a ProgressBar +type Config struct { + RefreshRate time.Duration + Width int + ANSIColors bool +} + +// Color is an enumerator of colors +type Color int + +// Color enumerators +const ( + White Color = iota + Red + Green + Yellow + Blue + Magenta + Cyan +) + +// Segment describes a single segment of the ProgressBar +type Segment struct { + Count int + Color Color + Transparent bool + Bold bool +} + +// Status holds the updated data of the ProgressBar +type Status struct { + Total int + Segments []Segment +} + +// ProgressBar returns a string with an ANSI-colored progress bar, providing +// realtime information about the status of the CTS run. +// Note: We'll want to skip this if !isatty or if we're running on windows. +type ProgressBar struct { + Config + out io.Writer + c chan Status +} + +// New returns a new ProgressBar that streams output to out. +// Call ProgressBar.Stop() once finished. +func New(out io.Writer, cfg *Config) *ProgressBar { + p := &ProgressBar{out: out, c: make(chan Status)} + if cfg != nil { + p.Config = *cfg + } else { + p.ANSIColors = DefaultANSIColors + } + if p.RefreshRate == 0 { + p.RefreshRate = DefaultRefreshRate + } + if p.Width == 0 { + p.Width = DefaultWidth + } + go func() { + var status *Status + t := time.NewTicker(p.RefreshRate) + defer t.Stop() + for frame := 0; ; frame++ { + select { + case s, ok := <-p.c: + if !ok { + return + } + status = &s + case <-t.C: + if status != nil { + status.Draw(out, p.Width, p.ANSIColors, frame) + } + } + } + }() + return p +} + +// Update updates the ProgressBar with the given status +func (p *ProgressBar) Update(s Status) { + p.c <- s +} + +// Stop stops drawing the progress bar. +// Once called, the ProgressBar must not be used. +func (p *ProgressBar) Stop() { + close(p.c) +} + +// Draw draws the ProgressBar status to out +func (s Status) Draw(out io.Writer, width int, ansiColors bool, animFrame int) { + // ANSI escape sequences + const ( + escape = "\u001B[" + positionLeft = escape + "0G" + ansiReset = escape + "0m" + + bold = escape + "1m" + + red = escape + "31m" + green = escape + "32m" + yellow = escape + "33m" + blue = escape + "34m" + magenta = escape + "35m" + cyan = escape + "36m" + white = escape + "37m" + ) + + animSymbols := []rune{'⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'} + blockSymbols := []rune{'▏', '▎', '▍', '▌', '▋', '▊', '▉'} + + numBlocksPrinted := 0 + + buf := &bytes.Buffer{} + fmt.Fprint(buf, " ", string(animSymbols[animFrame%len(animSymbols)]), " [") + + numFinished := 0 + for _, seg := range s.Segments { + if ansiColors { + switch seg.Color { + case Red: + buf.WriteString(red) + case Green: + buf.WriteString(green) + case Yellow: + buf.WriteString(yellow) + case Blue: + buf.WriteString(blue) + case Magenta: + buf.WriteString(magenta) + case Cyan: + buf.WriteString(cyan) + default: + buf.WriteString(white) + } + if seg.Bold { + buf.WriteString(bold) + } + } + + numFinished += seg.Count + statusFrac := float64(seg.Count) / float64(s.Total) + fNumBlocks := float64(width) * statusFrac + numBlocks := int(math.Ceil(fNumBlocks)) + if seg.Transparent { + if numBlocks > 0 { + fmt.Fprint(buf, strings.Repeat(string("░"), numBlocks)) + } + } else { + if numBlocks > 1 { + fmt.Fprint(buf, strings.Repeat(string("▉"), numBlocks-1)) + } + if numBlocks > 0 { + frac := fNumBlocks - math.Floor(fNumBlocks) + symbol := blockSymbols[int(math.Round(frac*float64(len(blockSymbols)-1)))] + fmt.Fprint(buf, string(symbol)) + } + } + numBlocksPrinted += numBlocks + } + + if width > numBlocksPrinted { + fmt.Fprint(buf, strings.Repeat(string(" "), width-numBlocksPrinted)) + } + fmt.Fprint(buf, ansiReset) + fmt.Fprint(buf, "] ", percentage(numFinished, s.Total)) + + if ansiColors { + // move cursor to start of line so the bar is overridden next print + fmt.Fprint(buf, positionLeft) + } else { + // cannot move cursor, so newline + fmt.Fprintln(buf) + } + + out.Write(buf.Bytes()) +} + +// percentage returns the percentage of n out of total as a string +func percentage(n, total int) string { + if total == 0 { + return "-" + } + f := float64(n) / float64(total) + return fmt.Sprintf("%.1f%c", f*100.0, '%') +}