diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 13:26:51 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 13:26:51 +0200 |
| commit | 07c24054cab965800983ef40f53a05c2db131ede (patch) | |
| tree | 741db561d614b35f8f94fee04fb5ce663a629a0b /cmd/krino/render_test.go | |
| parent | 70dccf8d573028aaed64185acb8e134e5339afa3 (diff) | |
| download | krino-07c24054cab965800983ef40f53a05c2db131ede.tar.gz krino-07c24054cab965800983ef40f53a05c2db131ede.zip | |
krino: 0.0.3 — the plan as one block per file, wrapped, and -Pv0.0.3
Each file shows its steps, then the rule and the reason it matched, one
field per line; on a terminal every line wraps to its width with
continuation lines under their own column, and piped output is never
wrapped. Choosing per file shows the same block. -P / --no-pager prints
the plan without the pager. A duplicate's original is shown with ~.
Diffstat (limited to 'cmd/krino/render_test.go')
| -rw-r--r-- | cmd/krino/render_test.go | 144 |
1 files changed, 119 insertions, 25 deletions
diff --git a/cmd/krino/render_test.go b/cmd/krino/render_test.go index 6f29b51..9df75f4 100644 --- a/cmd/krino/render_test.go +++ b/cmd/krino/render_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" "time" + "unicode/utf8" "krino/internal/engine" "krino/internal/plan" @@ -17,9 +18,10 @@ import ( // TestPrintPlan is the golden render test of spec §8.2. The DirPlan is // built by hand, not by running a scan, so the expected output cannot -// drift with a fixture: it asserts the header row, a numbered row, a -// continuation line, the "DELETE permanently" capitalisation and the -// counts line. +// drift with a fixture: it asserts one block per file (the numbered name, +// each step, and each rule's name and reason after its steps), a file +// whose steps come from two rules, the "DELETE permanently" +// capitalisation and the counts line. Width 0 never wraps. // // The two destinations pin both branches of destText: scan001.pdf's and // fv_123.pdf's "acme" step lands inside root and renders root-relative @@ -90,16 +92,26 @@ func TestPrintPlan(t *testing.T) { } var buf bytes.Buffer - printPlan(&buf, dp, false, palette{}) + printPlan(&buf, dp, false, palette{}, 0) out := buf.String() for _, want := range []string{ "6 scanned · 3 to act on · 1 warnings · 0.42s\n", - " # file actions rule\n", - " 1 scan001.pdf move → Work/Acme/2026/ acme content \"acme ltd\"\n", - " 2 fv_123.pdf copy → ~/backup/invoices/2026/ backup content \"invoice\"\n", - " move → Work/Acme/2026/ acme name \\bacme\\b\n", - " 3 setup-1.2.deb DELETE permanently old-pkgs age 94d\n", + "\n 1 scan001.pdf\n" + + " move → Work/Acme/2026/\n" + + " rule acme\n" + + " because content \"acme ltd\"\n", + "\n 2 fv_123.pdf\n" + + " copy → ~/backup/invoices/2026/\n" + + " rule backup\n" + + " because content \"invoice\"\n" + + " move → Work/Acme/2026/\n" + + " rule acme\n" + + " because name \\bacme\\b\n", + "\n 3 setup-1.2.deb\n" + + " DELETE permanently\n" + + " rule old-pkgs\n" + + " because age 94d\n", "warnings\n scan001.pdf acme: content unreadable: needs pdftotext, not installed\n", "not acted on: 1 busy · 1 excluded · 1 unmatched (-v lists them)\n", } { @@ -108,7 +120,91 @@ func TestPrintPlan(t *testing.T) { } } if strings.Contains(out, "excluded.txt") { - t.Errorf("excluded.txt has no steps and must not appear in the table:\n%s", out) + t.Errorf("excluded.txt has no steps and must not appear in the plan:\n%s", out) + } + if strings.Contains(out, "# file") { + t.Errorf("the plan is blocks now, with no table header:\n%s", out) + } +} + +// TestPrintPlanOmitsBecauseForUnconditionalRule: a rule with no condition +// has nothing to say under "because", so the line is left out. +func TestPrintPlanOmitsBecauseForUnconditionalRule(t *testing.T) { + home(t) + dp := &engine.DirPlan{ + Dir: &engine.Dir{Name: "dl", Root: "/r"}, + Chains: []plan.Chain{{File: scan.File{Rel: "a.iso"}, Steps: []plan.Step{ + {Kind: plan.Move, Rule: "to-sort", Src: "/r/a.iso", Dst: "/r/TO_SORT/a.iso", Reason: "no condition"}, + }}}, + Result: &engine.Result{Matched: []engine.FileMatch{{File: scan.File{Rel: "a.iso"}}}}, + } + var buf bytes.Buffer + printPlan(&buf, dp, false, palette{}, 0) + want := "\n 1 a.iso\n move → TO_SORT/\n rule to-sort\n" + if out := buf.String(); !strings.Contains(out, want) || strings.Contains(out, "because") { + t.Errorf("output lacks %q or still says because:\n%s", want, out) + } +} + +// TestPrintPlanWrapsToWidth: given a width, every line fits it, and a long +// name or reason continues on lines indented under its own first column, +// so reading the pieces back in order gives the whole text. +func TestPrintPlanWrapsToWidth(t *testing.T) { + home(t) + name := "A Rather Long Book Title -- First Author & Second Author -- 2005.pdf" + reason := `content "acme ltd" "long street 12" "0000000000" "000000000"` + dp := &engine.DirPlan{ + Dir: &engine.Dir{Name: "dl", Root: "/r"}, + Chains: []plan.Chain{{File: scan.File{Rel: name}, Steps: []plan.Step{ + {Kind: plan.Move, Rule: "work-content", Src: "/r/" + name, Dst: "/r/work/Acme_main/accounting_acme/2026_08_acme/" + name, Reason: reason}, + }}}, + Result: &engine.Result{Matched: []engine.FileMatch{{File: scan.File{Rel: name}}}}, + } + var buf bytes.Buffer + printPlan(&buf, dp, false, palette{}, 40) + out := buf.String() + lines := strings.Split(out, "\n") + for _, l := range lines { + if n := utf8.RuneCountInString(l); n > 40 { + t.Errorf("line of %d runes exceeds width 40: %q", n, l) + } + } + + start := -1 + for i, l := range lines { + if strings.HasPrefix(l, " 1 ") { + start = i + break + } + } + if start < 0 { + t.Fatalf("no numbered line:\n%s", out) + } + gotName := strings.TrimPrefix(lines[start], " 1 ") + i := start + 1 + for ; i < len(lines) && strings.HasPrefix(lines[i], " ") && !strings.HasPrefix(lines[i], " move"); i++ { + gotName += strings.TrimPrefix(lines[i], " ") + } + if gotName != name { + t.Errorf("wrapped name reads %q, want %q\n%s", gotName, name, out) + } + + // The reason's value starts at column 5 + len("because") + 1 = 13, and so + // does every continuation line of it. + const valueCol = 13 + gotReason, inReason := "", false + for _, l := range lines[i:] { + switch { + case strings.HasPrefix(l, " because "): + gotReason, inReason = strings.TrimPrefix(l, " because "), true + case inReason && len(l) > valueCol && strings.TrimLeft(l[:valueCol], " ") == "" && l[valueCol] != ' ': + gotReason += l[valueCol:] + default: + inReason = false + } + } + if gotReason != reason { + t.Errorf("wrapped reason reads %q, want %q\n%s", gotReason, reason, out) } } @@ -138,7 +234,7 @@ func TestPrintPlanSkippedStep(t *testing.T) { }, } var buf bytes.Buffer - printPlan(&buf, dp, false, palette{}) + printPlan(&buf, dp, false, palette{}, 0) out := buf.String() if !strings.Contains(out, "copy skipped: target exists") { t.Errorf("skipped step should show its reason in place of the destination:\n%s", out) @@ -151,13 +247,12 @@ func TestPrintPlanSkippedStep(t *testing.T) { } } -// TestPrintPlanRowNumberAlignment pins the table layout plan 4's review UI -// inherits: with 11 acted-on files the row-number column has to widen past -// a single digit, and the row number is right-aligned so "#" stays flush. -// The fixture also carries a file name past the 40-character cap and a -// rule name noticeably longer than the rest, exercising the file and rule -// columns' own per-section widths at the same time. -func TestPrintPlanRowNumberAlignment(t *testing.T) { +// TestPrintPlanBlockNumberAlignment: with 11 acted-on files the number +// widens past a single digit, numbers are right-aligned, and every block's +// body is indented one step further so the labels line up across the whole +// plan. A long file name and a long rule name no longer push anything out +// of line: each sits on its own line. +func TestPrintPlanBlockNumberAlignment(t *testing.T) { h := home(t) root := filepath.Join(h, "dl") long := strings.Repeat("z", 42) + ".txt" // 46 runes: past the 40-column cap @@ -191,15 +286,14 @@ func TestPrintPlanRowNumberAlignment(t *testing.T) { } var buf bytes.Buffer - printPlan(&buf, dp, false, palette{}) + printPlan(&buf, dp, false, palette{}, 0) out := buf.String() for _, want := range []string{ - " # file actions rule\n", - " 1 f01.txt move → Out/ r type txt\n", - " 6 f06.txt move → Out/ a-noticeably-longer-rule-name type txt\n", - " 10 f10.txt move → Out/ r type txt\n", - " 11 " + long + " move → Out/ r type txt\n", + "\n 1 f01.txt\n move → Out/\n rule r\n because type txt\n", + "\n 6 f06.txt\n move → Out/\n rule a-noticeably-longer-rule-name\n because type txt\n", + "\n 10 f10.txt\n move → Out/\n", + "\n 11 " + long + "\n move → Out/\n rule r\n", } { if !strings.Contains(out, want) { t.Errorf("output lacks %q:\n%s", want, out) @@ -235,7 +329,7 @@ func TestPrintPlanCountsFileOnceWithBothWarningKinds(t *testing.T) { }, } var buf bytes.Buffer - printPlan(&buf, dp, false, palette{}) + printPlan(&buf, dp, false, palette{}, 0) out := buf.String() for _, want := range []string{ |
