Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a04601f010 |
No files matched your search
@@ -756,21 +756,16 @@ Example shape (exact layout is flexible, content is not):
|
|||||||
|
|
||||||
Additional requirements:
|
Additional requirements:
|
||||||
|
|
||||||
- On a terminal, a phase with an exact total (every phase but `walk`, once it
|
- On a terminal, a phase with an exact total from its start (`hash`, `update`,
|
||||||
has counted what it will do) is drawn as its line followed by the progress-bar
|
and the content phase's reads) is drawn as the progress-bar library's bar,
|
||||||
library's bar, 10 columns wide, which shows the percent in place of the line's
|
with the phase and its place before it and the library's counts, rate, elapsed
|
||||||
own. Any other phase, and a phase whose bar has no room, is drawn as the
|
time and ETA after it. Any other phase (`load`, `walk`, and the content
|
||||||
library's spinner followed by its line. Each display is drawn again several
|
phase's checks) is drawn as the library's spinner followed by its line. Each
|
||||||
times a second, so its elapsed time keeps moving while a phase waits for its
|
display is drawn again several times a second, so its elapsed time keeps
|
||||||
next item.
|
moving while a phase waits for its next item.
|
||||||
- On a terminal no line reaches the terminal's last column, however narrow the
|
- On a terminal every line fits the terminal's width: the bar takes the room the
|
||||||
terminal is. The phase and its place, the progress, the elapsed time and the
|
rest of its line leaves, and a spinner's line is cut short where it would
|
||||||
ETA are kept whole: a line too wide for the terminal leaves out its rate first
|
reach the terminal's last column.
|
||||||
(or what the phase is waiting for), and a bar with no room beside those four
|
|
||||||
gives way to the spinner. Only when those four alone are too wide, as on a
|
|
||||||
terminal narrower than about 70 columns hours into a scan of millions of
|
|
||||||
files, is the line cut short at the terminal's edge, so the end of the ETA is
|
|
||||||
lost first.
|
|
||||||
- When stderr is not a terminal (a pipe, a file, `/dev/null`), do not emit ANSI
|
- When stderr is not a terminal (a pipe, a file, `/dev/null`), do not emit ANSI
|
||||||
redraws: print the line the moment each phase starts, every 5 seconds while it
|
redraws: print the line the moment each phase starts, every 5 seconds while it
|
||||||
runs (whether or not an item has completed, and never more often), and once
|
runs (whether or not an item has completed, and never more often), and once
|
||||||
|
|||||||
@@ -34,14 +34,11 @@
|
|||||||
time and ETA: `load` counts the records first, the walk's total is estimated
|
time and ETA: `load` counts the records first, the walk's total is estimated
|
||||||
from the records under the operands, and the content phase counts the records
|
from the records under the operands, and the content phase counts the records
|
||||||
it checks from SQLite's grouping alone, then says it is searching the database
|
it checks from SQLite's grouping alone, then says it is searching the database
|
||||||
until the first one is returned. On a terminal a phase with an exact total
|
until the first one is returned. On a terminal `hash`, `update` and the
|
||||||
draws its line followed by the library's bar, and any other a spinner followed
|
content phase's reads keep the library's bar and the other displays are a
|
||||||
by the line; every display is drawn again several times a second. No line
|
spinner followed by the line; every display is drawn again several times a
|
||||||
reaches the terminal's last column, however narrow: a line too wide leaves out
|
second, and every line fits the terminal's width. In plain mode the line is
|
||||||
its rate, then its bar, keeping the phase, progress, elapsed time and ETA
|
printed every 5 seconds whether or not an item completed (2026-10-16,
|
||||||
whole, and is cut short only when those alone do not fit, as below about 70
|
|
||||||
columns late in a large scan. In plain mode the line is printed every 5
|
|
||||||
seconds whether or not an item completed (2026-10-16,
|
|
||||||
https://git.eeqj.de/sneak/sfdupes/issues/112)
|
https://git.eeqj.de/sneak/sfdupes/issues/112)
|
||||||
|
|
||||||
- `scan`, `report` and `trees` first print a line on stderr naming the database
|
- `scan`, `report` and `trees` first print a line on stderr naming the database
|
||||||
|
|||||||
+102
-144
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -23,28 +22,19 @@ const redrawInterval = 100 * time.Millisecond
|
|||||||
// spinnerType selects the progressbar library's spinner style.
|
// spinnerType selects the progressbar library's spinner style.
|
||||||
const spinnerType = 14
|
const spinnerType = 14
|
||||||
|
|
||||||
// barWidth is the width of the progressbar library's bar between its
|
|
||||||
// two ends, as in the example line in README "Progress".
|
|
||||||
const barWidth = 10
|
|
||||||
|
|
||||||
// percentScale converts a fraction to a percentage.
|
// percentScale converts a fraction to a percentage.
|
||||||
const percentScale = 100
|
const percentScale = 100
|
||||||
|
|
||||||
// fallbackWidth is the width taken for a terminal whose width cannot be
|
// fallbackWidth is the width the progressbar library takes a terminal
|
||||||
// read, as the progressbar library takes it.
|
// to have when it cannot read it, as when stderr is not a terminal.
|
||||||
const fallbackWidth = 80
|
const fallbackWidth = 80
|
||||||
|
|
||||||
// spinnerMargin is the columns of a spinner's line besides its text:
|
// spinnerMargin is the columns of a spinner's line besides its text:
|
||||||
// the spinner and the three spaces the library draws around the text,
|
// the spinner and the three spaces the library draws around the text,
|
||||||
// and the terminal's last column, which every line stays clear of so
|
// and the terminal's last column, which the line stays clear of, as the
|
||||||
// that no terminal wraps it.
|
// library's bars do.
|
||||||
const spinnerMargin = 5
|
const spinnerMargin = 5
|
||||||
|
|
||||||
// barMargin is the columns of a bar's line besides its text: the
|
|
||||||
// percent and the bar the library draws after the text, the spaces
|
|
||||||
// around them, and the terminal's last column.
|
|
||||||
const barMargin = barWidth + 11
|
|
||||||
|
|
||||||
// stderrIsTTY reports whether stderr is attached to a terminal.
|
// stderrIsTTY reports whether stderr is attached to a terminal.
|
||||||
func stderrIsTTY() bool {
|
func stderrIsTTY() bool {
|
||||||
return term.IsTerminal(int(os.Stderr.Fd()))
|
return term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
@@ -77,32 +67,35 @@ type progressParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// progress renders one phase's progress on stderr. On a TTY the
|
// progress renders one phase's progress on stderr. On a TTY the
|
||||||
// progressbar library draws line, fitted to the terminal's width: after
|
// progressbar library draws it. A phase with an exact total from its
|
||||||
// the library's spinner, or, for a phase with an exact total, before the
|
// start gets the library's bar, with the phase and its place as the
|
||||||
// library's bar when the terminal has room for the bar beside it. It is
|
// bar's description and the library's counts, rate, elapsed time and
|
||||||
// drawn again every redrawInterval, so its elapsed time keeps moving
|
// ETA beside it; the bar takes the room the rest leaves on the line. Any
|
||||||
// while the phase waits for its next item. When stderr is not a TTY it
|
// other phase gets the library's spinner, with line as its text, cut to
|
||||||
// emits no ANSI redraws: it prints line as the phase starts, every
|
// the terminal's width. Either is drawn again every redrawInterval, so
|
||||||
// plainInterval while it runs, and as it ends.
|
// its elapsed time keeps moving while the phase waits for its next
|
||||||
|
// item. When stderr is not a TTY it emits no ANSI redraws: it prints
|
||||||
|
// line as the phase starts, every plainInterval while it runs, and as it
|
||||||
|
// ends.
|
||||||
//
|
//
|
||||||
// The draws come from a goroutine of their own, so mu guards the fields
|
// The draws come from a goroutine of their own, so mu guards the fields
|
||||||
// below it. Every method must be called from the main goroutine. A nil
|
// below it and every call on bar. Every method must be called from the
|
||||||
// *progress is a valid no-display receiver: every method is a no-op, so
|
// main goroutine. A nil *progress is a valid no-display receiver: every
|
||||||
// batched database flushes during the hash phase can reuse the update
|
// method is a no-op, so batched database flushes during the hash phase
|
||||||
// phase's helpers without rendering anything.
|
// can reuse the update phase's helpers without rendering anything.
|
||||||
type progress struct {
|
type progress struct {
|
||||||
width func() int // reads the terminal's width; nil when not a TTY
|
bar *progressbar.ProgressBar // nil when stderr is not a TTY
|
||||||
|
spinner bool // bar is a spinner, with line as its text
|
||||||
stop chan struct{} // closed by finish to end the draws
|
stop chan struct{} // closed by finish to end the draws
|
||||||
done chan struct{} // closed once the draws have ended
|
done chan struct{} // closed once the draws have ended
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
bar *progressbar.ProgressBar // what the library draws; nil until then
|
|
||||||
withBar bool // bar is the library's bar, not its spinner
|
|
||||||
label string
|
label string
|
||||||
total int64
|
total int64
|
||||||
estimate bool
|
estimate bool
|
||||||
waitingFor string
|
waitingFor string
|
||||||
count int64
|
count int64
|
||||||
|
drawn int64 // the items bar has been given, unless a spinner
|
||||||
start time.Time // when the phase started, for the elapsed time
|
start time.Time // when the phase started, for the elapsed time
|
||||||
rateStart time.Time // when the count started, for the rate
|
rateStart time.Time // when the count started, for the rate
|
||||||
}
|
}
|
||||||
@@ -110,26 +103,33 @@ type progress struct {
|
|||||||
// newProgress starts the display of one phase on stderr.
|
// newProgress starts the display of one phase on stderr.
|
||||||
func newProgress(params progressParams) *progress {
|
func newProgress(params progressParams) *progress {
|
||||||
if stderrIsTTY() {
|
if stderrIsTTY() {
|
||||||
return startProgress(params, terminalWidth, redrawInterval)
|
return startProgress(params, true, redrawInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
return startProgress(params, nil, plainInterval)
|
return startProgress(params, false, plainInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProgress shows the phase params describes at once, and again
|
// startProgress shows the phase params describes at once, with the
|
||||||
// every interval until finish: with the progressbar library on a
|
// progressbar library when onTTY is set and as a plain line when not,
|
||||||
// terminal, whose width each draw reads by calling width, or as a plain
|
// and again every interval until finish.
|
||||||
// line when width is nil.
|
func startProgress(params progressParams, onTTY bool,
|
||||||
func startProgress(params progressParams, width func() int,
|
|
||||||
interval time.Duration,
|
interval time.Duration,
|
||||||
) *progress {
|
) *progress {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
p := &progress{
|
p := &progress{
|
||||||
width: width, stop: make(chan struct{}), done: make(chan struct{}),
|
stop: make(chan struct{}), done: make(chan struct{}),
|
||||||
label: params.label, total: params.total, estimate: params.estimate,
|
label: params.label, total: params.total, estimate: params.estimate,
|
||||||
waitingFor: params.waitingFor, start: now, rateStart: now,
|
waitingFor: params.waitingFor, start: now, rateStart: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case onTTY && (params.total < 0 || params.estimate):
|
||||||
|
// The library's bar needs an exact total.
|
||||||
|
p.bar, p.spinner = newSpinner(), true
|
||||||
|
case onTTY:
|
||||||
|
p.bar = newBar(params.label, params.total)
|
||||||
|
}
|
||||||
|
|
||||||
// Show the zero state at once: the first item may take minutes,
|
// Show the zero state at once: the first item may take minutes,
|
||||||
// and a phase must never look hung.
|
// and a phase must never look hung.
|
||||||
p.draw()
|
p.draw()
|
||||||
@@ -153,74 +153,65 @@ func startProgress(params progressParams, width func() int,
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newBar builds the library's bar for a phase with an exact total, with
|
||||||
|
// label as its description and the library's counts, rate, elapsed time
|
||||||
|
// and ETA beside it. The bar is as wide as the rest of the line leaves
|
||||||
|
// room for on the terminal.
|
||||||
|
func newBar(label string, total int64) *progressbar.ProgressBar {
|
||||||
|
return progressbar.NewOptions64(total,
|
||||||
|
progressbar.OptionSetWriter(os.Stderr),
|
||||||
|
progressbar.OptionSetDescription(label),
|
||||||
|
progressbar.OptionShowCount(),
|
||||||
|
progressbar.OptionShowIts(),
|
||||||
|
progressbar.OptionSetItsString("files"),
|
||||||
|
progressbar.OptionSetElapsedTime(true),
|
||||||
|
progressbar.OptionSetPredictTime(true),
|
||||||
|
progressbar.OptionShowElapsedTimeOnFinish(),
|
||||||
|
progressbar.OptionFullWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSpinner builds the library's spinner for a phase without an exact
|
||||||
|
// total, followed by the text draw gives it.
|
||||||
|
func newSpinner() *progressbar.ProgressBar {
|
||||||
|
return progressbar.NewOptions64(-1,
|
||||||
|
progressbar.OptionSetWriter(os.Stderr),
|
||||||
|
progressbar.OptionSpinnerType(spinnerType),
|
||||||
|
// The spinner turns at each draw, not on a timer of the
|
||||||
|
// library's own.
|
||||||
|
progressbar.OptionSetSpinnerChangeInterval(0),
|
||||||
|
// The text carries the elapsed time.
|
||||||
|
progressbar.OptionSetElapsedTime(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// draw shows the phase's progress: on a TTY in place of the line
|
// draw shows the phase's progress: on a TTY in place of the line
|
||||||
// before, and otherwise as a plain line.
|
// before, and otherwise as a plain line.
|
||||||
func (p *progress) draw() {
|
func (p *progress) draw() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
if p.width == nil {
|
switch {
|
||||||
text, _ := p.line(math.MaxInt, true)
|
case p.bar == nil:
|
||||||
fmt.Fprintln(os.Stderr, text)
|
fmt.Fprintln(os.Stderr, p.line())
|
||||||
|
case p.spinner:
|
||||||
return
|
text := p.line()
|
||||||
}
|
if room := terminalWidth() - spinnerMargin; len(text) > room {
|
||||||
|
text = text[:max(room, 0)]
|
||||||
width := p.width()
|
|
||||||
|
|
||||||
// A phase with an exact total gets the library's bar when its line,
|
|
||||||
// without the percent the bar shows, fits whole beside the bar. A
|
|
||||||
// total of 0 leaves the bar nothing to fill.
|
|
||||||
text, withBar := "", false
|
|
||||||
if p.total > 0 && !p.estimate {
|
|
||||||
text, withBar = p.line(width-barMargin, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !withBar {
|
|
||||||
text, _ = p.line(width-spinnerMargin, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The library makes a bar or a spinner. A display that turns from
|
|
||||||
// one to the other, as its line grows or the terminal is resized,
|
|
||||||
// gets a new one.
|
|
||||||
if p.bar == nil || p.withBar != withBar {
|
|
||||||
p.newBar(withBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
if withBar {
|
|
||||||
_ = p.bar.Set64(p.count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.bar.Describe(text)
|
p.bar.Describe(text)
|
||||||
|
case p.count > p.drawn:
|
||||||
|
// The library measures its rate over the items it is given,
|
||||||
|
// so it is given items only when there are new ones: an
|
||||||
|
// empty add would count a wait as a rate of zero.
|
||||||
|
_ = p.bar.Add64(p.count - p.drawn)
|
||||||
|
p.drawn = p.count
|
||||||
|
default:
|
||||||
|
// No new items: the bar is drawn again all the same, with
|
||||||
|
// its elapsed time as of now.
|
||||||
|
p.bar.Describe(p.label)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newBar has the library make what draw draws: a bar for the phase's
|
|
||||||
// total when withBar is set, and a spinner when not. Either shows the
|
|
||||||
// text draw gives it, and nothing of its own besides the spinner, or
|
|
||||||
// the bar and its percent. p.mu must be held.
|
|
||||||
func (p *progress) newBar(withBar bool) {
|
|
||||||
if p.bar != nil {
|
|
||||||
// Erase what was drawn before.
|
|
||||||
_ = p.bar.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
total := int64(-1) // the library's spinner
|
|
||||||
if withBar {
|
|
||||||
total = p.total
|
|
||||||
}
|
|
||||||
|
|
||||||
p.bar = progressbar.NewOptions64(total,
|
|
||||||
progressbar.OptionSetWriter(os.Stderr),
|
|
||||||
progressbar.OptionSetWidth(barWidth),
|
|
||||||
progressbar.OptionSpinnerType(spinnerType),
|
|
||||||
// The spinner turns at each draw, not on a timer of the
|
|
||||||
// library's own.
|
|
||||||
progressbar.OptionSetSpinnerChangeInterval(0),
|
|
||||||
// The text carries the elapsed time and the ETA.
|
|
||||||
progressbar.OptionSetElapsedTime(false),
|
|
||||||
progressbar.OptionSetPredictTime(false),
|
|
||||||
)
|
|
||||||
p.withBar = withBar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTotal gives a phase that started without a total its total, once
|
// setTotal gives a phase that started without a total its total, once
|
||||||
@@ -286,84 +277,51 @@ func (p *progress) finish() {
|
|||||||
|
|
||||||
p.draw()
|
p.draw()
|
||||||
|
|
||||||
if p.width != nil {
|
if p.bar != nil {
|
||||||
// The display stays on the TTY as drawn; what follows starts
|
// The display stays on the TTY as drawn; what follows starts
|
||||||
// on a line of its own.
|
// on a line of its own.
|
||||||
fmt.Fprintln(os.Stderr)
|
fmt.Fprintln(os.Stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// line formats the phase's progress in at most room columns, as
|
// line formats the phase's progress, as
|
||||||
//
|
//
|
||||||
// [3/5] hash: [1234/5000] 25%, 300 files/s, elapsed 4s, eta 13s
|
// [3/5] hash: [1234/5000] 25%, 300 files/s, elapsed 4s, eta 13s
|
||||||
//
|
//
|
||||||
// without the percent when percent is false, as before the library's
|
// with "~" before a total that is an estimate. A phase with no total
|
||||||
// bar, which shows its own. A phase with no total shows the items done
|
// shows the items done instead, or what it waits for. p.mu must be
|
||||||
// instead, or what it waits for.
|
// held.
|
||||||
//
|
func (p *progress) line() string {
|
||||||
// The phase and its place, the progress, the elapsed time and the ETA
|
|
||||||
// are kept whole. A line wider than room leaves out the rate, or what
|
|
||||||
// the phase waits for; only if it is still wider is it cut at room, and
|
|
||||||
// line reports that it does not fit. Every part is ASCII, so its length
|
|
||||||
// is its width in columns. p.mu must be held.
|
|
||||||
func (p *progress) line(room int, percent bool) (string, bool) {
|
|
||||||
elapsed := time.Since(p.start).Round(time.Second)
|
elapsed := time.Since(p.start).Round(time.Second)
|
||||||
|
|
||||||
// head and tail hold the parts kept whole; extra, between them, is
|
|
||||||
// the part left out.
|
|
||||||
var head, extra, tail string
|
|
||||||
|
|
||||||
if p.total < 0 && p.waitingFor != "" {
|
if p.total < 0 && p.waitingFor != "" {
|
||||||
head, extra = p.label+": ", p.waitingFor
|
return fmt.Sprintf("%s: %s, elapsed %s, eta unknown",
|
||||||
tail = fmt.Sprintf("elapsed %s, eta unknown", elapsed)
|
p.label, p.waitingFor, elapsed)
|
||||||
} else {
|
}
|
||||||
|
|
||||||
rate := 0.0
|
rate := 0.0
|
||||||
if s := time.Since(p.rateStart).Seconds(); s > 0 {
|
if s := time.Since(p.rateStart).Seconds(); s > 0 {
|
||||||
rate = float64(p.count) / s
|
rate = float64(p.count) / s
|
||||||
}
|
}
|
||||||
|
|
||||||
head = p.label + ": " + p.itemsDone(percent) + ", "
|
done := fmt.Sprintf("%d files", p.count)
|
||||||
extra = fmt.Sprintf("%.0f files/s", rate)
|
|
||||||
tail = fmt.Sprintf("elapsed %s, eta %s", elapsed, p.eta(rate))
|
|
||||||
}
|
|
||||||
|
|
||||||
full, kept := head+extra+", "+tail, head+tail
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(full) <= room:
|
|
||||||
return full, true
|
|
||||||
case len(kept) <= room:
|
|
||||||
return kept, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return kept[:max(room, 0)], false
|
|
||||||
}
|
|
||||||
|
|
||||||
// itemsDone formats the items done, as "[1234/5000] 25%", with "~"
|
|
||||||
// before a total that is an estimate and without the percent when
|
|
||||||
// percent is false, or as "1234 files" when the phase has no total.
|
|
||||||
// p.mu must be held.
|
|
||||||
func (p *progress) itemsDone(percent bool) string {
|
|
||||||
if p.total < 0 {
|
|
||||||
return fmt.Sprintf("%d files", p.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if p.total >= 0 {
|
||||||
approx := ""
|
approx := ""
|
||||||
if p.estimate {
|
if p.estimate {
|
||||||
approx = "~"
|
approx = "~"
|
||||||
}
|
}
|
||||||
|
|
||||||
done := fmt.Sprintf("[%d/%s%d]", p.count, approx, p.total)
|
|
||||||
if !percent {
|
|
||||||
return done
|
|
||||||
}
|
|
||||||
|
|
||||||
pct := float64(percentScale)
|
pct := float64(percentScale)
|
||||||
if p.total > 0 {
|
if p.total > 0 {
|
||||||
pct = percentScale * float64(p.count) / float64(p.total)
|
pct = percentScale * float64(p.count) / float64(p.total)
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %.0f%%", done, pct)
|
done = fmt.Sprintf("[%d/%s%d] %.0f%%", p.count, approx, p.total, pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s: %s, %.0f files/s, elapsed %s, eta %s",
|
||||||
|
p.label, done, rate, elapsed, p.eta(rate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// eta returns the time the phase has left at rate items a second, or
|
// eta returns the time the phase has left at rate items a second, or
|
||||||
|
|||||||
+56
-163
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -115,66 +114,12 @@ func TestProgressLine(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc.p.start, tc.p.rateStart = ago, ago
|
tc.p.start, tc.p.rateStart = ago, ago
|
||||||
|
|
||||||
if got, _ := tc.p.line(math.MaxInt, true); got != tc.want {
|
if got := tc.p.line(); got != tc.want {
|
||||||
t.Errorf("line = %q, want %q", got, tc.want)
|
t.Errorf("line = %q, want %q", got, tc.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLineFitsRoom checks how a line is fitted to the room it has, ten
|
|
||||||
// seconds into the phase: it leaves out the rate, or what the phase
|
|
||||||
// waits for, and is cut only when what is left is still too wide.
|
|
||||||
func TestLineFitsRoom(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ago := time.Now().Add(-10 * time.Second)
|
|
||||||
hash := &progress{
|
|
||||||
label: scanPhase("hash"), total: 100, count: 20,
|
|
||||||
start: ago, rateStart: ago,
|
|
||||||
}
|
|
||||||
content := &progress{
|
|
||||||
label: scanPhase("content"), total: -1,
|
|
||||||
waitingFor: "searching the database", start: ago, rateStart: ago,
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
p *progress
|
|
||||||
room int
|
|
||||||
percent bool
|
|
||||||
want string
|
|
||||||
fits bool
|
|
||||||
}{{
|
|
||||||
p: hash, room: 57, percent: true,
|
|
||||||
want: "[3/5] hash: [20/100] 20%, 2 files/s, elapsed 10s, eta 40s",
|
|
||||||
fits: true,
|
|
||||||
}, {
|
|
||||||
p: hash, room: 56, percent: true,
|
|
||||||
want: "[3/5] hash: [20/100] 20%, elapsed 10s, eta 40s",
|
|
||||||
fits: true,
|
|
||||||
}, {
|
|
||||||
p: hash, room: 45, percent: true,
|
|
||||||
want: "[3/5] hash: [20/100] 20%, elapsed 10s, eta 40",
|
|
||||||
fits: false,
|
|
||||||
}, {
|
|
||||||
// Before the library's bar, which shows the percent.
|
|
||||||
p: hash, room: 57, percent: false,
|
|
||||||
want: "[3/5] hash: [20/100], 2 files/s, elapsed 10s, eta 40s",
|
|
||||||
fits: true,
|
|
||||||
}, {
|
|
||||||
p: content, room: 40, percent: true,
|
|
||||||
want: "[5/5] content: elapsed 10s, eta unknown",
|
|
||||||
fits: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
got, fits := tc.p.line(tc.room, tc.percent)
|
|
||||||
if got != tc.want || fits != tc.fits {
|
|
||||||
t.Errorf("line(%d, %t) = %q, %t, want %q, %t",
|
|
||||||
tc.room, tc.percent, got, fits, tc.want, tc.fits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSetTotalStartsRate checks that setTotal starts a phase's rate,
|
// TestSetTotalStartsRate checks that setTotal starts a phase's rate,
|
||||||
// and so its ETA, from when the total becomes known, not from when the
|
// and so its ETA, from when the total becomes known, not from when the
|
||||||
// phase started waiting for it.
|
// phase started waiting for it.
|
||||||
@@ -195,7 +140,7 @@ func TestSetTotalStartsRate(t *testing.T) {
|
|||||||
p.count = 20
|
p.count = 20
|
||||||
|
|
||||||
want := "[1/5] load: [20/100] 20%, 2 files/s, elapsed 30s, eta 40s"
|
want := "[1/5] load: [20/100] 20%, 2 files/s, elapsed 30s, eta 40s"
|
||||||
if got, _ := p.line(math.MaxInt, true); got != want {
|
if got := p.line(); got != want {
|
||||||
t.Errorf("line = %q, want %q", got, want)
|
t.Errorf("line = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,7 +198,7 @@ func TestPlainProgressPrintsWhileWaiting(t *testing.T) {
|
|||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: scanPhase("load"), total: -1,
|
label: scanPhase("load"), total: -1,
|
||||||
waitingFor: "counting the records",
|
waitingFor: "counting the records",
|
||||||
}, nil, testRedraw)
|
}, false, testRedraw)
|
||||||
|
|
||||||
awaitOutput(t, stderr,
|
awaitOutput(t, stderr,
|
||||||
"[1/5] load: counting the records, elapsed 0s, eta unknown\n", 3)
|
"[1/5] load: counting the records, elapsed 0s, eta unknown\n", 3)
|
||||||
@@ -276,136 +221,84 @@ func lastFrame(out string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestTerminalRedrawsWhileWaiting checks that the terminal display of a
|
// TestTerminalRedrawsWhileWaiting checks that the terminal display of a
|
||||||
// phase, spinner or bar, keeps being drawn while the phase waits for its
|
// phase keeps being drawn while the phase waits for its next item, and
|
||||||
// next item, with the elapsed time as of now, and shows every item
|
// shows every item completed before the wait: a spinner's line with
|
||||||
// completed before the wait. stderr is not a terminal here, so the
|
// the elapsed time as of now, and the library's bar, which works out
|
||||||
// displays take it to be fallbackWidth wide.
|
// its elapsed time at each draw.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
func TestTerminalRedrawsWhileWaiting(t *testing.T) {
|
func TestTerminalRedrawsWhileWaiting(t *testing.T) {
|
||||||
stderr := captureStderr(t)
|
stderr := captureStderr(t)
|
||||||
|
|
||||||
cases := []struct {
|
walk := startProgress(progressParams{label: scanPhase("walk"), total: -1},
|
||||||
params progressParams
|
true, testRedraw)
|
||||||
want string // how the line shows a count of 50
|
|
||||||
}{
|
|
||||||
{progressParams{label: scanPhase("walk"), total: -1}, "50 files,"},
|
|
||||||
{progressParams{label: scanPhase("hash"), total: 100}, "[50/100],"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
before := len(stderr())
|
|
||||||
since := func() string { return stderr()[before:] }
|
|
||||||
p := startProgress(tc.params, terminalWidth, testRedraw)
|
|
||||||
|
|
||||||
for range 50 {
|
for range 50 {
|
||||||
p.increment()
|
walk.increment()
|
||||||
}
|
}
|
||||||
|
|
||||||
// An hour into the phase, without waiting an hour.
|
// An hour into the phase, without waiting an hour.
|
||||||
p.mu.Lock()
|
walk.mu.Lock()
|
||||||
p.start = p.start.Add(-time.Hour)
|
walk.start = walk.start.Add(-time.Hour)
|
||||||
p.mu.Unlock()
|
walk.mu.Unlock()
|
||||||
|
|
||||||
awaitOutput(t, since, "elapsed 1h0m0s", 1)
|
awaitOutput(t, stderr, "elapsed 1h0m0s", 1)
|
||||||
p.finish()
|
walk.finish()
|
||||||
|
|
||||||
if shown := lastFrame(since()); !strings.Contains(shown, tc.want) {
|
if shown := lastFrame(stderr()); !strings.Contains(shown, "50 files,") {
|
||||||
t.Errorf("terminal shows %q, want %q", shown, tc.want)
|
t.Errorf("terminal shows %q, want a count of 50", shown)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawLate draws the display params describes on a terminal width
|
hash := startProgress(progressParams{label: scanPhase("hash"), total: 100},
|
||||||
// columns wide as it starts and once more as it ends, with count items
|
true, testRedraw)
|
||||||
// done two and a half hours into the phase, and returns what it wrote
|
|
||||||
// to stderr.
|
|
||||||
func drawLate(stderr func() string, params progressParams, width int,
|
|
||||||
count int64,
|
|
||||||
) string {
|
|
||||||
before := len(stderr())
|
|
||||||
|
|
||||||
// No timed draws come between the two.
|
for range 50 {
|
||||||
p := startProgress(params, func() int { return width }, time.Hour)
|
hash.increment()
|
||||||
|
|
||||||
// Without counting the items one by one or waiting the hours.
|
|
||||||
p.mu.Lock()
|
|
||||||
p.count = count
|
|
||||||
p.start = p.start.Add(-150 * time.Minute)
|
|
||||||
p.rateStart = p.start
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
p.finish()
|
|
||||||
|
|
||||||
return stderr()[before:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTerminalLinesKeepETA checks the line each display shows on an
|
// Drawn with the 50 items, then twice more with none.
|
||||||
// 80-column terminal at a large scan's counts and times: the rate is
|
awaitOutput(t, stderr, "(50/100,", 3)
|
||||||
// left out, so that the elapsed time and the ETA stay whole.
|
hash.finish()
|
||||||
//
|
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
|
||||||
func TestTerminalLinesKeepETA(t *testing.T) {
|
|
||||||
stderr := captureStderr(t)
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
params progressParams
|
|
||||||
want string
|
|
||||||
}{{
|
|
||||||
// Past its estimate, the walk cannot tell how long it has left.
|
|
||||||
params: progressParams{
|
|
||||||
label: scanPhase("walk"), total: 1200000, estimate: true,
|
|
||||||
},
|
|
||||||
want: "[2/5] walk: [1234567/~1200000] 103%, elapsed 2h30m0s, " +
|
|
||||||
"eta unknown",
|
|
||||||
}, {
|
|
||||||
params: progressParams{label: scanPhase("hash"), total: 9876543},
|
|
||||||
want: "[3/5] hash: [1234567/9876543] 12%, elapsed 2h30m0s, " +
|
|
||||||
"eta 17h30m0s",
|
|
||||||
}, {
|
|
||||||
params: progressParams{label: scanPhase("content"), total: 9876543},
|
|
||||||
want: "[5/5] content: [1234567/9876543] 12%, elapsed 2h30m0s, " +
|
|
||||||
"eta 17h30m0s",
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
out := drawLate(stderr, tc.params, 80, 1234567)
|
|
||||||
if shown := lastFrame(out); !strings.Contains(shown, tc.want) {
|
|
||||||
t.Errorf("terminal shows %q, want %q", shown, tc.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestTerminalLinesFitWidth draws a large scan's bar display and a small
|
// TestTerminalLinesFitWidth checks that every line a terminal display
|
||||||
// scan's, two and a half hours into the phase, on terminals from 120
|
// draws, bar or spinner, is narrower than the terminal at the size of a
|
||||||
// columns down to 40, and checks that every line stays clear of the
|
// large scan. A line as wide as the terminal or wider would wrap, and
|
||||||
// terminal's last column. A line that reaches it wraps, and every
|
// every redraw would leave a line behind. stderr is not a terminal
|
||||||
// redraw then leaves a line behind. The bar is drawn where it has room,
|
// here, so the displays take it to be fallbackWidth wide.
|
||||||
// as at 120 columns, and left out where it has none.
|
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
func TestTerminalLinesFitWidth(t *testing.T) {
|
func TestTerminalLinesFitWidth(t *testing.T) {
|
||||||
stderr := captureStderr(t)
|
stderr := captureStderr(t)
|
||||||
|
|
||||||
for _, width := range []int{120, 100, 80, 72, 60, 50, 40} {
|
cases := []progressParams{
|
||||||
for _, total := range []int64{9876543, 20000} {
|
{label: scanPhase("content"), total: 9876543},
|
||||||
params := progressParams{label: scanPhase("content"), total: total}
|
{label: scanPhase("walk"), total: 9876543, estimate: true},
|
||||||
out := drawLate(stderr, params, width, total/8)
|
|
||||||
|
|
||||||
if width == 120 && !strings.Contains(out, "% |") {
|
|
||||||
t.Errorf("no bar on a terminal %d wide: %q", width, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.FieldsFuncSeq(out, func(r rune) bool {
|
for _, params := range cases {
|
||||||
|
p := startProgress(params, true, testRedraw)
|
||||||
|
|
||||||
|
// 1234567 items two and a half hours into the phase, without
|
||||||
|
// counting them one by one or waiting the hours.
|
||||||
|
p.mu.Lock()
|
||||||
|
p.count = 1234567
|
||||||
|
p.start = p.start.Add(-150 * time.Minute)
|
||||||
|
p.rateStart = p.start
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
awaitOutput(t, stderr, "1234567", 1)
|
||||||
|
p.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.FieldsFuncSeq(stderr(), func(r rune) bool {
|
||||||
return r == '\r' || r == '\n'
|
return r == '\r' || r == '\n'
|
||||||
})
|
})
|
||||||
|
|
||||||
for line := range lines {
|
for line := range lines {
|
||||||
if n := utf8.RuneCountInString(line); n >= width {
|
if n := utf8.RuneCountInString(line); n >= fallbackWidth {
|
||||||
t.Errorf("a line of %d columns on a terminal %d wide: %q",
|
t.Errorf("a line of %d columns on a terminal %d wide: %q",
|
||||||
n, width, line)
|
n, fallbackWidth, line)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,7 +320,7 @@ func TestProgressWarningsOnOwnLines(t *testing.T) {
|
|||||||
issued := 0
|
issued := 0
|
||||||
|
|
||||||
for _, params := range cases {
|
for _, params := range cases {
|
||||||
p := startProgress(params, terminalWidth, testRedraw)
|
p := startProgress(params, true, testRedraw)
|
||||||
|
|
||||||
// No pause between warnings: one written straight to stderr is
|
// No pause between warnings: one written straight to stderr is
|
||||||
// garbled only if a redraw lands while it is being written.
|
// garbled only if a redraw lands while it is being written.
|
||||||
@@ -467,13 +360,13 @@ func TestProgressWarningsOnOwnLines(t *testing.T) {
|
|||||||
func TestTerminalStoppedShortKeepsCount(t *testing.T) {
|
func TestTerminalStoppedShortKeepsCount(t *testing.T) {
|
||||||
stderr := captureStderr(t)
|
stderr := captureStderr(t)
|
||||||
p := startProgress(progressParams{label: scanPhase("hash"), total: 10},
|
p := startProgress(progressParams{label: scanPhase("hash"), total: 10},
|
||||||
terminalWidth, redrawInterval)
|
true, redrawInterval)
|
||||||
|
|
||||||
p.increment()
|
p.increment()
|
||||||
p.increment()
|
p.increment()
|
||||||
p.finish()
|
p.finish()
|
||||||
|
|
||||||
if shown := lastFrame(stderr()); !strings.Contains(shown, "[2/10],") {
|
if shown := lastFrame(stderr()); !strings.Contains(shown, "(2/10,") {
|
||||||
t.Errorf("terminal shows %q, want a count of 2 of 10", shown)
|
t.Errorf("terminal shows %q, want a count of 2 of 10", shown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,16 +379,16 @@ func TestTerminalStoppedShortKeepsCount(t *testing.T) {
|
|||||||
func TestWarningAfterLastItemKeepsBar(t *testing.T) {
|
func TestWarningAfterLastItemKeepsBar(t *testing.T) {
|
||||||
stderr := captureStderr(t)
|
stderr := captureStderr(t)
|
||||||
p := startProgress(progressParams{label: scanPhase("hash"), total: 2},
|
p := startProgress(progressParams{label: scanPhase("hash"), total: 2},
|
||||||
terminalWidth, testRedraw)
|
true, testRedraw)
|
||||||
|
|
||||||
p.increment()
|
p.increment()
|
||||||
p.increment()
|
p.increment()
|
||||||
|
|
||||||
awaitOutput(t, stderr, "[2/2],", 1)
|
awaitOutput(t, stderr, "(2/2,", 1)
|
||||||
p.warnf("warning")
|
p.warnf("warning")
|
||||||
p.finish()
|
p.finish()
|
||||||
|
|
||||||
if shown := lastFrame(stderr()); !strings.Contains(shown, "[2/2],") {
|
if shown := lastFrame(stderr()); !strings.Contains(shown, "(2/2,") {
|
||||||
t.Errorf("terminal shows %q, want a count of 2 of 2", shown)
|
t.Errorf("terminal shows %q, want a count of 2 of 2", shown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user