From 3b36a48b7ce5a53a9366f3b31f94311f178e2553 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Sat, 12 Sep 2026 01:22:12 +0200 Subject: krino: matching — scan, ignore, conditions, extraction, duplicates, explain, dry run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/engine/engine_test.go | 166 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 internal/engine/engine_test.go (limited to 'internal/engine/engine_test.go') diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go new file mode 100644 index 0000000..5c0536f --- /dev/null +++ b/internal/engine/engine_test.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package engine + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "krino/internal/cond" +) + +// sandbox gives a test its own HOME with no XDG overrides and returns it. +func sandbox(t *testing.T) string { + t.Helper() + h := t.TempDir() + t.Setenv("HOME", h) + for _, v := range []string{"XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"} { + t.Setenv(v, "") + } + return h +} + +// writeConfig writes krino.conf and dirs/.conf files under home/.config/krino. +func writeConfig(t *testing.T, home, main string, dirs map[string]string) string { + t.Helper() + cdir := filepath.Join(home, ".config", "krino") + if err := os.MkdirAll(filepath.Join(cdir, "dirs"), 0o755); err != nil { + t.Fatal(err) + } + mainFile := filepath.Join(cdir, "krino.conf") + if err := os.WriteFile(mainFile, []byte(main), 0o644); err != nil { + t.Fatal(err) + } + for n, body := range dirs { + if err := os.WriteFile(filepath.Join(cdir, "dirs", n+".conf"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + return mainFile +} + +// fakeFacts is a minimal cond.Facts for checking compiled rules. +type fakeFacts struct{ name string } + +func (f fakeFacts) Name() string { return f.name } +func (f fakeFacts) Rel() string { return f.name } +func (f fakeFacts) Size() int64 { return 1 } +func (f fakeFacts) ModTime() time.Time { return time.Time{} } +func (f fakeFacts) Now() time.Time { return time.Time{} } +func (f fakeFacts) Content(bool, bool) (string, error) { return "", nil } +func (f fakeFacts) Duplicate([]string) (string, bool, error) { return "", false, nil } +func (f fakeFacts) Matched() bool { return false } + +var _ cond.Facts = fakeFacts{} + +func TestLoadCompilesWithRuleSettings(t *testing.T) { + h := sandbox(t) + os.Mkdir(filepath.Join(h, "dl"), 0o755) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(case strict) +(ignore "*.part") +(rule "strict" (when (name "^img")) (stop)) +(rule "loose" (case ignore) (when (name "^img")) (stop)) +`}) + e, errs := Load(main, "dl", "dl") + if len(errs) > 0 { + t.Fatal(errs) + } + if len(e.Dirs) != 1 { + t.Fatalf("got %d dirs, want 1 (duplicate name ignored)", len(e.Dirs)) + } + d := e.Dirs[0] + if d.Root != filepath.Join(h, "dl") || d.Ignore == nil || !d.Ignore.Match("x.part", false) { + t.Fatalf("dir = %+v", d) + } + img := fakeFacts{name: "IMG_1.jpg"} + if d.Rules[0].Cond.Eval(img).Match { + t.Error("rule under (case strict) matched IMG against ^img") + } + if !d.Rules[1].Cond.Eval(img).Match { + t.Error("rule-level (case ignore) did not apply at compile time") + } +} + +func TestLoadReportsEveryProblem(t *testing.T) { + h := sandbox(t) + main := writeConfig(t, h, `(include "a" "b")`, map[string]string{ + "a": `(path "/tmp") (rule "x" (when (type "pdf")) (stop))`, + "b": `(path "/tmp") (ignore "[abc") (rule "y" (when (size 1)) (stop))`, + }) + e, errs := Load(main) + if e != nil { + t.Fatal("engine returned despite errors") + } + joined := "" + for _, d := range errs { + joined += d.Error() + "\n" + } + for _, want := range []string{ + "a.conf:1:37: type names are bare words: write (type pdf)", + `b.conf: bad ignore pattern "[abc": unterminated [`, + "b.conf:1:47: size takes an operator and a size, like (size > 10M)", + } { + if !strings.Contains(joined, want) { + t.Errorf("missing %q in:\n%s", want, joined) + } + } +} + +func TestCheck(t *testing.T) { + h := sandbox(t) + bin := t.TempDir() + os.WriteFile(filepath.Join(bin, "pdftotext"), []byte("#!/bin/sh\n"), 0o755) + t.Setenv("PATH", bin) + os.Mkdir(filepath.Join(h, "here"), 0o755) + main := writeConfig(t, h, `(include "here" "gone")`, map[string]string{ + "here": `(path "~/here") (rule "r" (stop))`, + "gone": `(path "~/gone") (rule "r" (stop))`, + }) + e, errs := Load(main) + if len(errs) > 0 { + t.Fatal(errs) + } + r := e.Check() + if r.MainFile != main || r.LogFile != filepath.Join(h, ".local", "state", "krino", "krino.log") { + t.Errorf("report files = %q %q", r.MainFile, r.LogFile) + } + if len(r.Dirs) != 2 || r.Dirs[0].Missing || !r.Dirs[1].Missing { + t.Errorf("dirs = %+v", r.Dirs) + } + if r.Tools[0].Name != "pdftotext" || r.Tools[0].Path != filepath.Join(bin, "pdftotext") || r.Tools[1].Path != "" { + t.Errorf("tools = %+v", r.Tools) + } +} + +// TestContentVariantsComputedAtLoad: B2 plumbing. Load computes each +// directory's distinct (ignoreCase, fold) content-test variants from its +// rules' resolved settings: a rule with no content test contributes +// nothing; two rules sharing a variant fold into one; a rule-level (case +// ignore) override adds a second. +func TestContentVariantsComputedAtLoad(t *testing.T) { + h := sandbox(t) + os.Mkdir(filepath.Join(h, "dl"), 0o755) + main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": ` +(path "~/dl") +(case strict) +(rule "no-content" (when (type pdf)) (stop)) +(rule "strict-content" (when (content "acme")) (stop)) +(rule "also-strict-content" (when (content "other")) (stop)) +(rule "loose-content" (case ignore) (when (content "acme")) (stop)) +`}) + e, errs := Load(main, "dl") + if len(errs) > 0 { + t.Fatal(errs) + } + got := e.Dirs[0].ContentVariants + want := []cond.Options{{IgnoreCase: false, Fold: true}, {IgnoreCase: true, Fold: true}} + if !reflect.DeepEqual(got, want) { + t.Fatalf("ContentVariants = %+v, want %+v", got, want) + } +} -- cgit v1.3