diff --git a/filestore.go b/filestore.go index 3a497f0..1809268 100644 --- a/filestore.go +++ b/filestore.go @@ -92,19 +92,17 @@ func readEntriesFromReader(r io.Reader) ([]PathEntry, error) { var mu sync.Mutex -// writeFileStore ages the entries if they have grown past the rank threshold, -// then writes them out. -func writeFileStore(entries []PathEntry) { - var cumulativeRank float64 - for _, entry := range entries { - cumulativeRank += entry.Rank - } - +// writeFileStore ages the entries if the store has grown past the rank +// threshold, then writes them out. priorRank is the total read out of the data +// file: fasd sums the ranks of the lines it reads, before applying any +// rank + 1/rank increment and without counting the paths being added, so the +// caller supplies that rather than the post-update total. +func writeFileStore(entries []PathEntry, priorRank float64) { // Apply decay if cumulative rank exceeds threshold const threshold = 2000.0 const decayFactor = 0.9 - if cumulativeRank > threshold { + if priorRank > threshold { logger.Log.Println("Rank threshold met. Decaying...") for i := range entries { entries[i].Rank *= decayFactor @@ -217,6 +215,14 @@ func AddToStore(paths ...string) { entries = pruneDecayed(entries) + // Totalled here, after the prune and before any increment, because that is + // what fasd's `count += $2` accumulates: the ranks of the surviving lines + // as they were read, with the paths being added excluded. + var priorRank float64 + for _, entry := range entries { + priorRank += entry.Rank + } + now := time.Now().Unix() seen := make(map[string]bool, len(paths)) @@ -254,5 +260,5 @@ func AddToStore(paths ...string) { } // Write updated entries back to the file - writeFileStore(entries) + writeFileStore(entries, priorRank) } diff --git a/main_test.go b/main_test.go index 8cb7295..b0a3ebe 100644 --- a/main_test.go +++ b/main_test.go @@ -815,3 +815,83 @@ func TestAddToStoreCountsRepeatedPathsOnce(t *testing.T) { t.Fatalf("Expected a single entry at rank 1, but got %v", entries) } } + +func TestDecayIgnoresNewlyAddedPaths(t *testing.T) { + cwd, readEntries := setupProcTest(t) + + high := filepath.Join(cwd, "high") + first := filepath.Join(cwd, "first") + second := filepath.Join(cwd, "second") + for _, dir := range []string{high, first, second} { + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } + } + // Just under the threshold. Counting the two new paths would push the + // total to 2001 and age the store, but fasd's `count += $2` only sums the + // lines it read, so 1999 stands and nothing is aged. + writeEntries([]PathEntry{{Path: high, Rank: 1999, LastAccessed: 100}}) + + AddPaths([]string{first, second}) + + expected := map[string]float64{high: 1999, first: 1, second: 1} + assertRanks(t, readEntries(), expected) +} + +func TestDecayIgnoresTheRankIncrement(t *testing.T) { + cwd, readEntries := setupProcTest(t) + + high := filepath.Join(cwd, "high") + if err := os.MkdirAll(high, 0700); err != nil { + t.Fatal(err) + } + // Exactly at the threshold, which is not over it. Re-adding this path + // raises it to 2000.0005, but fasd totals the pre-increment rank, so the + // store is not aged. + writeEntries([]PathEntry{{Path: high, Rank: 2000, LastAccessed: 100}}) + + AddPaths([]string{high}) + + assertRanks(t, readEntries(), map[string]float64{high: 2000 + 1.0/2000}) +} + +func TestDecayIgnoresPrunedEntries(t *testing.T) { + cwd, readEntries := setupProcTest(t) + + high := filepath.Join(cwd, "high") + faded := filepath.Join(cwd, "faded") + fresh := filepath.Join(cwd, "fresh") + for _, dir := range []string{high, faded, fresh} { + if err := os.MkdirAll(dir, 0700); err != nil { + t.Fatal(err) + } + } + // fasd accumulates count inside the `$2 >= 1` block, so a line below the + // prune threshold contributes nothing to the decay decision. + writeEntries([]PathEntry{ + {Path: high, Rank: 1999.5, LastAccessed: 100}, + {Path: faded, Rank: 0.9, LastAccessed: 200}, + }) + + AddPaths([]string{fresh}) + + expected := map[string]float64{high: 1999.5, fresh: 1} + assertRanks(t, readEntries(), expected) +} + +func assertRanks(t *testing.T, entries []PathEntry, expected map[string]float64) { + t.Helper() + + if len(entries) != len(expected) { + t.Fatalf("Expected %d entries, but got %v", len(expected), entries) + } + for _, entry := range entries { + want, ok := expected[entry.Path] + if !ok { + t.Fatalf("Unexpected entry %v", entry) + } + if math.Abs(entry.Rank-want) > 1e-6 { + t.Fatalf("Expected %s at rank %v, but got %v", entry.Path, want, entry.Rank) + } + } +}