// SPDX-License-Identifier: GPL-3.0-or-later package kwcache import ( "os" "path/filepath" "reflect" "testing" ) var ( a = ID{Dev: 1, Ino: 10, Size: 100, MTime: 1000} b = ID{Dev: 1, Ino: 11, Size: 200, MTime: 2000} ) func saved(t *testing.T, c *Cache, present ...ID) string { t.Helper() path := filepath.Join(t.TempDir(), "sub", "dl.cache") if err := c.Save(path, present); err != nil { t.Fatal(err) } return path } // TestLookupAfterReload: answers stored and saved come back after a Load // with the same fingerprint, for any keywords the entry covers. func TestLookupAfterReload(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true, "k:faktura": false, "k:nip": true}) path := saved(t, c, a) got, err := Load(path, "fp") if err != nil { t.Fatal(err) } hits, ok := got.Lookup(a, []string{"k:faktura", "k:nip"}) if !ok || !reflect.DeepEqual(hits, []bool{false, true}) { t.Errorf("Lookup = %v, %v; want [false true], true", hits, ok) } if _, ok := got.Lookup(a, []string{"k:acme", "k:new"}); ok { t.Error("a keyword the entry was never checked against must be a miss") } if _, ok := got.Lookup(b, []string{"k:acme"}); ok { t.Error("a file never stored must be a miss") } } // TestLookupBeforeSave: what was stored in this run answers at once. func TestLookupBeforeSave(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true}) if hits, ok := c.Lookup(a, []string{"k:acme"}); !ok || !hits[0] { t.Errorf("Lookup = %v, %v", hits, ok) } } // TestChangedFileMisses: a different size or modification time is a // different ID, so nothing stored for the old one answers. func TestChangedFileMisses(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true}) for _, id := range []ID{{Dev: 1, Ino: 10, Size: 101, MTime: 1000}, {Dev: 1, Ino: 10, Size: 100, MTime: 1001}} { if _, ok := c.Lookup(id, []string{"k:acme"}); ok { t.Errorf("%+v answered from %+v's entry", id, a) } } } // TestFingerprintMismatchDiscardsAll: a cache written under a different // extractor fingerprint loads empty, without error. func TestFingerprintMismatchDiscardsAll(t *testing.T) { c := New("old") c.Store(a, map[string]bool{"k:acme": true}) path := saved(t, c, a) got, err := Load(path, "new") if err != nil { t.Fatal(err) } if _, ok := got.Lookup(a, []string{"k:acme"}); ok { t.Error("entry survived a fingerprint change") } } // TestSaveKeepsOnlyPresentFiles: entries for files not passed to Save, from // this run or an earlier one, are dropped. func TestSaveKeepsOnlyPresentFiles(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true}) c.Store(b, map[string]bool{"k:acme": false}) path := saved(t, c, a, b) next, err := Load(path, "fp") if err != nil { t.Fatal(err) } if err := next.Save(path, []ID{b}); err != nil { t.Fatal(err) } last, err := Load(path, "fp") if err != nil { t.Fatal(err) } if _, ok := last.Lookup(a, []string{"k:acme"}); ok { t.Error("a is gone from the directory but its entry was kept") } if hits, ok := last.Lookup(b, []string{"k:acme"}); !ok || hits[0] { t.Errorf("b's entry lost across a reload: %v, %v", hits, ok) } } // TestStoreReplacesLoadedEntry: a file read again this run replaces what // an earlier run stored for it. func TestStoreReplacesLoadedEntry(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true}) path := saved(t, c, a) next, _ := Load(path, "fp") next.Store(a, map[string]bool{"k:acme": false, "k:new": true}) if hits, ok := next.Lookup(a, []string{"k:acme", "k:new"}); !ok || hits[0] || !hits[1] { t.Errorf("Lookup = %v, %v; want the new entry", hits, ok) } } // TestSavePermissions: the directory is private and the file readable by // its owner only, with no temporary file left behind. func TestSavePermissions(t *testing.T) { c := New("fp") c.Store(a, map[string]bool{"k:acme": true}) path := saved(t, c, a) fi, err := os.Stat(path) if err != nil { t.Fatal(err) } if perm := fi.Mode().Perm(); perm != 0o600 { t.Errorf("file mode %o, want 600", perm) } di, err := os.Stat(filepath.Dir(path)) if err != nil { t.Fatal(err) } if perm := di.Mode().Perm(); perm != 0o700 { t.Errorf("directory mode %o, want 700", perm) } entries, _ := os.ReadDir(filepath.Dir(path)) if len(entries) != 1 { t.Errorf("directory holds %d entries, want only the cache", len(entries)) } } // TestLoadMissingAndCorrupt: no file is an empty cache and no error; an // unreadable one is an empty cache and an error saying so. func TestLoadMissingAndCorrupt(t *testing.T) { dir := t.TempDir() c, err := Load(filepath.Join(dir, "none.cache"), "fp") if err != nil || c == nil { t.Fatalf("missing: %v, %v", c, err) } bad := filepath.Join(dir, "bad.cache") os.WriteFile(bad, []byte("{not json"), 0o600) c, err = Load(bad, "fp") if err == nil || c == nil { t.Fatalf("corrupt: want an empty cache and an error, got %v, %v", c, err) } if _, ok := c.Lookup(a, []string{"k:acme"}); ok { t.Error("corrupt cache answered") } } // TestSaveWithNothingWritesNothing: a cache that never held an entry, and // has no file yet, creates none. func TestSaveWithNothingWritesNothing(t *testing.T) { path := filepath.Join(t.TempDir(), "sub", "dl.cache") if err := New("fp").Save(path, []ID{a}); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Dir(path)); !os.IsNotExist(err) { t.Errorf("Save created %s for an empty cache", filepath.Dir(path)) } }