diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/krino/matching_test.go | 49 | ||||
| -rw-r--r-- | cmd/krino/sort.go | 16 |
2 files changed, 64 insertions, 1 deletions
diff --git a/cmd/krino/matching_test.go b/cmd/krino/matching_test.go index 6986e99..754e577 100644 --- a/cmd/krino/matching_test.go +++ b/cmd/krino/matching_test.go @@ -499,3 +499,52 @@ func TestLongNameGetsItsOwnLine(t *testing.T) { t.Errorf("short name block:\n%s\nwant substring:\n%s", out, want) } } + +// TestDryRunJSONCarriesExclusionsAndWarnings: the JSON plan says which +// exclude set a file aside and carries the warnings matching raised, for +// matched and unmatched files alike, as the text plan does (triage 28h). +func TestDryRunJSONCarriesExclusionsAndWarnings(t *testing.T) { + h := home(t) + dl := filepath.Join(h, "dl") + os.MkdirAll(dl, 0o755) + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for name, body := range map[string]string{"keep.pdf": "x", "big.txt": strings.Repeat("y", 4096)} { + p := filepath.Join(dl, name) + os.WriteFile(p, []byte(body), 0o644) + os.Chtimes(p, old, old) + } + if code, _, errOut := runCLI(t, "init"); code != 0 { + t.Fatal(errOut) + } + if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 { + t.Fatal(errOut) + } + rules := "(path \"~/dl\")\n(max-read 1K)\n(exclude (name \"^keep\"))\n(rule \"acme\" (when (content \"acme\")) (move \"Acme\"))\n" + os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644) + code, out, errOut := runCLI(t, "-n", "--json") + if code != 0 { + t.Fatalf("exit %d: %s", code, errOut) + } + var doc struct { + Dirs []struct { + Files []struct { + Rel string `json:"rel"` + Excluded string `json:"excluded"` + } `json:"files"` + Unmatched []struct { + Rel string `json:"rel"` + Warnings []string `json:"warnings"` + } `json:"unmatched"` + } `json:"dirs"` + } + if err := json.Unmarshal([]byte(out), &doc); err != nil { + t.Fatalf("%v\n%s", err, out) + } + d := doc.Dirs[0] + if len(d.Files) != 1 || d.Files[0].Rel != "keep.pdf" || d.Files[0].Excluded != `(exclude (name "^keep"))` { + t.Errorf("files = %+v; want keep.pdf excluded by its form\n%s", d.Files, out) + } + if len(d.Unmatched) != 1 || d.Unmatched[0].Rel != "big.txt" || len(d.Unmatched[0].Warnings) == 0 || !strings.Contains(d.Unmatched[0].Warnings[0], "content unreadable") { + t.Errorf("unmatched = %+v; want big.txt with its content warning\n%s", d.Unmatched, out) + } +} diff --git a/cmd/krino/sort.go b/cmd/krino/sort.go index f600475..83ede11 100644 --- a/cmd/krino/sort.go +++ b/cmd/krino/sort.go @@ -183,7 +183,7 @@ func cmdSort(g *globals, names []string, stdout, stderr io.Writer) int { // --json is only ever reached with -n (checked above), and // Ruling 7 is explicit that JSON must never be paged, so // this returns before any of the paging/review code below. - jsonDirs = append(jsonDirs, plan.NewJSONDir(d.Name, d.Root, dp.Chains, dp.Result.Warnings)) + jsonDirs = append(jsonDirs, plan.NewJSONDir(d.Name, d.Root, dp.Chains, fileNotes(dp.Result), dp.Result.Warnings)) return false } @@ -427,6 +427,20 @@ type warnLine struct { // the chain-level warnings (e.g. "moved more than once"), keyed by // Chain.File.Rel; every chain's file is necessarily also in r.Matched (only // matched files ever reach plan.Build), so it is visited exactly once here. +// fileNotes is r's per-file findings - the exclude that set a file aside, +// the warnings matching raised - for the JSON plan. +func fileNotes(r *engine.Result) map[string]plan.FileNotes { + notes := map[string]plan.FileNotes{} + for _, fms := range [][]engine.FileMatch{r.Matched, r.Unmatched} { + for _, fm := range fms { + if fm.Excluded != "" || len(fm.Warnings) > 0 { + notes[fm.File.Rel] = plan.FileNotes{File: fm.File, Excluded: fm.Excluded, Warnings: fm.Warnings} + } + } + } + return notes +} + func collectWarnings(r *engine.Result, chains []plan.Chain) []warnLine { files := make([]engine.FileMatch, 0, len(r.Matched)+len(r.Unmatched)) files = append(files, r.Matched...) |
