check / check (push) Successful in 1m25s
On a pipe, a file or /dev/null each scan phase now prints its zero-state line the moment it starts instead of after its first finished item, so a slow first read under cron no longer leaves the log silent. Terminal detection uses golang.org/x/term, now a direct dependency listed in the README; the old character-device test took /dev/null for a terminal. The walk spinner no longer redraws from the library's own goroutine; it advances only as items complete, so a redraw cannot garble a warning. Building the bar moved into newBar so the terminal display can be tested without a terminal. Lint suppressed: gosec on two test file opens, paralleltest on three tests that replace os.Stderr. Model: opus-5-5
151 lines
3.4 KiB
Go
151 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// spinnerIdle outlasts the 100ms interval at which the progressbar
|
|
// library redraws a spinner from its own goroutine unless told not to.
|
|
const spinnerIdle = 300 * time.Millisecond
|
|
|
|
// captureStderr points os.Stderr at a file for the rest of the test and
|
|
// returns a function reading back everything written to it.
|
|
func captureStderr(t *testing.T) func() string {
|
|
t.Helper()
|
|
|
|
path := filepath.Join(t.TempDir(), "stderr")
|
|
|
|
f, err := os.Create(path) //nolint:gosec // test-controlled path
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
saved := os.Stderr
|
|
os.Stderr = f
|
|
|
|
t.Cleanup(func() {
|
|
os.Stderr = saved
|
|
|
|
_ = f.Close()
|
|
})
|
|
|
|
return func() string {
|
|
b, err := os.ReadFile(path) //nolint:gosec // test-controlled path
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return string(b)
|
|
}
|
|
}
|
|
|
|
//nolint:paralleltest // replaces the process-wide os.Stderr
|
|
func TestStderrIsTTYFalseForNonTerminals(t *testing.T) {
|
|
r, pipe, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
regular, err := os.Create(filepath.Join(t.TempDir(), "stderr"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
saved := os.Stderr
|
|
|
|
t.Cleanup(func() {
|
|
os.Stderr = saved
|
|
|
|
for _, f := range []*os.File{r, pipe, regular, devNull} {
|
|
_ = f.Close()
|
|
}
|
|
})
|
|
|
|
cases := map[string]*os.File{
|
|
"a pipe": pipe,
|
|
"a regular file": regular,
|
|
os.DevNull: devNull,
|
|
}
|
|
|
|
for name, f := range cases {
|
|
os.Stderr = f
|
|
|
|
if stderrIsTTY() {
|
|
t.Errorf("stderrIsTTY() = true with stderr on %s", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestNewProgressPrintsBeforeFirstItem checks that each pass shows its
|
|
// zero state the moment it starts when stderr is not a terminal, and
|
|
// that the next line still waits for plainInterval.
|
|
//
|
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
|
func TestNewProgressPrintsBeforeFirstItem(t *testing.T) {
|
|
stderr := captureStderr(t)
|
|
|
|
newProgress("walk", -1).increment()
|
|
newProgress("hash", 10).increment()
|
|
|
|
want := "walk: 0 files, elapsed 0s\n" +
|
|
"hash: [0/10] 0% 0 files/s elapsed 0s eta ?\n"
|
|
if got := stderr(); got != want {
|
|
t.Errorf("stderr = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// TestProgressWarningsOnOwnLines drives the terminal display of the walk
|
|
// pass: every warning must land on a line of its own between redraws of
|
|
// the spinner, and nothing may redraw while no method is being called,
|
|
// or a warning could be garbled by a redraw from another goroutine.
|
|
//
|
|
//nolint:paralleltest // captureStderr replaces the process-wide os.Stderr
|
|
func TestProgressWarningsOnOwnLines(t *testing.T) {
|
|
stderr := captureStderr(t)
|
|
|
|
p := &progress{
|
|
label: "walk", total: -1, start: time.Now(),
|
|
bar: newBar("walk", -1),
|
|
}
|
|
|
|
for _, w := range []string{"first", "second", "third"} {
|
|
p.increment()
|
|
p.warnf("warning %s", w)
|
|
}
|
|
|
|
p.increment()
|
|
|
|
before := stderr()
|
|
|
|
time.Sleep(spinnerIdle)
|
|
|
|
if after := stderr(); after != before {
|
|
t.Errorf("stderr written while idle: %q", after[len(before):])
|
|
}
|
|
|
|
// A terminal shows each line as the text after its last carriage
|
|
// return.
|
|
var warnings []string
|
|
|
|
for line := range strings.SplitSeq(before, "\n") {
|
|
if strings.Contains(line, "warning") {
|
|
warnings = append(warnings, line[strings.LastIndex(line, "\r")+1:])
|
|
}
|
|
}
|
|
|
|
want := []string{"warning first", "warning second", "warning third"}
|
|
if !slices.Equal(warnings, want) {
|
|
t.Errorf("warning lines = %q, want %q", warnings, want)
|
|
}
|
|
}
|