Show the progress rate over the last 5 seconds, and what a phase is doing after 10 seconds with no item done (closes #125)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The rate was the phase's average, so a burst after a long wait read as over 1,000 files a second. It now covers the items completed over the last 5 seconds, or since the line before off a terminal, and falls to 0 while nothing completes; the ETA still comes from the average. Once no item has completed for 10 seconds, the line says so and what the phase is doing, a phrase each phase gives where it is defined. A narrow terminal leaves that out after the rate, keeping the phase, progress, elapsed time and ETA whole. The tests set the display's clock by hand. Judgement call: the line reads "no files finished", in the phase's unit. Judgement call: trees' group phase says "grouping directories". Model: opus-5-5
This commit is contained in:
@@ -833,17 +833,33 @@ Each display is one line holding:
|
|||||||
- a `[m/n] x%` display (items processed / total items, percent); before a phase
|
- a `[m/n] x%` display (items processed / total items, percent); before a phase
|
||||||
has a total, the items processed, or what the phase is waiting for
|
has a total, the items processed, or what the phase is waiting for
|
||||||
- current rate (items/s), naming the items the phase counts: files, records,
|
- current rate (items/s), naming the items the phase counts: files, records,
|
||||||
directories or groups
|
directories or groups. It covers the items completed over the last 5 seconds,
|
||||||
|
or, when stderr is not a terminal, since the line before, so it falls to 0
|
||||||
|
while nothing completes, and a burst of items after a wait shows as a burst
|
||||||
|
rather than as a steady rate.
|
||||||
|
- once no item has completed for 10 seconds, how long that has been and what the
|
||||||
|
phase is doing (see below)
|
||||||
- elapsed time
|
- elapsed time
|
||||||
- estimated time remaining, or `unknown`
|
- estimated time remaining, or `unknown`. It comes from the phase's average rate
|
||||||
|
since it started counting, so that it does not swing with each burst.
|
||||||
|
|
||||||
Example shape (exact layout is flexible, content is not):
|
Example shape (exact layout is flexible, content is not):
|
||||||
|
|
||||||
```
|
```
|
||||||
[3/5] hash: [12345/98765] 12% |████ | 92 files/s elapsed 2:32 eta 17:54
|
[3/5] hash: [12345/98765] 12% |████ | 92 files/s elapsed 2:32 eta 17:54
|
||||||
[2/5] walk: [1234567/~1200000] 103%, 5120 files/s, elapsed 4m1s, eta unknown
|
[2/5] walk: [1234567/~1200000] 103%, 5120 files/s, elapsed 4m1s, eta unknown
|
||||||
|
[3/5] hash: [12345/98765] 12%, 0 files/s, no files finished for 42s, reading files, elapsed 4m1s, eta 28m7s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When no item has completed for 10 seconds, as while one large file is read, a
|
||||||
|
disk is slow or the database is busy, the line says so and what the phase is
|
||||||
|
doing: `walk` is listing directories; `hash` and the content phase's reads are
|
||||||
|
reading files; `update` is writing to the database; `load` and the `read` phases
|
||||||
|
of `trees` and `report` are reading the database; the content phase's checks are
|
||||||
|
searching the database; `trees`' `group` is grouping directories, and its
|
||||||
|
`write` is writing the report. A phase still waiting for its total already says
|
||||||
|
what it waits for, and says nothing more.
|
||||||
|
|
||||||
Additional requirements:
|
Additional requirements:
|
||||||
|
|
||||||
- On a terminal, a phase with an exact total (every phase but `scan`'s `walk`,
|
- On a terminal, a phase with an exact total (every phase but `scan`'s `walk`,
|
||||||
@@ -857,13 +873,14 @@ Additional requirements:
|
|||||||
- On a terminal no line reaches the terminal's last column, however narrow the
|
- On a terminal no line reaches the terminal's last column, however narrow the
|
||||||
terminal is. The phase and its place, the progress, the elapsed time and the
|
terminal is. The phase and its place, the progress, the elapsed time and the
|
||||||
ETA are kept whole: a line too wide for the terminal leaves out its rate first
|
ETA are kept whole: a line too wide for the terminal leaves out its rate first
|
||||||
(or what the phase is waiting for), and a bar with no room beside those four
|
(or what the phase is waiting for), then what it says of 10 seconds with no
|
||||||
gives way to the spinner. Only when those four alone are too wide, as on a
|
item completed, and a bar with no room beside those four gives way to the
|
||||||
terminal narrower than about 70 columns hours into a scan of millions of
|
spinner. Only when those four alone are too wide, as on a terminal narrower
|
||||||
files, is the line cut short at the terminal's edge, so the end of the ETA is
|
than about 70 columns hours into a scan of millions of files, is the line cut
|
||||||
lost first. A terminal made narrower while a phase runs gets the same: the
|
short at the terminal's edge, so the end of the ETA is lost first. A terminal
|
||||||
next draw erases the line within the new width and fits the line to it, and
|
made narrower while a phase runs gets the same: the next draw erases the line
|
||||||
the display goes on redrawing in place.
|
within the new width and fits the line to it, and the display goes on
|
||||||
|
redrawing in place.
|
||||||
- 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
|
||||||
|
|||||||
@@ -29,6 +29,16 @@
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- the rate a progress display shows covers the items completed over the last 5
|
||||||
|
seconds, or since the line before when stderr is not a terminal, so it falls
|
||||||
|
to 0 while nothing completes, and a burst after a wait no longer reads as a
|
||||||
|
steady rate of over 1,000 files a second; the ETA still comes from the phase's
|
||||||
|
average. Once no item has completed for 10 seconds, the line says how long and
|
||||||
|
what the phase is doing, as `no files finished for 42s, reading files`, each
|
||||||
|
phase naming its own work where it is defined. A line too wide for the
|
||||||
|
terminal leaves that out after the rate, keeping the phase, progress, elapsed
|
||||||
|
time and ETA whole (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/125)
|
||||||
|
|
||||||
- new subcommand `info` prints the database's details on stdout, one per line as
|
- new subcommand `info` prints the database's details on stdout, one per line as
|
||||||
a name, a tab and the value: its path and where it came from, the sizes of the
|
a name, a tab and the value: its path and where it came from, the sizes of the
|
||||||
database file and of its `-wal` file when there is one, when the database file
|
database file and of its `-wal` file when there is one, when the database file
|
||||||
|
|||||||
+114
-42
@@ -22,6 +22,15 @@ const plainInterval = 5 * time.Second
|
|||||||
// a TTY.
|
// a TTY.
|
||||||
const redrawInterval = 100 * time.Millisecond
|
const redrawInterval = 100 * time.Millisecond
|
||||||
|
|
||||||
|
// rateWindow is the time the rate a display shows covers: the items
|
||||||
|
// completed over the last rateWindow, which is the time since the line
|
||||||
|
// before when stderr is not a TTY.
|
||||||
|
const rateWindow = 5 * time.Second
|
||||||
|
|
||||||
|
// stallTime is how long a phase goes with no item completed before its
|
||||||
|
// line says so, and what the phase is doing.
|
||||||
|
const stallTime = 10 * time.Second
|
||||||
|
|
||||||
// spinnerType selects the progressbar library's spinner style.
|
// spinnerType selects the progressbar library's spinner style.
|
||||||
const spinnerType = 14
|
const spinnerType = 14
|
||||||
|
|
||||||
@@ -59,6 +68,10 @@ const (
|
|||||||
openingDatabase = "opening the database"
|
openingDatabase = "opening the database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// readingDatabase is what the display of a phase that reads the records
|
||||||
|
// says it is doing once no record has been read for stallTime.
|
||||||
|
const readingDatabase = "reading the database"
|
||||||
|
|
||||||
// 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()))
|
||||||
@@ -89,6 +102,14 @@ type progressParams struct {
|
|||||||
total int64 // the items the phase will complete; -1 when not known
|
total int64 // the items the phase will complete; -1 when not known
|
||||||
estimate bool // total is an estimate, which the count may pass
|
estimate bool // total is an estimate, which the count may pass
|
||||||
waitingFor string // what the phase waits for until setTotal is called
|
waitingFor string // what the phase waits for until setTotal is called
|
||||||
|
doing string // what the phase is doing, as "reading files"; see stallTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawnCount is the count of items completed as of one draw, and when
|
||||||
|
// that draw was.
|
||||||
|
type drawnCount struct {
|
||||||
|
at time.Time
|
||||||
|
count int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// progress renders one phase's progress on stderr. On a TTY the
|
// progress renders one phase's progress on stderr. On a TTY the
|
||||||
@@ -107,9 +128,10 @@ type progressParams struct {
|
|||||||
// phase's helpers without rendering anything, and its writer writes
|
// phase's helpers without rendering anything, and its writer writes
|
||||||
// with no display to clear.
|
// with no display to clear.
|
||||||
type progress struct {
|
type progress struct {
|
||||||
width func() int // reads the terminal's width; nil when not a TTY
|
now func() time.Time // reads the time
|
||||||
stop chan struct{} // closed by finish to end the draws
|
width func() int // reads the terminal's width; nil when not a TTY
|
||||||
done chan struct{} // closed once the draws have ended
|
stop chan struct{} // closed by finish to end the draws
|
||||||
|
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
|
bar *progressbar.ProgressBar // what the library draws; nil until then
|
||||||
@@ -120,33 +142,42 @@ type progress struct {
|
|||||||
total int64
|
total int64
|
||||||
estimate bool
|
estimate bool
|
||||||
waitingFor string
|
waitingFor string
|
||||||
|
doing string
|
||||||
count int64
|
count int64
|
||||||
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 ETA
|
||||||
|
lastItem time.Time // when the last item was completed, or the count started
|
||||||
|
// drawn holds the count at each draw over the last rateWindow, oldest
|
||||||
|
// first; the rate counts from the first. It keeps the draw before the
|
||||||
|
// latest even when that is older, as the line before is when lines
|
||||||
|
// are plainInterval apart and a tick comes late.
|
||||||
|
drawn []drawnCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, terminalWidth, redrawInterval, time.Now)
|
||||||
}
|
}
|
||||||
|
|
||||||
return startProgress(params, nil, plainInterval)
|
return startProgress(params, nil, plainInterval, time.Now)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startProgress shows the phase params describes at once, and again
|
// startProgress shows the phase params describes at once, and again
|
||||||
// every interval until finish: with the progressbar library on a
|
// every interval until finish: with the progressbar library on a
|
||||||
// terminal, whose width each draw reads by calling width, or as a plain
|
// terminal, whose width each draw reads by calling width, or as a plain
|
||||||
// line when width is nil.
|
// line when width is nil. The display reads the time by calling now.
|
||||||
func startProgress(params progressParams, width func() int,
|
func startProgress(params progressParams, width func() int,
|
||||||
interval time.Duration,
|
interval time.Duration, now func() time.Time,
|
||||||
) *progress {
|
) *progress {
|
||||||
now := time.Now()
|
start := now()
|
||||||
p := &progress{
|
p := &progress{
|
||||||
width: width, stop: make(chan struct{}), done: make(chan struct{}),
|
now: now, width: width,
|
||||||
|
stop: make(chan struct{}), done: make(chan struct{}),
|
||||||
label: params.label, unit: params.unit, total: params.total,
|
label: params.label, unit: params.unit, total: params.total,
|
||||||
estimate: params.estimate, waitingFor: params.waitingFor,
|
estimate: params.estimate, waitingFor: params.waitingFor,
|
||||||
start: now, rateStart: now,
|
doing: params.doing, start: start, rateStart: start,
|
||||||
|
lastItem: start, drawn: []drawnCount{{at: start}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the zero state at once: the first item may take minutes,
|
// Show the zero state at once: the first item may take minutes,
|
||||||
@@ -173,13 +204,22 @@ func startProgress(params progressParams, width func() int,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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. It first adds the count to
|
||||||
|
// drawn, then drops from drawn the draws older than rateWindow, except
|
||||||
|
// the one before this draw.
|
||||||
func (p *progress) draw() {
|
func (p *progress) draw() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
now := p.now()
|
||||||
|
|
||||||
|
p.drawn = append(p.drawn, drawnCount{at: now, count: p.count})
|
||||||
|
for len(p.drawn) > 2 && now.Sub(p.drawn[0].at) > rateWindow {
|
||||||
|
p.drawn = p.drawn[1:]
|
||||||
|
}
|
||||||
|
|
||||||
if p.width == nil {
|
if p.width == nil {
|
||||||
text, _ := p.line(math.MaxInt, true)
|
text, _ := p.line(now, math.MaxInt, true)
|
||||||
fmt.Fprintln(os.Stderr, text)
|
fmt.Fprintln(os.Stderr, text)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -192,13 +232,13 @@ func (p *progress) draw() {
|
|||||||
// total of 0 leaves the bar nothing to fill.
|
// total of 0 leaves the bar nothing to fill.
|
||||||
text, withBar := "", false
|
text, withBar := "", false
|
||||||
if p.total > 0 && !p.estimate {
|
if p.total > 0 && !p.estimate {
|
||||||
text, withBar = p.line(width-barMargin, false)
|
text, withBar = p.line(now, width-barMargin, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
margin := barMargin
|
margin := barMargin
|
||||||
if !withBar {
|
if !withBar {
|
||||||
margin = spinnerMargin
|
margin = spinnerMargin
|
||||||
text, _ = p.line(width-margin, true)
|
text, _ = p.line(now, width-margin, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The library makes a bar or a spinner, and before each draw erases
|
// The library makes a bar or a spinner, and before each draw erases
|
||||||
@@ -255,18 +295,22 @@ func (p *progress) newBar(withBar bool, width int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setTotal gives a phase that started without a total its total, once
|
// setTotal gives a phase that started without a total its total, once
|
||||||
// it is known. The rate, and so the ETA, count from then on: the time
|
// it is known. The rate, the ETA and the time with no item completed
|
||||||
// spent waiting for the total says nothing about how fast items go.
|
// count from then on: the time spent waiting for the total says nothing
|
||||||
|
// about how fast items go.
|
||||||
func (p *progress) setTotal(total int64) {
|
func (p *progress) setTotal(total int64) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := p.now()
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
p.total = total
|
p.total = total
|
||||||
p.rateStart = time.Now()
|
p.rateStart, p.lastItem = now, now
|
||||||
|
p.drawn = []drawnCount{{at: now, count: p.count}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// increment records one completed item, which the display shows at its
|
// increment records one completed item, which the display shows at its
|
||||||
@@ -276,10 +320,13 @@ func (p *progress) increment() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := p.now()
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
p.count++
|
p.count++
|
||||||
|
p.lastItem = now
|
||||||
}
|
}
|
||||||
|
|
||||||
// warnf prints a one-line warning to stderr without corrupting the
|
// warnf prints a one-line warning to stderr without corrupting the
|
||||||
@@ -391,52 +438,77 @@ func (p *progress) finish() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// line formats the phase's progress in at most room columns, as
|
// line formats the phase's progress as of now in at most room columns,
|
||||||
|
// 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
|
||||||
//
|
//
|
||||||
// where "files" is the phase's unit, without the percent when percent
|
// where "files" is the phase's unit, without the percent when percent
|
||||||
// is false, as before the library's bar, which shows its own. A phase
|
// is false, as before the library's bar, which shows its own. A phase
|
||||||
// with no total shows the items done instead, or what it waits for.
|
// with no total shows the items done instead, or what it waits for. The
|
||||||
|
// rate is that since the first draw in drawn, and the ETA comes from
|
||||||
|
// the rate since the count started. Once no item has been completed for
|
||||||
|
// stallTime, the line says so before the elapsed time, and what the
|
||||||
|
// phase is doing, as "no files finished for 42s, reading files".
|
||||||
//
|
//
|
||||||
// The phase and its place, the progress, the elapsed time and the ETA
|
// 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
|
// 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
|
// the phase waits for, then what it says of a stall; only if it is
|
||||||
// line reports that it does not fit. Every part is ASCII, so its length
|
// still wider is it cut at room, and line reports that it does not fit.
|
||||||
// is its width in columns. p.mu must be held.
|
// Every part is ASCII, so its length is its width in columns. p.mu must
|
||||||
func (p *progress) line(room int, percent bool) (string, bool) {
|
// be held.
|
||||||
elapsed := time.Since(p.start).Round(time.Second)
|
func (p *progress) line(now time.Time, room int,
|
||||||
|
percent bool,
|
||||||
|
) (string, bool) {
|
||||||
|
elapsed := now.Sub(p.start).Round(time.Second)
|
||||||
|
|
||||||
// head and tail hold the parts kept whole; extra, between them, is
|
// head and tail hold the parts kept whole. extra, after head, is the
|
||||||
// the part left out.
|
// part left out first, and stall, before tail, the part left out
|
||||||
var head, extra, tail string
|
// next.
|
||||||
|
var head, extra, stall, tail string
|
||||||
|
|
||||||
if p.total < 0 && p.waitingFor != "" {
|
if p.total < 0 && p.waitingFor != "" {
|
||||||
head, extra = p.label+": ", p.waitingFor
|
head, extra = p.label+": ", p.waitingFor+", "
|
||||||
tail = fmt.Sprintf("elapsed %s, eta unknown", elapsed)
|
tail = fmt.Sprintf("elapsed %s, eta unknown", elapsed)
|
||||||
} else {
|
} else {
|
||||||
rate := 0.0
|
since := p.drawn[0]
|
||||||
if s := time.Since(p.rateStart).Seconds(); s > 0 {
|
rate := perSecond(p.count-since.count, now.Sub(since.at))
|
||||||
rate = float64(p.count) / s
|
average := perSecond(p.count, now.Sub(p.rateStart))
|
||||||
}
|
|
||||||
|
|
||||||
head = p.label + ": " + p.itemsDone(percent) + ", "
|
head = p.label + ": " + p.itemsDone(percent) + ", "
|
||||||
extra = fmt.Sprintf("%.0f %s/s", rate, p.unit)
|
extra = fmt.Sprintf("%.0f %s/s, ", rate, p.unit)
|
||||||
tail = fmt.Sprintf("elapsed %s, eta %s", elapsed, p.eta(rate))
|
|
||||||
|
if idle := now.Sub(p.lastItem); idle >= stallTime {
|
||||||
|
stall = fmt.Sprintf("no %s finished for %s, %s, ",
|
||||||
|
p.unit, idle.Round(time.Second), p.doing)
|
||||||
|
}
|
||||||
|
|
||||||
|
tail = fmt.Sprintf("elapsed %s, eta %s", elapsed, p.eta(average))
|
||||||
}
|
}
|
||||||
|
|
||||||
full, kept := head+extra+", "+tail, head+tail
|
for _, text := range []string{
|
||||||
|
head + extra + stall + tail, head + stall + tail, head + tail,
|
||||||
switch {
|
} {
|
||||||
case len(full) <= room:
|
if len(text) <= room {
|
||||||
return full, true
|
return text, true
|
||||||
case len(kept) <= room:
|
}
|
||||||
return kept, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kept := head + tail
|
||||||
|
|
||||||
return kept[:max(room, 0)], false
|
return kept[:max(room, 0)], false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// perSecond returns count items over d as items a second, or 0 when no
|
||||||
|
// time has passed.
|
||||||
|
func perSecond(count int64, d time.Duration) float64 {
|
||||||
|
if d <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(count) / d.Seconds()
|
||||||
|
}
|
||||||
|
|
||||||
// itemsDone formats the items done, as "[1234/5000] 25%", with "~"
|
// itemsDone formats the items done, as "[1234/5000] 25%", with "~"
|
||||||
// before a total that is an estimate and without the percent when
|
// before a total that is an estimate and without the percent when
|
||||||
// percent is false, or as "1234 files", in the phase's unit, when the
|
// percent is false, or as "1234 files", in the phase's unit, when the
|
||||||
|
|||||||
+232
-39
@@ -6,7 +6,9 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -20,6 +22,29 @@ const testRedraw = time.Millisecond
|
|||||||
// it expects.
|
// it expects.
|
||||||
const testDeadline = 5 * time.Second
|
const testDeadline = 5 * time.Second
|
||||||
|
|
||||||
|
// testClock is a clock a test moves by hand, for a display that reads
|
||||||
|
// it from its own goroutine too.
|
||||||
|
type testClock struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
now time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now returns the clock's time.
|
||||||
|
func (c *testClock) Now() time.Time {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
return c.now
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add moves the clock on by d.
|
||||||
|
func (c *testClock) Add(d time.Duration) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.now = c.now.Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
//nolint:paralleltest // replaces the process-wide os.Stderr
|
//nolint:paralleltest // replaces the process-wide os.Stderr
|
||||||
func TestStderrIsTTYFalseForNonTerminals(t *testing.T) {
|
func TestStderrIsTTYFalseForNonTerminals(t *testing.T) {
|
||||||
r, pipe, err := os.Pipe()
|
r, pipe, err := os.Pipe()
|
||||||
@@ -72,11 +97,13 @@ func TestPhaseLabel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestProgressLine checks the line a display shows in each state, ten
|
// TestProgressLine checks the line a display shows in each state, ten
|
||||||
// seconds into the phase.
|
// seconds into the phase, with no draw since the first and the last
|
||||||
|
// item just completed.
|
||||||
func TestProgressLine(t *testing.T) {
|
func TestProgressLine(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ago := time.Now().Add(-10 * time.Second)
|
now := time.Now()
|
||||||
|
ago := now.Add(-10 * time.Second)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
p *progress
|
p *progress
|
||||||
@@ -129,28 +156,41 @@ 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, tc.p.lastItem = ago, ago, now
|
||||||
|
tc.p.drawn = []drawnCount{{at: ago}}
|
||||||
|
|
||||||
if got, _ := tc.p.line(math.MaxInt, true); got != tc.want {
|
if got, _ := tc.p.line(now, math.MaxInt, true); 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
|
// 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
|
// seconds into the phase, and fifty into a phase with no file read for
|
||||||
// waits for, and is cut only when what is left is still too wide.
|
// the last 42: it leaves out the rate, or what the phase waits for,
|
||||||
|
// then what it says of the stall, and is cut only when what is left is
|
||||||
|
// still too wide.
|
||||||
func TestLineFitsRoom(t *testing.T) {
|
func TestLineFitsRoom(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ago := time.Now().Add(-10 * time.Second)
|
now := time.Now()
|
||||||
|
ago := now.Add(-10 * time.Second)
|
||||||
hash := &progress{
|
hash := &progress{
|
||||||
label: scanPhase("hash"), unit: scanUnit, total: 100, count: 20,
|
label: scanPhase("hash"), unit: scanUnit, total: 100, count: 20,
|
||||||
start: ago, rateStart: ago,
|
start: ago, rateStart: ago, lastItem: now,
|
||||||
|
drawn: []drawnCount{{at: ago}},
|
||||||
}
|
}
|
||||||
content := &progress{
|
content := &progress{
|
||||||
label: scanPhase("content"), total: -1,
|
label: scanPhase("content"), total: -1,
|
||||||
waitingFor: "searching the database", start: ago, rateStart: ago,
|
waitingFor: searchingDatabase, start: ago, rateStart: ago,
|
||||||
|
}
|
||||||
|
|
||||||
|
started := now.Add(-50 * time.Second)
|
||||||
|
stalled := &progress{
|
||||||
|
label: scanPhase("hash"), unit: scanUnit, total: 100, count: 20,
|
||||||
|
doing: readingFiles, start: started, rateStart: started,
|
||||||
|
lastItem: now.Add(-42 * time.Second),
|
||||||
|
drawn: []drawnCount{{at: now.Add(-rateWindow), count: 20}},
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
@@ -180,10 +220,24 @@ func TestLineFitsRoom(t *testing.T) {
|
|||||||
p: content, room: 40, percent: true,
|
p: content, room: 40, percent: true,
|
||||||
want: "[5/5] content: elapsed 10s, eta unknown",
|
want: "[5/5] content: elapsed 10s, eta unknown",
|
||||||
fits: true,
|
fits: true,
|
||||||
|
}, {
|
||||||
|
p: stalled, room: 101, percent: true,
|
||||||
|
want: "[3/5] hash: [20/100] 20%, 0 files/s, no files finished for " +
|
||||||
|
"42s, reading files, elapsed 50s, eta 3m20s",
|
||||||
|
fits: true,
|
||||||
|
}, {
|
||||||
|
p: stalled, room: 100, percent: true,
|
||||||
|
want: "[3/5] hash: [20/100] 20%, no files finished for 42s, " +
|
||||||
|
"reading files, elapsed 50s, eta 3m20s",
|
||||||
|
fits: true,
|
||||||
|
}, {
|
||||||
|
p: stalled, room: 89, percent: true,
|
||||||
|
want: "[3/5] hash: [20/100] 20%, elapsed 50s, eta 3m20s",
|
||||||
|
fits: true,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got, fits := tc.p.line(tc.room, tc.percent)
|
got, fits := tc.p.line(now, tc.room, tc.percent)
|
||||||
if got != tc.want || fits != tc.fits {
|
if got != tc.want || fits != tc.fits {
|
||||||
t.Errorf("line(%d, %t) = %q, %t, want %q, %t",
|
t.Errorf("line(%d, %t) = %q, %t, want %q, %t",
|
||||||
tc.room, tc.percent, got, fits, tc.want, tc.fits)
|
tc.room, tc.percent, got, fits, tc.want, tc.fits)
|
||||||
@@ -192,27 +246,37 @@ func TestLineFitsRoom(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// its ETA and its time with no record read from when the total becomes
|
||||||
// phase started waiting for it.
|
// known, not from when the phase started waiting for it.
|
||||||
func TestSetTotalStartsRate(t *testing.T) {
|
func TestSetTotalStartsRate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
waited := time.Now().Add(-20 * time.Second)
|
clock := &testClock{now: time.Now()}
|
||||||
|
waited := clock.Now().Add(-20 * time.Second)
|
||||||
p := &progress{
|
p := &progress{
|
||||||
label: scanPhase("load"), unit: scanUnit, total: -1, start: waited,
|
now: clock.Now, label: scanPhase("load"), unit: scanUnit, total: -1,
|
||||||
rateStart: waited,
|
doing: readingDatabase, start: waited, rateStart: waited,
|
||||||
|
lastItem: waited, drawn: []drawnCount{{at: waited}},
|
||||||
}
|
}
|
||||||
|
|
||||||
p.setTotal(100)
|
p.setTotal(100)
|
||||||
|
|
||||||
// Ten seconds after the total came, without waiting ten seconds,
|
// Ten seconds after the total came, no record has been read yet.
|
||||||
// 20 records are read.
|
clock.Add(10 * time.Second)
|
||||||
p.start = p.start.Add(-10 * time.Second)
|
|
||||||
p.rateStart = p.rateStart.Add(-10 * time.Second)
|
|
||||||
p.count = 20
|
|
||||||
|
|
||||||
want := "[1/5] load: [20/100] 20%, 2 files/s, elapsed 30s, eta 40s"
|
want := "[1/5] load: [0/100] 0%, 0 files/s, no files finished for 10s, " +
|
||||||
if got, _ := p.line(math.MaxInt, true); got != want {
|
"reading the database, elapsed 30s, eta unknown"
|
||||||
|
if got, _ := p.line(clock.Now(), math.MaxInt, true); got != want {
|
||||||
|
t.Errorf("line = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then 20 are.
|
||||||
|
for range 20 {
|
||||||
|
p.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
want = "[1/5] load: [20/100] 20%, 2 files/s, elapsed 30s, eta 40s"
|
||||||
|
if got, _ := p.line(clock.Now(), math.MaxInt, true); got != want {
|
||||||
t.Errorf("line = %q, want %q", got, want)
|
t.Errorf("line = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,13 +336,138 @@ func TestPlainProgressPrintsWhileWaiting(t *testing.T) {
|
|||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: scanPhase("load"), total: -1,
|
label: scanPhase("load"), total: -1,
|
||||||
waitingFor: countingRecords,
|
waitingFor: countingRecords,
|
||||||
}, nil, testRedraw)
|
}, nil, testRedraw, time.Now)
|
||||||
|
|
||||||
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)
|
||||||
p.finish()
|
p.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPlainProgressStalledProducer checks that a phase whose files stop
|
||||||
|
// coming, as when one large file is being read or a disk is slow, goes
|
||||||
|
// on printing its line every interval: the rate falls to 0 once no file
|
||||||
|
// has been read for rateWindow, and once none has for stallTime the
|
||||||
|
// line says so, and what the phase is doing. The test moves the
|
||||||
|
// display's clock by hand.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
|
func TestPlainProgressStalledProducer(t *testing.T) {
|
||||||
|
stderr := captureStderr(t)
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
p := startProgress(progressParams{
|
||||||
|
label: scanPhase("hash"), unit: scanUnit, total: 100,
|
||||||
|
doing: readingFiles,
|
||||||
|
}, nil, testRedraw, clock.Now)
|
||||||
|
|
||||||
|
// Pass or fail, the display ends before stderr is put back.
|
||||||
|
t.Cleanup(p.finish)
|
||||||
|
|
||||||
|
// 20 files read as the phase starts, then none.
|
||||||
|
for range 20 {
|
||||||
|
p.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
clock.Add(5 * time.Second)
|
||||||
|
awaitOutput(t, stderr,
|
||||||
|
"[3/5] hash: [20/100] 20%, 4 files/s, elapsed 5s, eta 20s\n", 3)
|
||||||
|
|
||||||
|
clock.Add(time.Second)
|
||||||
|
awaitOutput(t, stderr,
|
||||||
|
"[3/5] hash: [20/100] 20%, 0 files/s, elapsed 6s, eta 24s\n", 3)
|
||||||
|
|
||||||
|
clock.Add(4 * time.Second)
|
||||||
|
awaitOutput(t, stderr,
|
||||||
|
"[3/5] hash: [20/100] 20%, 0 files/s, no files finished for 10s, "+
|
||||||
|
"reading files, elapsed 10s, eta 40s\n", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPlainProgressRateAfterStall draws a phase's plain line by hand, a
|
||||||
|
// little over plainInterval after the line before, as a late tick
|
||||||
|
// does, through a stall and a burst of files after it: the rate covers
|
||||||
|
// the files read since the line before, while the ETA comes from the
|
||||||
|
// phase's average.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
|
func TestPlainProgressRateAfterStall(t *testing.T) {
|
||||||
|
stderr := captureStderr(t)
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
|
||||||
|
// No timed draws: the test draws.
|
||||||
|
p := startProgress(progressParams{
|
||||||
|
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
||||||
|
doing: readingFiles,
|
||||||
|
}, nil, time.Hour, clock.Now)
|
||||||
|
|
||||||
|
late := plainInterval + time.Millisecond
|
||||||
|
|
||||||
|
clock.Add(late)
|
||||||
|
p.draw()
|
||||||
|
clock.Add(late)
|
||||||
|
p.draw()
|
||||||
|
|
||||||
|
for range 100 {
|
||||||
|
p.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
clock.Add(late)
|
||||||
|
p.finish()
|
||||||
|
|
||||||
|
want := []string{
|
||||||
|
"[3/5] hash: [0/1000] 0%, 0 files/s, elapsed 0s, eta unknown",
|
||||||
|
"[3/5] hash: [0/1000] 0%, 0 files/s, elapsed 5s, eta unknown",
|
||||||
|
"[3/5] hash: [0/1000] 0%, 0 files/s, no files finished for 10s, " +
|
||||||
|
"reading files, elapsed 10s, eta unknown",
|
||||||
|
"[3/5] hash: [100/1000] 10%, 20 files/s, elapsed 15s, eta 2m15s",
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(strings.TrimSuffix(stderr(), "\n"), "\n")
|
||||||
|
if !slices.Equal(lines, want) {
|
||||||
|
t.Errorf("stderr lines = %q, want %q", lines, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProgressRateAfterStallRedrawn draws a phase by hand every
|
||||||
|
// redrawInterval, as a terminal does, through a stall of stallTime and
|
||||||
|
// a burst of files after it: the rate covers the files read over the
|
||||||
|
// last rateWindow, and falls to 0 once the burst is older than that,
|
||||||
|
// while the ETA comes from the phase's average. Plain lines show each
|
||||||
|
// draw.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
|
func TestProgressRateAfterStallRedrawn(t *testing.T) {
|
||||||
|
stderr := captureStderr(t)
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
|
||||||
|
// No timed draws: the test draws.
|
||||||
|
p := startProgress(progressParams{
|
||||||
|
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
||||||
|
doing: readingFiles,
|
||||||
|
}, nil, time.Hour, clock.Now)
|
||||||
|
|
||||||
|
// redraw draws the display every redrawInterval for d.
|
||||||
|
redraw := func(d time.Duration) {
|
||||||
|
for range d / redrawInterval {
|
||||||
|
clock.Add(redrawInterval)
|
||||||
|
p.draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw(stallTime)
|
||||||
|
|
||||||
|
for range 50 {
|
||||||
|
p.increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
redraw(rateWindow + redrawInterval)
|
||||||
|
p.finish()
|
||||||
|
|
||||||
|
want := "[3/5] hash: [50/1000] 5%, 10 files/s, elapsed 15s, eta 4m45s\n" +
|
||||||
|
"[3/5] hash: [50/1000] 5%, 0 files/s, elapsed 15s, eta 4m47s\n"
|
||||||
|
if !strings.Contains(stderr(), want) {
|
||||||
|
t.Errorf("stderr = %q, want the lines %q", stderr(), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// lastFrame returns what a terminal shows of the frames a display drew:
|
// lastFrame returns what a terminal shows of the frames a display drew:
|
||||||
// the last one. The library starts each frame with a carriage return
|
// the last one. The library starts each frame with a carriage return
|
||||||
// and erases the previous one with spaces first.
|
// and erases the previous one with spaces first.
|
||||||
@@ -321,7 +510,7 @@ func TestTerminalRedrawsWhileWaiting(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
before := len(stderr())
|
before := len(stderr())
|
||||||
since := func() string { return stderr()[before:] }
|
since := func() string { return stderr()[before:] }
|
||||||
p := startProgress(tc.params, terminalWidth, testRedraw)
|
p := startProgress(tc.params, terminalWidth, testRedraw, time.Now)
|
||||||
|
|
||||||
for range 50 {
|
for range 50 {
|
||||||
p.increment()
|
p.increment()
|
||||||
@@ -343,21 +532,23 @@ func TestTerminalRedrawsWhileWaiting(t *testing.T) {
|
|||||||
|
|
||||||
// drawLate draws the display params describes on a terminal width
|
// drawLate draws the display params describes on a terminal width
|
||||||
// columns wide as it starts and once more as it ends, with count items
|
// columns wide as it starts and once more as it ends, with count items
|
||||||
// done two and a half hours into the phase, and returns what it wrote
|
// done two and a half hours into the phase, the last just now, and
|
||||||
// to stderr.
|
// returns what it wrote to stderr.
|
||||||
func drawLate(stderr func() string, params progressParams, width int,
|
func drawLate(stderr func() string, params progressParams, width int,
|
||||||
count int64,
|
count int64,
|
||||||
) string {
|
) string {
|
||||||
before := len(stderr())
|
before := len(stderr())
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
|
||||||
// No timed draws come between the two.
|
// No timed draws come between the two.
|
||||||
p := startProgress(params, func() int { return width }, time.Hour)
|
p := startProgress(params, func() int { return width }, time.Hour,
|
||||||
|
clock.Now)
|
||||||
|
|
||||||
// Without counting the items one by one or waiting the hours.
|
// Without counting the items one by one or waiting the hours.
|
||||||
|
clock.Add(150 * time.Minute)
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.count = count
|
p.count, p.lastItem = count, clock.Now()
|
||||||
p.start = p.start.Add(-150 * time.Minute)
|
|
||||||
p.rateStart = p.start
|
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
p.finish()
|
p.finish()
|
||||||
@@ -479,14 +670,16 @@ func TestTerminalNarrowedRedrawsInPlace(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
width := 120
|
width := 120
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
|
||||||
// No timed draws: the test draws.
|
// No timed draws: the test draws.
|
||||||
p := startProgress(tc.params, func() int { return width }, time.Hour)
|
p := startProgress(tc.params, func() int { return width }, time.Hour,
|
||||||
|
clock.Now)
|
||||||
|
|
||||||
|
clock.Add(150 * time.Minute)
|
||||||
|
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.count = 1234567
|
p.count, p.lastItem = 1234567, clock.Now()
|
||||||
p.start = p.start.Add(-150 * time.Minute)
|
|
||||||
p.rateStart = p.start
|
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
p.draw()
|
p.draw()
|
||||||
@@ -528,7 +721,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, terminalWidth, testRedraw, time.Now)
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -581,7 +774,7 @@ func TestProgressWriterRowsOnOwnLines(t *testing.T) {
|
|||||||
var want []string
|
var want []string
|
||||||
|
|
||||||
for _, params := range cases {
|
for _, params := range cases {
|
||||||
p := startProgress(params, terminalWidth, testRedraw)
|
p := startProgress(params, terminalWidth, testRedraw, time.Now)
|
||||||
out := p.writer(os.Stderr)
|
out := p.writer(os.Stderr)
|
||||||
|
|
||||||
// No pause between rows: one is garbled only if a redraw lands
|
// No pause between rows: one is garbled only if a redraw lands
|
||||||
@@ -644,7 +837,7 @@ func TestProgressWriterStalledStdoutKeepsLines(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
p := startProgress(progressParams{label: treesPhase("write"), total: 1},
|
p := startProgress(progressParams{label: treesPhase("write"), total: 1},
|
||||||
nil, testRedraw)
|
nil, testRedraw, time.Now)
|
||||||
out := p.writer(w)
|
out := p.writer(w)
|
||||||
written := make(chan error, 1)
|
written := make(chan error, 1)
|
||||||
|
|
||||||
@@ -687,7 +880,7 @@ func TestProgressWriterStalledStdoutKeepsLines(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)
|
terminalWidth, redrawInterval, time.Now)
|
||||||
|
|
||||||
p.increment()
|
p.increment()
|
||||||
p.increment()
|
p.increment()
|
||||||
@@ -706,7 +899,7 @@ 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)
|
terminalWidth, testRedraw, time.Now)
|
||||||
|
|
||||||
p.increment()
|
p.increment()
|
||||||
p.increment()
|
p.increment()
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func runReport(ctx context.Context, stdout io.Writer) error {
|
|||||||
|
|
||||||
prog = newProgress(progressParams{
|
prog = newProgress(progressParams{
|
||||||
label: reportPhase("read"), unit: "records", total: -1,
|
label: reportPhase("read"), unit: "records", total: -1,
|
||||||
waitingFor: countingRecords,
|
waitingFor: countingRecords, doing: readingDatabase,
|
||||||
})
|
})
|
||||||
report, err := readReport(ctx, db, dbPath, stdout, prog)
|
report, err := readReport(ctx, db, dbPath, stdout, prog)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -156,7 +156,7 @@ func TestReportStdoutFailsWhileReading(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Counts the records read, and draws nothing.
|
// Counts the records read, and draws nothing.
|
||||||
prog := &progress{}
|
prog := &progress{now: time.Now}
|
||||||
|
|
||||||
_, err = readReport(t.Context(), db, "", failingWriter{}, prog)
|
_, err = readReport(t.Context(), db, "", failingWriter{}, prog)
|
||||||
if !errors.Is(err, errWriteFailed) ||
|
if !errors.Is(err, errWriteFailed) ||
|
||||||
@@ -250,7 +250,7 @@ func TestReadReportOnTerminal(t *testing.T) {
|
|||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: reportPhase("read"), unit: "records", total: -1,
|
label: reportPhase("read"), unit: "records", total: -1,
|
||||||
waitingFor: countingRecords,
|
waitingFor: countingRecords,
|
||||||
}, terminalWidth, time.Hour)
|
}, terminalWidth, time.Hour, time.Now)
|
||||||
|
|
||||||
_, err = readReport(t.Context(), db, "", os.Stderr, p)
|
_, err = readReport(t.Context(), db, "", os.Stderr, p)
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ type fileMeta struct {
|
|||||||
// names it.
|
// names it.
|
||||||
const scanUnit = "files"
|
const scanUnit = "files"
|
||||||
|
|
||||||
|
// readingFiles is what the display of a phase that reads files says it
|
||||||
|
// is doing once no file has been read for stallTime.
|
||||||
|
const readingFiles = "reading files"
|
||||||
|
|
||||||
|
// searchingDatabase is what the display of the content phase's checks
|
||||||
|
// says it waits for until the search returns its first record, and what
|
||||||
|
// it says it is doing once no record has been checked for stallTime.
|
||||||
|
const searchingDatabase = "searching the database"
|
||||||
|
|
||||||
// scanPhase names one phase of a scan with its place among all of them,
|
// scanPhase names one phase of a scan with its place among all of them,
|
||||||
// as "[2/5] walk", for the phase's progress display. The list here is
|
// as "[2/5] walk", for the phase's progress display. The list here is
|
||||||
// the one place the phases are counted.
|
// the one place the phases are counted.
|
||||||
@@ -446,7 +455,7 @@ func (s *scanState) loadIndex(ctx context.Context, roots []string) error {
|
|||||||
// first gives the display its total.
|
// first gives the display its total.
|
||||||
prog := newProgress(progressParams{
|
prog := newProgress(progressParams{
|
||||||
label: scanPhase("load"), unit: scanUnit, total: -1,
|
label: scanPhase("load"), unit: scanUnit, total: -1,
|
||||||
waitingFor: countingRecords,
|
waitingFor: countingRecords, doing: readingDatabase,
|
||||||
})
|
})
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
|
|
||||||
@@ -497,7 +506,7 @@ func (s *scanState) walkPhase(
|
|||||||
|
|
||||||
prog := newProgress(progressParams{
|
prog := newProgress(progressParams{
|
||||||
label: scanPhase("walk"), unit: scanUnit, total: estimate,
|
label: scanPhase("walk"), unit: scanUnit, total: estimate,
|
||||||
estimate: true,
|
estimate: true, doing: "listing directories",
|
||||||
})
|
})
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -689,6 +698,7 @@ func (s *scanState) readRuns(ctx context.Context, workers int,
|
|||||||
|
|
||||||
prog := newProgress(progressParams{
|
prog := newProgress(progressParams{
|
||||||
label: scanPhase(label), unit: scanUnit, total: int64(len(runs)),
|
label: scanPhase(label), unit: scanUnit, total: int64(len(runs)),
|
||||||
|
doing: readingFiles,
|
||||||
})
|
})
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
|
|
||||||
@@ -778,6 +788,7 @@ func (s *scanState) updatePhase(ctx context.Context) error {
|
|||||||
total := len(s.batch) + len(s.sentinels) + len(deletes)
|
total := len(s.batch) + len(s.sentinels) + len(deletes)
|
||||||
prog := newProgress(progressParams{
|
prog := newProgress(progressParams{
|
||||||
label: scanPhase("update"), unit: scanUnit, total: int64(total),
|
label: scanPhase("update"), unit: scanUnit, total: int64(total),
|
||||||
|
doing: "writing to the database",
|
||||||
})
|
})
|
||||||
|
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
@@ -866,7 +877,7 @@ func (s *scanState) contentCandidates(
|
|||||||
// the search returns, and so do their rate and ETA.
|
// the search returns, and so do their rate and ETA.
|
||||||
prog := newProgress(progressParams{
|
prog := newProgress(progressParams{
|
||||||
label: scanPhase("content"), unit: scanUnit, total: -1,
|
label: scanPhase("content"), unit: scanUnit, total: -1,
|
||||||
waitingFor: "searching the database",
|
waitingFor: searchingDatabase, doing: searchingDatabase,
|
||||||
})
|
})
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func runTrees(ctx context.Context, stdout io.Writer) error {
|
|||||||
|
|
||||||
prog = newProgress(progressParams{
|
prog = newProgress(progressParams{
|
||||||
label: treesPhase("read"), unit: "records", total: -1,
|
label: treesPhase("read"), unit: "records", total: -1,
|
||||||
waitingFor: countingRecords,
|
waitingFor: countingRecords, doing: readingDatabase,
|
||||||
})
|
})
|
||||||
records, super, allDirs, err := readTree(ctx, db, prog)
|
records, super, allDirs, err := readTree(ctx, db, prog)
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ func runTrees(ctx context.Context, stdout io.Writer) error {
|
|||||||
|
|
||||||
prog = newProgress(progressParams{
|
prog = newProgress(progressParams{
|
||||||
label: treesPhase("group"), unit: "directories",
|
label: treesPhase("group"), unit: "directories",
|
||||||
total: int64(len(allDirs)),
|
total: int64(len(allDirs)), doing: "grouping directories",
|
||||||
})
|
})
|
||||||
dupes := collectTreeGroups(allDirs, super, prog)
|
dupes := collectTreeGroups(allDirs, super, prog)
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ func runTrees(ctx context.Context, stdout io.Writer) error {
|
|||||||
|
|
||||||
prog = newProgress(progressParams{
|
prog = newProgress(progressParams{
|
||||||
label: treesPhase("write"), unit: "groups", total: int64(len(dupes)),
|
label: treesPhase("write"), unit: "groups", total: int64(len(dupes)),
|
||||||
|
doing: "writing the report",
|
||||||
})
|
})
|
||||||
dupeTrees, reclaimable, err := writeTreeGroups(stdout, dupes, prog)
|
dupeTrees, reclaimable, err := writeTreeGroups(stdout, dupes, prog)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -279,7 +279,7 @@ func TestWriteTreeGroupsOnTerminal(t *testing.T) {
|
|||||||
// No timed draws: the display is drawn as it starts and as it ends.
|
// No timed draws: the display is drawn as it starts and as it ends.
|
||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: treesPhase("write"), unit: "groups", total: int64(len(dupes)),
|
label: treesPhase("write"), unit: "groups", total: int64(len(dupes)),
|
||||||
}, terminalWidth, time.Hour)
|
}, terminalWidth, time.Hour, time.Now)
|
||||||
|
|
||||||
_, _, err := writeTreeGroups(os.Stderr, dupes, p)
|
_, _, err := writeTreeGroups(os.Stderr, dupes, p)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user