Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52cafd1cd3 |
@@ -836,9 +836,8 @@ Each display is one line holding:
|
|||||||
directories or groups. It covers the items completed over the last 5 seconds,
|
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
|
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
|
while nothing completes, and a burst of items after a wait shows as a burst
|
||||||
rather than as a steady rate.
|
rather than as a steady rate. Once no item has completed for 10 seconds, the
|
||||||
- once no item has completed for 10 seconds, how long that has been and what the
|
word `stalled` and what the phase is doing take the rate's place (see below).
|
||||||
phase is doing (see below)
|
|
||||||
- elapsed time
|
- elapsed time
|
||||||
- estimated time remaining, or `unknown`. It comes from the phase's average rate
|
- 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.
|
since it started counting, so that it does not swing with each burst.
|
||||||
@@ -848,17 +847,19 @@ 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
|
[3/5] hash: [12345/45678] 27%, stalled reading, elapsed 4m30s, eta 12m9s
|
||||||
```
|
```
|
||||||
|
|
||||||
When no item has completed for 10 seconds, as while one large file is read, a
|
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
|
disk is slow or the database is busy, the line says `stalled` and what the phase
|
||||||
doing: `walk` is listing directories; `hash` and the content phase's reads are
|
is doing in place of the rate, which would read 0: `walk` is `listing`
|
||||||
reading files; `update` is writing to the database; `load` and the `read` phases
|
directories; `hash` and the content phase's reads are `reading` files; `update`
|
||||||
of `trees` and `report` are reading the database; the content phase's checks are
|
is `writing` to the database; `load` and the `read` phase of `trees` are
|
||||||
searching the database; `trees`' `group` is grouping directories, and its
|
`reading` the database; the content phase's checks are `searching` the database;
|
||||||
`write` is writing the report. A phase still waiting for its total already says
|
`trees`' `group` is `grouping` directories, and its `write` is `writing` the
|
||||||
what it waits for, and says nothing more.
|
report. The `read` phase of `report` writes the report while it reads the
|
||||||
|
records and may be waiting on either, so it says `read/write`. A phase still
|
||||||
|
waiting for its total already says what it waits for, and says nothing more.
|
||||||
|
|
||||||
Additional requirements:
|
Additional requirements:
|
||||||
|
|
||||||
@@ -871,16 +872,18 @@ Additional requirements:
|
|||||||
times a second, so its elapsed time keeps moving while a phase waits for its
|
times a second, so its elapsed time keeps moving while a phase waits for its
|
||||||
next item.
|
next item.
|
||||||
- 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, what a stalled phase is
|
||||||
ETA are kept whole: a line too wide for the terminal leaves out its rate first
|
doing, the elapsed time and the ETA are kept whole: a line too wide for the
|
||||||
(or what the phase is waiting for), then what it says of 10 seconds with no
|
terminal leaves out its rate (or what the phase is waiting for), and a bar
|
||||||
item completed, and a bar with no room beside those four gives way to the
|
with no room beside the parts kept whole gives way to the spinner. The words a
|
||||||
spinner. Only when those four alone are too wide, as on a terminal narrower
|
stalled phase shows are short, so that a line like the stalled one in the
|
||||||
than about 70 columns hours into a scan of millions of files, is the line cut
|
example above, with tens of thousands of files and times in minutes, fits on
|
||||||
short at the terminal's edge, so the end of the ETA is lost first. A terminal
|
an 80-column terminal. Only when the parts kept whole are too wide, as on a
|
||||||
made narrower while a phase runs gets the same: the next draw erases the line
|
terminal narrower than about 70 columns hours into a scan of millions of
|
||||||
within the new width and fits the line to it, and the display goes on
|
files, or about 90 once the phase has stalled, is the line cut short at the
|
||||||
redrawing in place.
|
terminal's edge, so the end of the ETA is lost first. A terminal made narrower
|
||||||
|
while a phase runs gets the same: the next draw erases the line 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
|
||||||
|
|||||||
@@ -33,11 +33,13 @@
|
|||||||
seconds, or since the line before when stderr is not a terminal, so it falls
|
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
|
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
|
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
|
average. Once no item has completed for 10 seconds, the word `stalled` and
|
||||||
what the phase is doing, as `no files finished for 42s, reading files`, each
|
what the phase is doing take the rate's place, as `stalled reading`, each
|
||||||
phase naming its own work where it is defined. A line too wide for the
|
phase naming its own work where it is defined; `report`'s `read` says
|
||||||
terminal leaves that out after the rate, keeping the phase, progress, elapsed
|
`read/write`, as it writes the report while it reads. Those words are kept
|
||||||
time and ETA whole (2026-10-16, https://git.eeqj.de/sneak/sfdupes/issues/125)
|
whole with the phase, progress, elapsed time and ETA, so only the bar gives
|
||||||
|
way before them, and they fit on an 80-column terminal in an ordinary scan
|
||||||
|
(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
|
||||||
|
|||||||
+27
-30
@@ -28,7 +28,7 @@ const redrawInterval = 100 * time.Millisecond
|
|||||||
const rateWindow = 5 * time.Second
|
const rateWindow = 5 * time.Second
|
||||||
|
|
||||||
// stallTime is how long a phase goes with no item completed before its
|
// stallTime is how long a phase goes with no item completed before its
|
||||||
// line says so, and what the phase is doing.
|
// line says it has stalled, and what it is doing, in place of the rate.
|
||||||
const stallTime = 10 * time.Second
|
const stallTime = 10 * time.Second
|
||||||
|
|
||||||
// spinnerType selects the progressbar library's spinner style.
|
// spinnerType selects the progressbar library's spinner style.
|
||||||
@@ -68,9 +68,9 @@ const (
|
|||||||
openingDatabase = "opening the database"
|
openingDatabase = "opening the database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// readingDatabase is what the display of a phase that reads the records
|
// reading is what the display of a phase that reads files or records
|
||||||
// says it is doing once no record has been read for stallTime.
|
// says it is doing once it has stalled; see stallTime.
|
||||||
const readingDatabase = "reading the database"
|
const reading = "reading"
|
||||||
|
|
||||||
// stderrIsTTY reports whether stderr is attached to a terminal.
|
// stderrIsTTY reports whether stderr is attached to a terminal.
|
||||||
func stderrIsTTY() bool {
|
func stderrIsTTY() bool {
|
||||||
@@ -102,7 +102,7 @@ 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
|
doing string // what the phase is doing, as "reading"; see stallTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawnCount is the count of items completed as of one draw, and when
|
// drawnCount is the count of items completed as of one draw, and when
|
||||||
@@ -448,24 +448,23 @@ func (p *progress) finish() {
|
|||||||
// with no total shows the items done instead, or what it waits for. The
|
// 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
|
// 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
|
// 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
|
// stallTime, the rate, which would read 0, gives way to the word
|
||||||
// phase is doing, as "no files finished for 42s, reading files".
|
// "stalled" and what the phase is doing, as "stalled reading".
|
||||||
//
|
//
|
||||||
// The phase and its place, the progress, the elapsed time and the ETA
|
// The phase and its place, the progress, what a stalled phase is doing,
|
||||||
// are kept whole. A line wider than room leaves out the rate, or what
|
// the elapsed time and the ETA are kept whole. A line wider than room
|
||||||
// the phase waits for, then what it says of a stall; only if it is
|
// leaves out the rate, or what the phase waits for; only if it is still
|
||||||
// still wider is it cut at room, and line reports that it does not fit.
|
// wider is it cut at room, and line reports that it does not fit. Every
|
||||||
// Every part is ASCII, so its length is its width in columns. p.mu must
|
// part is ASCII, so its length is its width in columns. p.mu must be
|
||||||
// be held.
|
// held.
|
||||||
func (p *progress) line(now time.Time, room int,
|
func (p *progress) line(now time.Time, room int,
|
||||||
percent bool,
|
percent bool,
|
||||||
) (string, bool) {
|
) (string, bool) {
|
||||||
elapsed := now.Sub(p.start).Round(time.Second)
|
elapsed := now.Sub(p.start).Round(time.Second)
|
||||||
|
|
||||||
// head and tail hold the parts kept whole. extra, after head, is the
|
// head and tail hold the parts kept whole; extra, between them, is
|
||||||
// part left out first, and stall, before tail, the part left out
|
// the part left out.
|
||||||
// next.
|
var head, extra, tail string
|
||||||
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+", "
|
||||||
@@ -477,24 +476,22 @@ func (p *progress) line(now time.Time, room int,
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
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))
|
tail = fmt.Sprintf("elapsed %s, eta %s", elapsed, p.eta(average))
|
||||||
}
|
|
||||||
|
|
||||||
for _, text := range []string{
|
if now.Sub(p.lastItem) >= stallTime {
|
||||||
head + extra + stall + tail, head + stall + tail, head + tail,
|
head += "stalled " + p.doing + ", "
|
||||||
} {
|
extra = ""
|
||||||
if len(text) <= room {
|
|
||||||
return text, true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kept := head + tail
|
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
|
return kept[:max(room, 0)], false
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-28
@@ -167,9 +167,9 @@ func TestProgressLine(t *testing.T) {
|
|||||||
|
|
||||||
// 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, and fifty into a phase with no file read for
|
// seconds into the phase, and fifty into a phase with no file read for
|
||||||
// the last 42: it leaves out the rate, or what the phase waits for,
|
// the last 42: it leaves out the rate, or what the phase waits for, but
|
||||||
// then what it says of the stall, and is cut only when what is left is
|
// not what a stalled phase is doing, and is cut only when what is left
|
||||||
// still too wide.
|
// is still too wide.
|
||||||
func TestLineFitsRoom(t *testing.T) {
|
func TestLineFitsRoom(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -182,13 +182,13 @@ func TestLineFitsRoom(t *testing.T) {
|
|||||||
}
|
}
|
||||||
content := &progress{
|
content := &progress{
|
||||||
label: scanPhase("content"), total: -1,
|
label: scanPhase("content"), total: -1,
|
||||||
waitingFor: searchingDatabase, start: ago, rateStart: ago,
|
waitingFor: "searching the database", start: ago, rateStart: ago,
|
||||||
}
|
}
|
||||||
|
|
||||||
started := now.Add(-50 * time.Second)
|
started := now.Add(-50 * time.Second)
|
||||||
stalled := &progress{
|
stalled := &progress{
|
||||||
label: scanPhase("hash"), unit: scanUnit, total: 100, count: 20,
|
label: scanPhase("hash"), unit: scanUnit, total: 100, count: 20,
|
||||||
doing: readingFiles, start: started, rateStart: started,
|
doing: reading, start: started, rateStart: started,
|
||||||
lastItem: now.Add(-42 * time.Second),
|
lastItem: now.Add(-42 * time.Second),
|
||||||
drawn: []drawnCount{{at: now.Add(-rateWindow), count: 20}},
|
drawn: []drawnCount{{at: now.Add(-rateWindow), count: 20}},
|
||||||
}
|
}
|
||||||
@@ -221,19 +221,15 @@ func TestLineFitsRoom(t *testing.T) {
|
|||||||
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,
|
p: stalled, room: 65, percent: true,
|
||||||
want: "[3/5] hash: [20/100] 20%, 0 files/s, no files finished for " +
|
want: "[3/5] hash: [20/100] 20%, stalled reading, elapsed 50s, " +
|
||||||
"42s, reading files, elapsed 50s, eta 3m20s",
|
"eta 3m20s",
|
||||||
fits: true,
|
fits: true,
|
||||||
}, {
|
}, {
|
||||||
p: stalled, room: 100, percent: true,
|
p: stalled, room: 64, percent: true,
|
||||||
want: "[3/5] hash: [20/100] 20%, no files finished for 42s, " +
|
want: "[3/5] hash: [20/100] 20%, stalled reading, elapsed 50s, " +
|
||||||
"reading files, elapsed 50s, eta 3m20s",
|
"eta 3m20",
|
||||||
fits: true,
|
fits: false,
|
||||||
}, {
|
|
||||||
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 {
|
||||||
@@ -255,7 +251,7 @@ func TestSetTotalStartsRate(t *testing.T) {
|
|||||||
waited := clock.Now().Add(-20 * time.Second)
|
waited := clock.Now().Add(-20 * time.Second)
|
||||||
p := &progress{
|
p := &progress{
|
||||||
now: clock.Now, label: scanPhase("load"), unit: scanUnit, total: -1,
|
now: clock.Now, label: scanPhase("load"), unit: scanUnit, total: -1,
|
||||||
doing: readingDatabase, start: waited, rateStart: waited,
|
doing: reading, start: waited, rateStart: waited,
|
||||||
lastItem: waited, drawn: []drawnCount{{at: waited}},
|
lastItem: waited, drawn: []drawnCount{{at: waited}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +260,8 @@ func TestSetTotalStartsRate(t *testing.T) {
|
|||||||
// Ten seconds after the total came, no record has been read yet.
|
// Ten seconds after the total came, no record has been read yet.
|
||||||
clock.Add(10 * time.Second)
|
clock.Add(10 * time.Second)
|
||||||
|
|
||||||
want := "[1/5] load: [0/100] 0%, 0 files/s, no files finished for 10s, " +
|
want := "[1/5] load: [0/100] 0%, stalled reading, elapsed 30s, " +
|
||||||
"reading the database, elapsed 30s, eta unknown"
|
"eta unknown"
|
||||||
if got, _ := p.line(clock.Now(), math.MaxInt, true); got != want {
|
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)
|
||||||
}
|
}
|
||||||
@@ -347,8 +343,8 @@ func TestPlainProgressPrintsWhileWaiting(t *testing.T) {
|
|||||||
// coming, as when one large file is being read or a disk is slow, goes
|
// 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
|
// 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
|
// 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
|
// line says the phase has stalled, and what it is doing, in place of
|
||||||
// display's clock by hand.
|
// the rate. The test moves the display's clock by hand.
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
func TestPlainProgressStalledProducer(t *testing.T) {
|
func TestPlainProgressStalledProducer(t *testing.T) {
|
||||||
@@ -356,7 +352,7 @@ func TestPlainProgressStalledProducer(t *testing.T) {
|
|||||||
clock := &testClock{now: time.Now()}
|
clock := &testClock{now: time.Now()}
|
||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: scanPhase("hash"), unit: scanUnit, total: 100,
|
label: scanPhase("hash"), unit: scanUnit, total: 100,
|
||||||
doing: readingFiles,
|
doing: reading,
|
||||||
}, nil, testRedraw, clock.Now)
|
}, nil, testRedraw, clock.Now)
|
||||||
|
|
||||||
// Pass or fail, the display ends before stderr is put back.
|
// Pass or fail, the display ends before stderr is put back.
|
||||||
@@ -377,8 +373,8 @@ func TestPlainProgressStalledProducer(t *testing.T) {
|
|||||||
|
|
||||||
clock.Add(4 * time.Second)
|
clock.Add(4 * time.Second)
|
||||||
awaitOutput(t, stderr,
|
awaitOutput(t, stderr,
|
||||||
"[3/5] hash: [20/100] 20%, 0 files/s, no files finished for 10s, "+
|
"[3/5] hash: [20/100] 20%, stalled reading, elapsed 10s, eta 40s\n",
|
||||||
"reading files, elapsed 10s, eta 40s\n", 3)
|
3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPlainProgressRateAfterStall draws a phase's plain line by hand, a
|
// TestPlainProgressRateAfterStall draws a phase's plain line by hand, a
|
||||||
@@ -395,7 +391,7 @@ func TestPlainProgressRateAfterStall(t *testing.T) {
|
|||||||
// No timed draws: the test draws.
|
// No timed draws: the test draws.
|
||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
||||||
doing: readingFiles,
|
doing: reading,
|
||||||
}, nil, time.Hour, clock.Now)
|
}, nil, time.Hour, clock.Now)
|
||||||
|
|
||||||
late := plainInterval + time.Millisecond
|
late := plainInterval + time.Millisecond
|
||||||
@@ -415,8 +411,7 @@ func TestPlainProgressRateAfterStall(t *testing.T) {
|
|||||||
want := []string{
|
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 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, elapsed 5s, eta unknown",
|
||||||
"[3/5] hash: [0/1000] 0%, 0 files/s, no files finished for 10s, " +
|
"[3/5] hash: [0/1000] 0%, stalled reading, elapsed 10s, eta unknown",
|
||||||
"reading files, elapsed 10s, eta unknown",
|
|
||||||
"[3/5] hash: [100/1000] 10%, 20 files/s, elapsed 15s, eta 2m15s",
|
"[3/5] hash: [100/1000] 10%, 20 files/s, elapsed 15s, eta 2m15s",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +436,7 @@ func TestProgressRateAfterStallRedrawn(t *testing.T) {
|
|||||||
// No timed draws: the test draws.
|
// No timed draws: the test draws.
|
||||||
p := startProgress(progressParams{
|
p := startProgress(progressParams{
|
||||||
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
label: scanPhase("hash"), unit: scanUnit, total: 1000,
|
||||||
doing: readingFiles,
|
doing: reading,
|
||||||
}, nil, time.Hour, clock.Now)
|
}, nil, time.Hour, clock.Now)
|
||||||
|
|
||||||
// redraw draws the display every redrawInterval for d.
|
// redraw draws the display every redrawInterval for d.
|
||||||
@@ -604,6 +599,60 @@ func TestTerminalLinesKeepETA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestTerminalStalledLineKeepsWords draws a stalled phase on an
|
||||||
|
// 80-column terminal at an ordinary scan's counts and times, 12345 of
|
||||||
|
// 45678 files read four and a half minutes in, the last 42 seconds ago,
|
||||||
|
// and report's read phase at the same counts: the bar gives way, and
|
||||||
|
// what the phase is doing shows whole in place of the rate, beside the
|
||||||
|
// phase, progress, elapsed time and ETA.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
||||||
|
func TestTerminalStalledLineKeepsWords(t *testing.T) {
|
||||||
|
stderr := captureStderr(t)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
params progressParams
|
||||||
|
want string
|
||||||
|
}{{
|
||||||
|
params: progressParams{
|
||||||
|
label: scanPhase("hash"), unit: scanUnit, total: 45678,
|
||||||
|
doing: reading,
|
||||||
|
},
|
||||||
|
want: "[3/5] hash: [12345/45678] 27%, stalled reading, " +
|
||||||
|
"elapsed 4m30s, eta 12m9s",
|
||||||
|
}, {
|
||||||
|
params: progressParams{
|
||||||
|
label: reportPhase("read"), total: 45678, doing: "read/write",
|
||||||
|
},
|
||||||
|
want: "[2/2] read: [12345/45678] 27%, stalled read/write, " +
|
||||||
|
"elapsed 4m30s, eta 12m9s",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
before := len(stderr())
|
||||||
|
clock := &testClock{now: time.Now()}
|
||||||
|
|
||||||
|
// No timed draws: the display is drawn as it starts and as it
|
||||||
|
// ends.
|
||||||
|
p := startProgress(tc.params, func() int { return 80 }, time.Hour,
|
||||||
|
clock.Now)
|
||||||
|
|
||||||
|
clock.Add(4*time.Minute + 30*time.Second - 42*time.Second)
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
p.count, p.lastItem = 12345, clock.Now()
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
clock.Add(42 * time.Second)
|
||||||
|
p.finish()
|
||||||
|
|
||||||
|
shown := lastFrame(stderr()[before:])
|
||||||
|
if !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 draws a large scan's bar display and a small
|
||||||
// scan's, two and a half hours into the phase, on terminals from 120
|
// scan's, two and a half hours into the phase, on terminals from 120
|
||||||
// columns down to 40, and checks that every line stays clear of the
|
// columns down to 40, and checks that every line stays clear of the
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ func runReport(ctx context.Context, stdout io.Writer) error {
|
|||||||
|
|
||||||
defer func() { _ = db.Close() }()
|
defer func() { _ = db.Close() }()
|
||||||
|
|
||||||
|
// The read phase writes the report while it reads the records, so a
|
||||||
|
// stall may be in either.
|
||||||
prog = newProgress(progressParams{
|
prog = newProgress(progressParams{
|
||||||
label: reportPhase("read"), unit: "records", total: -1,
|
label: reportPhase("read"), unit: "records", total: -1,
|
||||||
waitingFor: countingRecords, doing: readingDatabase,
|
waitingFor: countingRecords, doing: "read/write",
|
||||||
})
|
})
|
||||||
report, err := readReport(ctx, db, dbPath, stdout, prog)
|
report, err := readReport(ctx, db, dbPath, stdout, prog)
|
||||||
|
|
||||||
|
|||||||
@@ -88,15 +88,6 @@ 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.
|
||||||
@@ -455,7 +446,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, doing: readingDatabase,
|
waitingFor: countingRecords, doing: reading,
|
||||||
})
|
})
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
|
|
||||||
@@ -506,7 +497,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, doing: "listing directories",
|
estimate: true, doing: "listing",
|
||||||
})
|
})
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -698,7 +689,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,
|
doing: reading,
|
||||||
})
|
})
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
|
|
||||||
@@ -788,7 +779,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",
|
doing: "writing",
|
||||||
})
|
})
|
||||||
|
|
||||||
defer prog.finish()
|
defer prog.finish()
|
||||||
@@ -877,7 +868,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: searchingDatabase, doing: searchingDatabase,
|
waitingFor: "searching the database", doing: "searching",
|
||||||
})
|
})
|
||||||
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, doing: readingDatabase,
|
waitingFor: countingRecords, doing: reading,
|
||||||
})
|
})
|
||||||
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)), doing: "grouping directories",
|
total: int64(len(allDirs)), doing: "grouping",
|
||||||
})
|
})
|
||||||
dupes := collectTreeGroups(allDirs, super, prog)
|
dupes := collectTreeGroups(allDirs, super, prog)
|
||||||
|
|
||||||
@@ -92,7 +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",
|
doing: "writing",
|
||||||
})
|
})
|
||||||
dupeTrees, reclaimable, err := writeTreeGroups(stdout, dupes, prog)
|
dupeTrees, reclaimable, err := writeTreeGroups(stdout, dupes, prog)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user