a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
198 lines
5.8 KiB
Go
198 lines
5.8 KiB
Go
package progress
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSpinnerDisabledModePrintsPlainText(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
sp := NewSpinner(&buf)
|
|
sp.Disable()
|
|
|
|
sp.Start("Indexing repository")
|
|
sp.Report("walking files", 0, 0)
|
|
sp.Report("walking files", 50, 100) // same stage, must not re-emit
|
|
sp.Report("parsing", 0, 0) // new stage: finishes the previous, starts its own
|
|
sp.Done()
|
|
|
|
out := buf.String()
|
|
wants := []string{
|
|
"Indexing repository",
|
|
"walking files ...", // stage start
|
|
"✓ walking files · 50", // stage finish with final counter
|
|
"parsing ...", // next stage start
|
|
"✓ parsing", // finished by Done
|
|
"✓ Indexing repository", // overall summary
|
|
}
|
|
for _, w := range wants {
|
|
if !strings.Contains(out, w) {
|
|
t.Errorf("output missing %q\n--- got ---\n%s", w, out)
|
|
}
|
|
}
|
|
// Each stage prints exactly twice — one start line, one finish line —
|
|
// regardless of how many progress ticks arrive in between.
|
|
if got := strings.Count(out, "walking files"); got != 2 {
|
|
t.Errorf("expected stage 'walking files' to print exactly twice (start+finish), got %d\n--- got ---\n%s", got, out)
|
|
}
|
|
if idx := strings.Index(out, "parsing ..."); idx < strings.Index(out, "✓ walking files") {
|
|
t.Errorf("stage finish line must precede the next stage start\n--- got ---\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestSpinnerDisabledFailPrintsErrorLine(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
sp := NewSpinner(&buf)
|
|
sp.Disable()
|
|
|
|
sp.Start("Stamping blame")
|
|
sp.Fail(errors.New("boom"))
|
|
|
|
out := buf.String()
|
|
if !strings.Contains(out, "✗ Stamping blame") {
|
|
t.Errorf("expected ✗ summary, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "boom") {
|
|
t.Errorf("expected error message in output, got: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestSpinnerDoneIsIdempotent(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
sp := NewSpinner(&buf)
|
|
sp.Disable()
|
|
|
|
sp.Start("Indexing")
|
|
sp.Done()
|
|
sp.Done() // must not double-print or panic
|
|
sp.Fail(errors.New("late")) // must be a no-op after Done
|
|
|
|
if got := strings.Count(buf.String(), "✓"); got != 1 {
|
|
t.Errorf("expected exactly one ✓, got %d in:\n%s", got, buf.String())
|
|
}
|
|
if strings.Contains(buf.String(), "✗") {
|
|
t.Errorf("expected no ✗ after successful Done, got: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestMultiFansOutAndSkipsNil(t *testing.T) {
|
|
a := &countingReporter{}
|
|
b := &countingReporter{}
|
|
r := Multi(a, nil, b)
|
|
|
|
r.Report("walk", 1, 10)
|
|
r.Report("parse", 0, 0)
|
|
|
|
if a.calls != 2 || b.calls != 2 {
|
|
t.Errorf("expected 2 calls each, got a=%d b=%d", a.calls, b.calls)
|
|
}
|
|
}
|
|
|
|
func TestMultiCollapsesToSingle(t *testing.T) {
|
|
a := &countingReporter{}
|
|
r := Multi(nil, a, nil)
|
|
if r != a {
|
|
t.Errorf("expected Multi with one non-nil to return that reporter directly")
|
|
}
|
|
}
|
|
|
|
func TestMultiAllNilReturnsNop(t *testing.T) {
|
|
r := Multi(nil, nil)
|
|
if _, ok := r.(Nop); !ok {
|
|
t.Errorf("expected Nop when all inputs nil, got %T", r)
|
|
}
|
|
}
|
|
|
|
type countingReporter struct{ calls int }
|
|
|
|
func (c *countingReporter) Report(string, int, int) { c.calls++ }
|
|
|
|
// TestASCIIGlyphFallbackOnOEMCodepage proves the F8 contract: a terminal that
|
|
// cannot render UTF-8 (a legacy OEM codepage, a linux virtual console, or an
|
|
// explicit GORTEX_ASCII) gets an ASCII glyph set for the spinner finish
|
|
// markers AND the box-drawing border — not just the check/cross glyphs.
|
|
func TestASCIIGlyphFallbackOnOEMCodepage(t *testing.T) {
|
|
t.Run("ascii_override_selects_ascii_set", func(t *testing.T) {
|
|
t.Setenv("GORTEX_UNICODE", "")
|
|
t.Setenv("GORTEX_ASCII", "1")
|
|
if supportsUnicode() {
|
|
t.Fatal("GORTEX_ASCII=1 must disable unicode")
|
|
}
|
|
g := activeGlyphs()
|
|
if g.OK != "+" || g.Fail != "x" {
|
|
t.Errorf("ascii markers = %q/%q, want +/x", g.OK, g.Fail)
|
|
}
|
|
// The border charset is ASCII too — the beat-codegraph axis.
|
|
if g.Border.TopLeft != "+" {
|
|
t.Errorf("ascii border TopLeft = %q, want +", g.Border.TopLeft)
|
|
}
|
|
})
|
|
|
|
t.Run("unicode_override_wins_over_term_linux", func(t *testing.T) {
|
|
t.Setenv("GORTEX_ASCII", "")
|
|
t.Setenv("TERM", "linux")
|
|
t.Setenv("GORTEX_UNICODE", "1")
|
|
if !supportsUnicode() {
|
|
t.Fatal("GORTEX_UNICODE=1 must enable unicode even on TERM=linux")
|
|
}
|
|
if activeGlyphs().OK != "✓" {
|
|
t.Error("unicode set must use ✓")
|
|
}
|
|
})
|
|
|
|
t.Run("term_linux_falls_back_to_ascii", func(t *testing.T) {
|
|
t.Setenv("GORTEX_ASCII", "")
|
|
t.Setenv("GORTEX_UNICODE", "")
|
|
t.Setenv("TERM", "linux")
|
|
if supportsUnicode() {
|
|
t.Fatal("TERM=linux must fall back to ASCII")
|
|
}
|
|
})
|
|
|
|
t.Run("spinner_done_uses_ascii_marker", func(t *testing.T) {
|
|
t.Setenv("GORTEX_UNICODE", "")
|
|
t.Setenv("GORTEX_ASCII", "1")
|
|
var buf bytes.Buffer
|
|
sp := NewSpinner(&buf) // not a TTY → disabled → direct Fprintf path
|
|
sp.Start("Indexing")
|
|
sp.Done()
|
|
out := buf.String()
|
|
if !strings.Contains(out, "+ Indexing") {
|
|
t.Errorf("done line should use the ASCII marker; got %q", out)
|
|
}
|
|
if strings.Contains(out, "✓") {
|
|
t.Errorf("done line must not contain the unicode check; got %q", out)
|
|
}
|
|
})
|
|
|
|
t.Run("spinner_fail_uses_ascii_cross", func(t *testing.T) {
|
|
t.Setenv("GORTEX_UNICODE", "")
|
|
t.Setenv("GORTEX_ASCII", "1")
|
|
var buf bytes.Buffer
|
|
sp := NewSpinner(&buf)
|
|
sp.Start("Indexing")
|
|
sp.Fail(errors.New("boom"))
|
|
out := buf.String()
|
|
if !strings.Contains(out, "x Indexing") {
|
|
t.Errorf("fail line should use the ASCII cross; got %q", out)
|
|
}
|
|
if strings.Contains(out, "✗") {
|
|
t.Errorf("fail line must not contain the unicode cross; got %q", out)
|
|
}
|
|
})
|
|
|
|
t.Run("card_border_is_ascii", func(t *testing.T) {
|
|
t.Setenv("GORTEX_UNICODE", "")
|
|
t.Setenv("GORTEX_ASCII", "1")
|
|
card := Card("", "hello")
|
|
if !strings.Contains(card, "+") {
|
|
t.Errorf("ascii card must use + corners; got %q", card)
|
|
}
|
|
if strings.ContainsAny(card, "╭╮╰╯─│") {
|
|
t.Errorf("ascii card must not use rounded box-drawing; got %q", card)
|
|
}
|
|
})
|
|
}
|