// SPDX-License-Identifier: GPL-3.0-or-later package main import ( "io" "strings" "testing" "git.labunix.xyz/krino/internal/engine" "git.labunix.xyz/krino/internal/plan" "git.labunix.xyz/krino/internal/scan" ) func TestPaletteZeroValueIsPlain(t *testing.T) { var p palette for _, got := range []string{p.bold("x"), p.faint("x"), p.warn("x"), p.rule("x"), p.alarm("x"), p.good("x"), p.bad("x")} { if got != "x" { t.Errorf("plain palette styled %q", got) } } if got := p.keys("[a] apply all [q] quit"); got != "[a] apply all [q] quit" { t.Errorf("plain keys = %q", got) } } func TestPaletteStylesWithAnsiSlotsOnly(t *testing.T) { p := palette{on: true} for _, tt := range []struct{ got, want string }{ {p.bold("x"), "\x1b[1mx\x1b[0m"}, {p.faint("x"), "\x1b[2mx\x1b[0m"}, {p.bad("x"), "\x1b[31mx\x1b[0m"}, {p.good("x"), "\x1b[32mx\x1b[0m"}, {p.warn("x"), "\x1b[33mx\x1b[0m"}, {p.rule("x"), "\x1b[34mx\x1b[0m"}, {p.alarm("x"), "\x1b[1;31mx\x1b[0m"}, {p.bold(""), ""}, {p.keys("[a] apply all [q] quit"), "\x1b[1m[a]\x1b[0m apply all \x1b[1m[q]\x1b[0m quit"}, } { if tt.got != tt.want { t.Errorf("got %q, want %q", tt.got, tt.want) } } } func TestColourOnHonoursNoColorFlag(t *testing.T) { old := colourPolicy t.Cleanup(func() { colourPolicy = old }) colourPolicy = func(io.Writer) bool { return true } if !colourOn(&globals{}, io.Discard) { t.Error("colour off on a terminal without --no-color") } if colourOn(&globals{noColor: true}, io.Discard) { t.Error("colour on despite --no-color") } } // TestNoColorFlagBeforeAndAfterSubcommand: --no-color parses in both // positions, and survives a subcommand registering its own flags. func TestNoColorFlagBeforeAndAfterSubcommand(t *testing.T) { home(t) if code, _, errOut := runCLI(t, "init"); code != 0 { t.Fatal(errOut) } for _, args := range [][]string{{"--no-color", "check"}, {"check", "--no-color"}} { if code, _, errOut := runCLI(t, args...); code != 0 { t.Errorf("%v: exit %d: %s", args, code, errOut) } } if !strings.Contains(usage, "--no-color") { t.Error("usage does not mention --no-color") } } // TestPlanBlocksColouredAndAligned: with colour on, each element of a // block carries its style, a wrapped value keeps its style on every piece, // and stripping the escapes gives exactly the plain rendering, so the // labels and the wrapping line up with colour on. func TestPlanBlocksColouredAndAligned(t *testing.T) { home(t) dp := &engine.DirPlan{ Dir: &engine.Dir{Name: "dl", Root: "/r"}, Chains: []plan.Chain{ {File: scan.File{Rel: "a.pdf"}, Steps: []plan.Step{ {Kind: plan.Move, Rule: "acme", Src: "/r/a.pdf", Dst: "/r/Work/a.pdf", Reason: "type pdf"}, {Kind: plan.Trash, Rule: "old", Src: "/r/Work/a.pdf", Skip: "a duplicate is never deleted", Reason: "matched"}, }}, {File: scan.File{Rel: "setup.deb"}, Steps: []plan.Step{ {Kind: plan.DeletePermanent, Rule: "pkgs", Src: "/r/setup.deb", Reason: "age > 90d"}, }}, }, Result: &engine.Result{Matched: []engine.FileMatch{{File: scan.File{Rel: "a.pdf"}}, {File: scan.File{Rel: "setup.deb"}}}}, } var plain, coloured strings.Builder printPlan(&plain, dp, false, palette{}, 30) printPlan(&coloured, dp, false, palette{on: true}, 30) for _, want := range []string{ "\x1b[32mmove\x1b[0m", "\x1b[34macme\x1b[0m", "\x1b[2mtype pdf\x1b[0m", "\x1b[2mtrash\x1b[0m", "\x1b[2mskipped: ", "\x1b[1;31mDELETE permanently\x1b[0m", } { if !strings.Contains(coloured.String(), want) { t.Errorf("coloured plan lacks %q:\n%q", want, coloured.String()) } } if got := stripSGR(coloured.String()); got != plain.String() { t.Errorf("stripped coloured plan differs from plain:\n%s\nvs\n%s", got, plain.String()) } } func TestUndoAndLogColoured(t *testing.T) { p := palette{on: true} if got := colourRefused(" 1 x refused: gone\n", p); !strings.Contains(got, "\x1b[1;31mrefused:\x1b[0m") { t.Errorf("refused not bold red: %q", got) } if got := colourRefused(" 1 x refused: gone\n", palette{}); got != " 1 x refused: gone\n" { t.Errorf("plain palette changed the undo plan: %q", got) } if got := styleUndone("20260914T101203-ab12 2026-09-14 10:12 dl 3 moved (undone)", p); !strings.HasSuffix(got, " \x1b[2m(undone)\x1b[0m") { t.Errorf("(undone) not faint: %q", got) } if got := styleUndone("20260914T101203-ab12 2026-09-14 10:12 dl 3 moved", p); strings.Contains(got, "\x1b[") { t.Errorf("a run that is not undone was styled: %q", got) } } // stripSGR removes every ESC [ ... m sequence. func stripSGR(s string) string { var b strings.Builder for i := 0; i < len(s); i++ { if s[i] == 0x1b && i+1 < len(s) && s[i+1] == '[' { if j := strings.IndexByte(s[i:], 'm'); j > 0 { i += j continue } } b.WriteByte(s[i]) } return b.String() } // TestDryRunColouredAndNoColor drives the CLI with colour forced on: the // header is bold and the warnings heading yellow; --no-color in either // position gives no escape at all. NO_COLOR is tested in internal/tui, // whose Colour colourPolicy is. func TestDryRunColouredAndNoColor(t *testing.T) { matchingFixture(t) old := colourPolicy t.Cleanup(func() { colourPolicy = old }) colourPolicy = func(io.Writer) bool { return true } _, out, _ := runCLI(t, "-n") if !strings.Contains(out, "\x1b[1mkrino: dl ~/dl\x1b[0m") || !strings.Contains(out, "\x1b[33mwarnings\x1b[0m") { t.Errorf("coloured dry run lacks bold header or yellow warnings:\n%q", out) } for _, args := range [][]string{{"--no-color", "-n"}, {"-n", "--no-color"}} { _, out, _ := runCLI(t, args...) if strings.Contains(out, "\x1b[") { t.Errorf("%v printed an escape:\n%q", args, out) } } } // TestNoPagerFlagBypassesThePager: with -P or --no-pager, in either // position, the plan is written straight out and never handed to the // pager; without it, the pager gets it. func TestNoPagerFlagBypassesThePager(t *testing.T) { matchingFixture(t) old := pager t.Cleanup(func() { pager = old }) paged := 0 pager = func(w io.Writer, text string) error { paged++ _, err := io.WriteString(w, text) return err } if _, out, _ := runCLI(t, "-n"); paged != 1 || !strings.Contains(out, "report (1).pdf") { t.Errorf("without -P: paged %d times, want 1", paged) } for _, args := range [][]string{{"-P", "-n"}, {"--no-pager", "-n"}, {"-n", "-P"}} { paged = 0 _, out, errOut := runCLI(t, args...) if paged != 0 { t.Errorf("%v went through the pager", args) } if !strings.Contains(out, "report (1).pdf") { t.Errorf("%v printed no plan:\n%s\n%s", args, out, errOut) } } if code, _, errOut := runCLI(t, "log", "-P"); code != 0 { t.Errorf("log -P: exit %d: %s", code, errOut) } if !strings.Contains(usage, "--no-pager") { t.Error("usage does not mention --no-pager") } }