aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/render_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino/render_test.go')
-rw-r--r--cmd/krino/render_test.go144
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{