summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:40:56 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:40:56 +0200
commitbb8b547023b0867f31d7452faceef5769a45b94b (patch)
tree9daf9efd0ac5336747483557976a83ac5725b956 /internal
parentc09020c2fb01da24d5befbed78ce3b73b5efbe4c (diff)
downloadkrino-bb8b547023b0867f31d7452faceef5769a45b94b.tar.gz
krino-bb8b547023b0867f31d7452faceef5769a45b94b.zip
explain: shows a duplicate's skipped delete, leaves the cache alone, walks only for duplicate tests
Diffstat (limited to 'internal')
-rw-r--r--internal/engine/exclude_test.go22
-rw-r--r--internal/engine/match.go23
2 files changed, 42 insertions, 3 deletions
diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go
index ceb8442..cd1cc4b 100644
--- a/internal/engine/exclude_test.go
+++ b/internal/engine/exclude_test.go
@@ -426,3 +426,25 @@ func TestPartlyReadableDocument(t *testing.T) {
t.Errorf("no warning for the keyword the unreadable part might hold")
}
}
+
+// TestExplainLeavesTheCacheAlone: explain never writes the keyword cache -
+// not even to remove one a directory without content tests no longer uses
+// (triage 28e: explain runs without the directory's lock).
+func TestExplainLeavesTheCacheAlone(t *testing.T) {
+ h, dl := excludeTree(t, map[string]string{"a.txt": "x"})
+ main := writeConfig(t, h, `(include "dl")`, map[string]string{"dl": "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"})
+ e, errs := Load(main)
+ if len(errs) > 0 {
+ t.Fatal(errs)
+ }
+ e.CacheDir = filepath.Join(h, "cache")
+ os.MkdirAll(e.CacheDir, 0o700)
+ cache := filepath.Join(e.CacheDir, "dl.cache")
+ os.WriteFile(cache, []byte("old"), 0o600)
+ if _, err := e.Explain(context.Background(), filepath.Join(dl, "a.txt")); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(cache); err != nil {
+ t.Errorf("explain removed the cache: %v", err)
+ }
+}
diff --git a/internal/engine/match.go b/internal/engine/match.go
index 162730b..0e8b3d5 100644
--- a/internal/engine/match.go
+++ b/internal/engine/match.go
@@ -227,6 +227,7 @@ type Explanation struct {
Excludes []ExcludeTrace
Excluded string // the first exclude that matches, which sets the file aside; "" when none does
Rules []RuleTrace
+ NoDelete string // why a delete from the matching rules would be skipped (spec ยง5.5); "" when it would not
}
// Explain reports, for one file, whether krino's ordinary scan would ever
@@ -266,8 +267,18 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error)
excl := e.excludeDirs(d)
skip := explainSkip(d, sf, excl, now)
- run := newMatchRun(e, d, ctx, now, e.filesForExplain(d, sf, excl, now))
- e.openCache(run) // read only: Explain never writes the cache
+ // The directory is walked only for a duplicate test, the one thing that
+ // needs its other files (triage 21).
+ files := []scan.File{sf}
+ if len(d.DupScopes) > 0 {
+ files = e.filesForExplain(d, sf, excl, now)
+ }
+ run := newMatchRun(e, d, ctx, now, files)
+ if len(d.ContentKeywords) > 0 {
+ // Loaded only: openCache would remove the cache of a directory with
+ // no content tests, and Explain holds no lock (triage 28e).
+ e.openCache(run)
+ }
f := newFacts(run, sf)
var excludes []ExcludeTrace
@@ -282,6 +293,7 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error)
}
var rules []RuleTrace
+ var matched []RuleMatch
stoppedBy := ""
for _, r := range d.Rules {
if stoppedBy != "" {
@@ -292,14 +304,19 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error)
match := trace.Value
if match {
f.matched = true
+ matched = append(matched, RuleMatch{Rule: r})
}
rules = append(rules, RuleTrace{Rule: r, Match: match, Trace: trace})
if match && r.Conf.Stop {
stoppedBy = r.Name
}
}
+ noDel := ""
+ if len(d.DupScopes) > 0 && deletes(matched) {
+ noDel = noDelete(f, d.DupScopes)
+ }
- return &Explanation{Dir: d, File: sf, Skip: skip, Excludes: excludes, Excluded: excluded, Rules: rules}, nil
+ return &Explanation{Dir: d, File: sf, Skip: skip, Excludes: excludes, Excluded: excluded, Rules: rules, NoDelete: noDel}, nil
}
// cacheFingerprint identifies what a cached keyword answer of d depends on