diff options
Diffstat (limited to 'internal/engine/exclude_test.go')
| -rw-r--r-- | internal/engine/exclude_test.go | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go new file mode 100644 index 0000000..40b3548 --- /dev/null +++ b/internal/engine/exclude_test.go @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package engine + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "krino/internal/plan" + "krino/internal/scan" +) + +// excludeTree creates ~/dl with files (name -> content), all old enough to +// be scanned, and returns home and dl. +func excludeTree(t *testing.T, files map[string]string) (home, dl string) { + t.Helper() + home = sandbox(t) + t.Setenv("PATH", t.TempDir()) + dl = filepath.Join(home, "dl") + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for name, body := range files { + p := filepath.Join(dl, name) + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte(body), 0o644); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(p, old, old); err != nil { + t.Fatal(err) + } + } + return home, dl +} + +// TestExcludeSetsFilesAside: an (exclude ...) in krino.conf applies to the +// directory, and the directory's own excludes apply too - by type, by name +// regex and by content - so those files get no actions from any rule and +// are reported as excluded, with the form that matched. +func TestExcludeSetsFilesAside(t *testing.T) { + h, _ := excludeTree(t, map[string]string{ + "a.iso": "disk image", + "draft-1.pdf": "%PDF draft", + "secret.txt": "this is poufne material", + "keep.txt": "ordinary notes", + }) + main := writeConfig(t, h, "(include \"dl\")\n(exclude (type iso))\n", map[string]string{"dl": ` +(path "~/dl") +(exclude (name "^draft")) +(exclude (type txt) (content "poufne")) +(rule "all" (move "Out")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + dp, err := e.Plan(context.Background(), e.Dirs[0], plan.NewClaims()) + if err != nil { + t.Fatal(err) + } + want := map[string]string{ + "a.iso": "(exclude (type iso))", + "draft-1.pdf": `(exclude (name "^draft"))`, + "secret.txt": `(exclude (type txt) (content "poufne"))`, + "keep.txt": "", + } + for _, fm := range dp.Result.Matched { + w, ok := want[fm.File.Rel] + if !ok { + t.Errorf("unexpected matched file %s", fm.File.Rel) + continue + } + if fm.Excluded != w { + t.Errorf("%s: Excluded = %q, want %q", fm.File.Rel, fm.Excluded, w) + } + if w != "" && len(fm.Rules) != 0 { + t.Errorf("%s: excluded but matched rules %v", fm.File.Rel, fm.Rules) + } + } + if len(dp.Result.Matched) != 4 || len(dp.Result.Unmatched) != 0 { + t.Errorf("matched %d, unmatched %d; want every file matched (3 excluded, 1 by the rule)", len(dp.Result.Matched), len(dp.Result.Unmatched)) + } + for _, c := range dp.Chains { + if acting := len(c.Steps) > 0; acting != (c.File.Rel == "keep.txt") { + t.Errorf("%s: steps %+v", c.File.Rel, c.Steps) + } + } +} + +// TestExcludeNeedsEveryCondition: within one form, every condition must +// hold, as in when. +func TestExcludeNeedsEveryCondition(t *testing.T) { + h, _ := excludeTree(t, map[string]string{"draft.txt": "x", "draft.pdf": "%PDF x"}) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(exclude (type pdf) (name "^draft")) +(rule "all" (move "Out")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r, err := e.Match(context.Background(), e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + for _, fm := range r.Matched { + if excluded := fm.Excluded != ""; excluded != (fm.File.Rel == "draft.pdf") { + t.Errorf("%s: Excluded = %q", fm.File.Rel, fm.Excluded) + } + } +} + +// TestExcludeContentKeepsRuleContentVariants: an exclude reading content +// under the directory's settings must not release the raw text a rule +// with different case/fold settings still needs. +func TestExcludeContentKeepsRuleContentVariants(t *testing.T) { + h, _ := excludeTree(t, map[string]string{"a.txt": "Invoice ACME"}) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(exclude (content "never present")) +(rule "strict" (case strict) (when (content "ACME")) (move "Out")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r, err := e.Match(context.Background(), e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + if len(r.Matched) != 1 || len(r.Matched[0].Rules) != 1 { + t.Fatalf("a.txt should match the strict content rule after the exclude read its text: %+v", r.Matched) + } +} + +// TestLoadReportsBadExcludeOnce: a condition error inside a krino.conf +// exclude is reported once, not once per directory. +func TestLoadReportsBadExcludeOnce(t *testing.T) { + h := sandbox(t) + main := writeConfig(t, h, "(include \"a\" \"b\")\n(exclude (size big))\n", map[string]string{ + "a": "(path \"/tmp\")", "b": "(path \"/tmp\")", + }) + _, errs := Load(main) + if len(errs) != 1 || !strings.Contains(errs[0].Error(), "size") { + t.Errorf("errs = %v, want exactly one error about size", errs) + } +} + +// TestMaxSizeSkipsTooBig: a directory's max-size skips larger files before +// any rule, as too big. +func TestMaxSizeSkipsTooBig(t *testing.T) { + h, _ := excludeTree(t, map[string]string{"small.txt": "x", "big.txt": strings.Repeat("x", 2048)}) + main := writeConfig(t, h, "(include \"dl\")\n(defaults (max-size 1K))\n", map[string]string{"dl": ` +(path "~/dl") +(rule "all" (move "Out")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r, err := e.Match(context.Background(), e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + if len(r.Skipped) != 1 || r.Skipped[0].Rel != "big.txt" || r.Skipped[0].Reason != scan.TooBig { + t.Errorf("skipped = %+v, want big.txt too big", r.Skipped) + } + if len(r.Matched) != 1 || r.Matched[0].File.Rel != "small.txt" { + t.Errorf("matched = %+v, want small.txt", r.Matched) + } +} + +// TestExplainReportsExclusionAndSize: explain names the exclude that sets a +// file aside, traces every exclude, and says a file is too big. +func TestExplainReportsExclusionAndSize(t *testing.T) { + h, dl := excludeTree(t, map[string]string{"a.iso": "disk", "big.txt": strings.Repeat("x", 2048)}) + main := writeConfig(t, h, "(include \"dl\")\n(exclude (type iso))\n", map[string]string{"dl": ` +(path "~/dl") +(max-size 1K) +(exclude (name "^nothing")) +(rule "all" (move "Out")) +`}) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + x, err := e.Explain(context.Background(), filepath.Join(dl, "a.iso")) + if err != nil { + t.Fatal(err) + } + if x.Excluded != "(exclude (type iso))" { + t.Errorf("Excluded = %q", x.Excluded) + } + if len(x.Excludes) != 2 || !x.Excludes[0].Match || x.Excludes[1].Match || x.Excludes[0].Trace == nil { + t.Errorf("Excludes = %+v, want the global one matching, the directory's one not", x.Excludes) + } + big, err := e.Explain(context.Background(), filepath.Join(dl, "big.txt")) + if err != nil { + t.Fatal(err) + } + if big.Skip != "too big" { + t.Errorf("Skip = %q, want too big", big.Skip) + } +} |
