All user-facing output now goes through a single ui.Writer with a
uniform style:
》 (white) for begin / info / notice
》 (green) for complete / success
Warning: for warnings (orange)
ERROR: for errors (red)
》 (indented) for progress heartbeats
Color is enabled when stdout is a TTY and NO_COLOR is unset.
Standards:
- Complete-sentence messages with fully qualified terms ("backup
destination store", "local index database", "snapshot source
files enumeration").
- Every Complete has a matching Begin.
- Natural verb tense conveys state ("Uploading" -> "Uploaded"). The
words "begin"/"complete" never appear in message bodies; the marker
color carries that information.
- ETA means clock time, not duration. Progress lines say "estimated
remaining time (<dur>), finish at <time>" with both labeled.
Adds globals.CommitDate (populated by Makefile/Dockerfile/goreleaser
via ldflags from `git show -s --format=%cI HEAD`) and a startup banner
printed once per invocation.
Strips fx call-chain noise from startup errors so users see the actual
underlying error (e.g. "creating base path: mkdir /Volumes/BACKUPS:
permission denied" instead of three layers of "could not build
arguments for function ...").
README documents the output style and the ui package conventions.
87 lines
2.3 KiB
Go
87 lines
2.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTestWriter(color bool) (*Writer, *bytes.Buffer) {
|
|
buf := &bytes.Buffer{}
|
|
return NewWithColor(buf, color), buf
|
|
}
|
|
|
|
func TestMessageMethodsPlain(t *testing.T) {
|
|
tests := []struct {
|
|
method string
|
|
fn func(*Writer)
|
|
want string
|
|
}{
|
|
{"Begin", func(w *Writer) { w.Begin("starting %s", "thing") }, "》 starting thing\n"},
|
|
{"Complete", func(w *Writer) { w.Complete("done %s", "thing") }, "》 done thing\n"},
|
|
{"Info", func(w *Writer) { w.Info("status") }, "》 status\n"},
|
|
{"Notice", func(w *Writer) { w.Notice("note") }, "》 note\n"},
|
|
{"Warning", func(w *Writer) { w.Warning("oops") }, "Warning: oops\n"},
|
|
{"Error", func(w *Writer) { w.Error("boom") }, "ERROR: boom\n"},
|
|
{"Progress", func(w *Writer) { w.Progress("p") }, " 》 p\n"},
|
|
{"Banner", func(w *Writer) { w.Banner("hello") }, "hello\n"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.method, func(t *testing.T) {
|
|
w, buf := newTestWriter(false)
|
|
tt.fn(w)
|
|
if got := buf.String(); got != tt.want {
|
|
t.Errorf("got %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestColorOutputContainsANSI(t *testing.T) {
|
|
w, buf := newTestWriter(true)
|
|
w.Error("boom")
|
|
out := buf.String()
|
|
if !strings.Contains(out, "\033[") {
|
|
t.Errorf("expected ANSI escapes in color output, got %q", out)
|
|
}
|
|
if !strings.Contains(out, "ERROR: ") {
|
|
t.Errorf("expected 'ERROR: ' text in output, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestValueFormattersPlain(t *testing.T) {
|
|
w, _ := newTestWriter(false)
|
|
|
|
if got := w.Hex("0123456789abcdef0123"); got != "0123456789ab..." {
|
|
t.Errorf("Hex long: got %q", got)
|
|
}
|
|
if got := w.Hex("short"); got != "short" {
|
|
t.Errorf("Hex short: got %q", got)
|
|
}
|
|
if got := w.Size(1024); got != "1.0 kB" {
|
|
t.Errorf("Size: got %q", got)
|
|
}
|
|
if got := w.Duration(90 * time.Second); got != "1m30s" {
|
|
t.Errorf("Duration: got %q", got)
|
|
}
|
|
if got := w.Count(12345); got != "12,345" {
|
|
t.Errorf("Count: got %q", got)
|
|
}
|
|
if got := w.Percent(12.34); got != "12.3%" {
|
|
t.Errorf("Percent: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestValueFormattersColored(t *testing.T) {
|
|
w, _ := newTestWriter(true)
|
|
hex := w.Hex("0123456789abcdef0123")
|
|
if !strings.Contains(hex, "\033[") {
|
|
t.Errorf("expected ANSI in colored Hex output, got %q", hex)
|
|
}
|
|
if !strings.Contains(hex, "0123456789ab") {
|
|
t.Errorf("expected hex content in output, got %q", hex)
|
|
}
|
|
}
|