summaryrefslogtreecommitdiff
path: root/internal/engine
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 21:39:00 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 21:39:00 +0200
commite0dddff176a01d410904b6750b3395de4f7e54db (patch)
treea0490057abfb75e89f7bd4559472717297cad823 /internal/engine
parentd53c83e2fe6f1ecef006f116095fa8d1d18f3a7d (diff)
downloadkrino-e0dddff176a01d410904b6750b3395de4f7e54db.tar.gz
krino-e0dddff176a01d410904b6750b3395de4f7e54db.zip
plan 9: keyword cache keys on extension, max-read and Unicode tables, trims removed keywords
Diffstat (limited to 'internal/engine')
-rw-r--r--internal/engine/cache_test.go57
-rw-r--r--internal/engine/facts.go2
-rw-r--r--internal/engine/match.go29
3 files changed, 79 insertions, 9 deletions
diff --git a/internal/engine/cache_test.go b/internal/engine/cache_test.go
index eee2860..8c52d48 100644
--- a/internal/engine/cache_test.go
+++ b/internal/engine/cache_test.go
@@ -6,9 +6,14 @@ import (
"context"
"os"
"path/filepath"
+ "runtime"
"strings"
"testing"
"time"
+ "unicode"
+
+ "krino/internal/config"
+ "krino/internal/extract"
)
// countingPDFTree makes ~/dl holding the given .pdf files, and a fake
@@ -222,3 +227,55 @@ func TestExplainUsesCacheWithoutWriting(t *testing.T) {
t.Errorf("explain extracted although the run cached a.pdf")
}
}
+
+// TestCacheRereadsARenamedExtension: the extension picks the extractor, so
+// a file renamed from .html to .txt - same inode, size and mtime - is read
+// again, not answered from what the markup reader found (review M6).
+func TestCacheRereadsARenamedExtension(t *testing.T) {
+ home, dl := excludeTree(t, map[string]string{"page.html": "<p>ACME</p>&nbsp;Ltd"})
+ main := writeConfig(t, home, `(include "dl")`, map[string]string{"dl": "(path \"~/dl\")\n(rule \"acme\" (when (content \"acme ltd\")) (move \"Acme\"))\n"})
+ if r := cachedMatch(t, home, main); matchedNames(r) != "page.html" {
+ t.Fatalf("as html: matched %q", matchedNames(r))
+ }
+ if err := os.Rename(filepath.Join(dl, "page.html"), filepath.Join(dl, "page.txt")); err != nil {
+ t.Fatal(err)
+ }
+ if r := cachedMatch(t, home, main); matchedNames(r) != "" {
+ t.Errorf("as txt, answered from the html read: matched %q", matchedNames(r))
+ }
+}
+
+// TestCacheFingerprintCoversMaxReadAndTables: answers depend on max-read
+// (it caps what is read), on the Go release and on the Unicode tables, so
+// all are in the fingerprint (review cache F2, F4).
+func TestCacheFingerprintCoversMaxReadAndTables(t *testing.T) {
+ e := &Engine{Extract: extract.New()}
+ small := e.cacheFingerprint(&Dir{Settings: config.Resolved{MaxRead: 1 << 10}})
+ large := e.cacheFingerprint(&Dir{Settings: config.Resolved{MaxRead: 1 << 20}})
+ if small == large {
+ t.Error("max-read does not change the fingerprint")
+ }
+ for _, want := range []string{runtime.Version(), unicode.Version} {
+ if !strings.Contains(small, want) {
+ t.Errorf("fingerprint %q lacks %q", small, want)
+ }
+ }
+}
+
+// TestCacheRemovedWhenNoContentTestsRemain: once a directory has no content
+// test, its cache - holding keywords it no longer uses - is removed (review
+// cache F6).
+func TestCacheRemovedWhenNoContentTestsRemain(t *testing.T) {
+ home, _, _ := countingPDFTree(t, map[string]string{"a.pdf": "Invoice ACME"})
+ main := writeConfig(t, home, `(include "dl")`, map[string]string{"dl": acmeRules})
+ cachedMatch(t, home, main)
+ file := filepath.Join(home, ".cache", "krino", "dl.cache")
+ if _, err := os.Stat(file); err != nil {
+ t.Fatal(err)
+ }
+ writeConfig(t, home, `(include "dl")`, map[string]string{"dl": "(path \"~/dl\")\n(rule \"all\" (move \"Out\"))\n"})
+ cachedMatch(t, home, main)
+ if _, err := os.Stat(file); !os.IsNotExist(err) {
+ t.Errorf("the cache of a directory without content tests is still there: %v", err)
+ }
+}
diff --git a/internal/engine/facts.go b/internal/engine/facts.go
index 9035245..487604b 100644
--- a/internal/engine/facts.go
+++ b/internal/engine/facts.go
@@ -212,7 +212,7 @@ func (f *facts) cacheID() (kwcache.ID, bool) {
// fileCacheID is file's kwcache.ID.
func fileCacheID(file scan.File) kwcache.ID {
- return kwcache.ID{Dev: file.Dev, Ino: file.Ino, Size: file.Size, MTime: file.ModTime.UnixNano()}
+ return kwcache.ID{Dev: file.Dev, Ino: file.Ino, Size: file.Size, MTime: file.ModTime.UnixNano(), Ext: strings.ToLower(filepath.Ext(file.Name))}
}
// Duplicate resolves dirs against the directory's root, builds (or reuses)
diff --git a/internal/engine/match.go b/internal/engine/match.go
index b922829..08d75af 100644
--- a/internal/engine/match.go
+++ b/internal/engine/match.go
@@ -114,7 +114,11 @@ func (e *Engine) Match(ctx context.Context, d *Dir) (*Result, error) {
ids = append(ids, fileCacheID(f))
}
}
- if err := run.cache.Save(e.cacheFile(d), ids); err != nil {
+ keys := make([]string, len(d.ContentKeywords))
+ for i, k := range d.ContentKeywords {
+ keys[i] = k.Key()
+ }
+ if err := run.cache.Save(e.cacheFile(d), ids, keys); err != nil {
warnings = append(warnings, "cache: "+err.Error())
}
}
@@ -297,11 +301,12 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error)
return &Explanation{Dir: d, File: sf, Skip: skip, Excludes: excludes, Excluded: excluded, Rules: rules}, nil
}
-// cacheFingerprint identifies what a cached keyword answer depends on
-// besides the file: the extractor (its version and tools) and the
-// normalisation version. A cache written under any other is discarded.
-func (e *Engine) cacheFingerprint() string {
- return fmt.Sprintf("%s norm%d", e.Extract.Fingerprint(), norm.Version)
+// cacheFingerprint identifies what a cached keyword answer of d depends on
+// besides the file: the extractor (its version and tools), normalisation
+// and its Unicode tables, the Go release, and d's max-read, which caps what
+// is read. A cache written under any other is discarded.
+func (e *Engine) cacheFingerprint(d *Dir) string {
+ return fmt.Sprintf("%s %s %s max-read=%d", e.Extract.Fingerprint(), norm.Fingerprint(), runtime.Version(), d.Settings.MaxRead)
}
// cacheFile is d's keyword cache file.
@@ -313,10 +318,18 @@ func (e *Engine) cacheFile(d *Dir) string {
// engine has a CacheDir and the directory has content tests at all. A cache
// that cannot be read is replaced by an empty one, reported as a warning.
func (e *Engine) openCache(run *matchRun) []string {
- if e.CacheDir == "" || len(run.d.ContentKeywords) == 0 {
+ if e.CacheDir == "" {
+ return nil
+ }
+ if len(run.d.ContentKeywords) == 0 {
+ // No content test left: a cache from an earlier configuration only
+ // holds keywords this directory no longer uses (review cache F6).
+ if err := os.Remove(e.cacheFile(run.d)); err != nil && !os.IsNotExist(err) {
+ return []string{"cache: " + err.Error()}
+ }
return nil
}
- c, err := kwcache.Load(e.cacheFile(run.d), e.cacheFingerprint())
+ c, err := kwcache.Load(e.cacheFile(run.d), e.cacheFingerprint(run.d))
run.cache = c
if err != nil {
return []string{"cache: " + err.Error() + " (starting a new one)"}