filebank_test.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package stringutil
  2. import (
  3. "os"
  4. "path/filepath"
  5. "testing"
  6. "time"
  7. )
  8. func TestFileBank_LoadOrStore_MissAndHit(t *testing.T) {
  9. path := filepath.Join(t.TempDir(), "strings.dat")
  10. bank, err := NewFileStringBank(path, 10, time.Minute)
  11. if err != nil {
  12. t.Fatal(err)
  13. }
  14. defer func() { _ = bank.(*fileStringBank).Close() }()
  15. v, loaded := bank.LoadOrStore("hello", "hello")
  16. if loaded {
  17. t.Errorf("first LoadOrStore: expected loaded=false, got true")
  18. }
  19. if v != "hello" {
  20. t.Errorf("first LoadOrStore: expected value %q, got %q", "hello", v)
  21. }
  22. v, loaded = bank.LoadOrStore("hello", "world")
  23. if !loaded {
  24. t.Errorf("second LoadOrStore: expected loaded=true, got false")
  25. }
  26. if v != "hello" {
  27. t.Errorf("second LoadOrStore: expected cached value %q, got %q", "hello", v)
  28. }
  29. }
  30. func TestFileBank_EvictionPersistsAndReloads(t *testing.T) {
  31. path := filepath.Join(t.TempDir(), "strings.dat")
  32. maxBytes := 4 // 2 entries of size len(key)+len(value)==2 each
  33. bank, err := NewFileStringBank(path, maxBytes, 200*time.Millisecond)
  34. if err != nil {
  35. t.Fatal(err)
  36. }
  37. defer func() { _ = bank.(*fileStringBank).Close() }()
  38. bank.LoadOrStore("a", "a")
  39. time.Sleep(50 * time.Millisecond)
  40. bank.LoadOrStore("b", "b")
  41. time.Sleep(50 * time.Millisecond)
  42. bank.LoadOrStore("c", "c")
  43. time.Sleep(50 * time.Millisecond)
  44. // force eviction over capacity
  45. bank.LoadOrStore("d", "d")
  46. time.Sleep(400 * time.Millisecond)
  47. fb := bank.(*fileStringBank)
  48. fb.lock.Lock()
  49. inMemBytes := fb.currentSize
  50. fb.lock.Unlock()
  51. if inMemBytes > maxBytes {
  52. t.Fatalf("expected in-memory bytes <= %d after eviction, got %d", maxBytes, inMemBytes)
  53. }
  54. // Oldest entries should be recoverable from file-backed spill.
  55. v, loaded := bank.LoadOrStore("a", "a")
  56. if !loaded {
  57. t.Error("expected hit for 'a' via spill after eviction")
  58. }
  59. if v != "a" {
  60. t.Errorf("expected value %q from spill, got %q", "a", v)
  61. }
  62. fb.lock.Lock()
  63. _, stillSpill := fb.spill["a"]
  64. fb.lock.Unlock()
  65. if stillSpill {
  66. t.Error("after promoting 'a' into cache, it should be removed from spill index")
  67. }
  68. }
  69. func TestFileBank_EvictionUsesByteSizeNotEntryCount(t *testing.T) {
  70. path := filepath.Join(t.TempDir(), "strings.dat")
  71. maxBytes := 10
  72. bank, err := NewFileStringBank(path, maxBytes, 150*time.Millisecond)
  73. if err != nil {
  74. t.Fatal(err)
  75. }
  76. defer func() { _ = bank.(*fileStringBank).Close() }()
  77. bank.LoadOrStore("k1", "aaaaaa") // size 8
  78. time.Sleep(30 * time.Millisecond)
  79. bank.LoadOrStore("k2", "bb") // size 4, total 12 > 10
  80. time.Sleep(250 * time.Millisecond)
  81. fb := bank.(*fileStringBank)
  82. fb.lock.Lock()
  83. defer fb.lock.Unlock()
  84. if fb.currentSize > maxBytes {
  85. t.Fatalf("expected in-memory bytes <= %d, got %d", maxBytes, fb.currentSize)
  86. }
  87. }
  88. func TestFileBank_LoadOrStoreFunc_FactoryCalledOnMissOnly(t *testing.T) {
  89. path := filepath.Join(t.TempDir(), "strings.dat")
  90. bank, err := NewFileStringBank(path, 10, time.Minute)
  91. if err != nil {
  92. t.Fatal(err)
  93. }
  94. defer func() { _ = bank.(*fileStringBank).Close() }()
  95. calls := 0
  96. factory := func() string {
  97. calls++
  98. return "k"
  99. }
  100. bank.LoadOrStoreFunc("k", factory)
  101. bank.LoadOrStoreFunc("k", factory)
  102. if calls != 1 {
  103. t.Errorf("factory should be called exactly once, got %d calls", calls)
  104. }
  105. }
  106. func TestFileBank_Clear(t *testing.T) {
  107. path := filepath.Join(t.TempDir(), "strings.dat")
  108. bank, err := NewFileStringBank(path, 10, time.Minute)
  109. if err != nil {
  110. t.Fatal(err)
  111. }
  112. defer func() { _ = bank.(*fileStringBank).Close() }()
  113. bank.LoadOrStore("x", "x")
  114. bank.Clear()
  115. st, err := os.Stat(path)
  116. if err != nil {
  117. t.Fatal(err)
  118. }
  119. if st.Size() != 0 {
  120. t.Errorf("expected truncated file after Clear, size=%d", st.Size())
  121. }
  122. _, loaded := bank.LoadOrStore("x", "y")
  123. if loaded {
  124. t.Error("expected miss after Clear")
  125. }
  126. }
  127. func TestFileBank_NewOpenError(t *testing.T) {
  128. // Non-directory path that cannot be created as a file parent.
  129. _, err := NewFileStringBank("/nonexistent/dir/bank.dat", 3, time.Minute)
  130. if err == nil {
  131. t.Fatal("expected error for invalid path")
  132. }
  133. }