codeberg.org/gruf/go-cache/v3@v3.5.7/result/cache_test.go (about) 1 package result_test 2 3 import ( 4 "errors" 5 "math" 6 "reflect" 7 "testing" 8 "time" 9 10 "codeberg.org/gruf/go-cache/v3/result" 11 "github.com/google/go-cmp/cmp" 12 ) 13 14 const ( 15 testLookupField1 = "Field1" 16 testLookupField2 = "Field2" 17 testLookupField3 = "Field3" 18 testLookupField4 = "Field4" 19 testLookupField5And6 = "Field5.Field6" 20 testLookupField7 = "Field7" 21 testLookupField8 = "Field8" 22 testLookupField9And10 = "Field9.Field10" 23 testLookupField11 = "Field11" 24 testLookupField12 = "Field12" 25 testLookupField13 = "Field13" 26 testLookupField14 = "Field14" 27 testLookupField15 = "Field15" 28 testLookupField16 = "Field16" 29 ) 30 31 var testLookups = []struct { 32 Lookup result.Lookup 33 Fields func(testType) []any 34 }{ 35 { 36 Lookup: result.Lookup{Name: testLookupField1, AllowZero: true}, 37 Fields: func(tt testType) []any { return []any{tt.Field1} }, 38 }, 39 { 40 Lookup: result.Lookup{Name: testLookupField2, AllowZero: true}, 41 Fields: func(tt testType) []any { return []any{tt.Field2} }, 42 }, 43 { 44 Lookup: result.Lookup{Name: testLookupField3, AllowZero: true}, 45 Fields: func(tt testType) []any { return []any{tt.Field3} }, 46 }, 47 { 48 Lookup: result.Lookup{Name: testLookupField4, AllowZero: true}, 49 Fields: func(tt testType) []any { return []any{tt.Field4} }, 50 }, 51 { 52 Lookup: result.Lookup{Name: testLookupField5And6, AllowZero: true}, 53 Fields: func(tt testType) []any { return []any{tt.Field5, tt.Field6} }, 54 }, 55 { 56 Lookup: result.Lookup{Name: testLookupField7, AllowZero: true}, 57 Fields: func(tt testType) []any { return []any{tt.Field7} }, 58 }, 59 { 60 Lookup: result.Lookup{Name: testLookupField8, AllowZero: true}, 61 Fields: func(tt testType) []any { return []any{tt.Field8} }, 62 }, 63 { 64 Lookup: result.Lookup{Name: testLookupField9And10, AllowZero: true}, 65 Fields: func(tt testType) []any { return []any{tt.Field9, tt.Field10} }, 66 }, 67 { 68 Lookup: result.Lookup{Name: testLookupField11, AllowZero: true}, 69 Fields: func(tt testType) []any { return []any{tt.Field11} }, 70 }, 71 { 72 Lookup: result.Lookup{Name: testLookupField12, AllowZero: true}, 73 Fields: func(tt testType) []any { return []any{tt.Field12} }, 74 }, 75 { 76 Lookup: result.Lookup{Name: testLookupField13, AllowZero: false}, 77 Fields: func(tt testType) []any { return []any{tt.Field13} }, 78 }, 79 { 80 Lookup: result.Lookup{Name: testLookupField14, AllowZero: false}, 81 Fields: func(tt testType) []any { return []any{tt.Field14} }, 82 }, 83 { 84 Lookup: result.Lookup{Name: testLookupField15, AllowZero: false}, 85 Fields: func(tt testType) []any { return []any{tt.Field15} }, 86 }, 87 { 88 Lookup: result.Lookup{Name: testLookupField16, AllowZero: false}, 89 Fields: func(tt testType) []any { return []any{tt.Field16} }, 90 }, 91 } 92 93 type testType struct { 94 // Each must be unique 95 Field1 string 96 Field2 int 97 Field3 uint 98 Field4 float32 99 Field7 time.Time 100 Field8 *time.Time 101 Field11 []byte 102 Field12 []rune 103 104 // Combined must be unique 105 Field5 string 106 Field6 string 107 Field9 time.Duration 108 Field10 *time.Duration 109 110 // Empty, should be ignored 111 Field13 int 112 Field14 float32 113 Field15 string 114 Field16 []byte 115 } 116 117 var testEntries = []testType{ 118 { 119 Field1: "i am medium", 120 Field2: 42, 121 Field3: 69, 122 Field4: 42.69, 123 Field5: "hello", 124 Field6: "world", 125 Field7: time.Time{}.Add(time.Nanosecond), 126 Field8: func() *time.Time { t := time.Time{}.Add(time.Nanosecond); return &t }(), 127 Field9: time.Nanosecond, 128 Field10: func() *time.Duration { d := time.Nanosecond; return &d }(), 129 Field11: []byte{'0'}, 130 Field12: []rune{'0'}, 131 }, 132 { 133 Field1: "i am small", 134 Field2: math.MinInt, 135 Field3: 1, 136 Field4: math.SmallestNonzeroFloat32, 137 Field5: "hello", 138 Field6: "earth", 139 Field7: time.Time{}.Add(time.Millisecond), 140 Field8: func() *time.Time { t := time.Time{}.Add(time.Millisecond); return &t }(), 141 Field9: time.Millisecond, 142 Field10: func() *time.Duration { d := time.Millisecond; return &d }(), 143 Field11: []byte("hello world"), 144 Field12: []rune("hello world"), 145 }, 146 { 147 Field1: "i am large", 148 Field2: math.MaxInt, 149 Field3: math.MaxUint, 150 Field4: math.MaxFloat32, 151 Field5: "hello", 152 Field6: "moon", 153 Field7: time.Time{}.Add(time.Second), 154 Field8: func() *time.Time { t := time.Time{}.Add(time.Second); return &t }(), 155 Field9: time.Second, 156 Field10: func() *time.Duration { d := time.Second; return &d }(), 157 Field11: []byte{'\n'}, 158 Field12: []rune{'\n'}, 159 }, 160 } 161 162 func TestCache(t *testing.T) { 163 // Convert test lookups to lookup string slice 164 lookups := func() []result.Lookup { 165 var lookups []result.Lookup 166 for _, l := range testLookups { 167 lookups = append(lookups, l.Lookup) 168 } 169 return lookups 170 }() 171 172 // Prepare cache and schedule cleaning 173 c := result.New(lookups, func(tt *testType) *testType { 174 tt2 := new(testType) 175 *tt2 = *tt 176 return tt2 177 }, 3) 178 179 done := make(chan struct{}) 180 go func() { 181 for { 182 // Return if done 183 select { 184 case <-done: 185 return 186 default: 187 } 188 189 // Continually loop checking keys 190 // (puts concurrent strain on cache) 191 for _, entry := range testEntries { 192 for _, lookup := range testLookups { 193 c.Has(lookup.Lookup.Name, lookup.Fields(entry)...) 194 } 195 } 196 } 197 }() 198 defer close(done) 199 200 // Allocate callbacks slice of length >= expected. 201 callbacks := make([]testType, 0, len(testEntries)) 202 203 // Track callbacks performed 204 c.SetInvalidateCallback(func(tt *testType) { 205 t.Logf("-> Invalidate: %+v", tt) 206 callbacks = append(callbacks, *tt) 207 }) 208 c.SetEvictionCallback(func(tt *testType) { 209 t.Logf("-> Evict: %+v", tt) 210 callbacks = append(callbacks, *tt) 211 }) 212 213 // Prepare callback search function 214 findInCallbacks := func(cb []testType, tt testType) bool { 215 for _, entry := range cb { 216 if cmp.Equal(entry, tt) { 217 return true 218 } 219 } 220 return false 221 } 222 223 // Add all entries to cache 224 for i := range testEntries { 225 t.Logf("Cache.Store(%+v)", testEntries[i]) 226 227 if err := c.Store(&(testEntries[i]), func() error { 228 return nil 229 }); err != nil { 230 t.Fatalf("placing entry failed: %v", err) 231 } 232 } 233 234 // Ensure all entries are expected 235 for _, entry := range testEntries { 236 for _, lookup := range testLookups { 237 key := lookup.Fields(entry) 238 zero := true 239 240 // Skip zero value keys 241 for _, field := range key { 242 zero = zero && reflect.ValueOf(field).IsZero() 243 } 244 if zero { 245 continue 246 } 247 248 check, err := c.Load(lookup.Lookup.Name, func() (*testType, error) { 249 return nil, errors.New("item SHOULD be cached") 250 }, lookup.Fields(entry)...) 251 if err != nil { 252 t.Errorf("key unexpectedly not found in cache: %v", err) 253 } else if !cmp.Equal(entry, *check) { 254 t.Errorf("value not as expected for key in cache: %s", lookup.Lookup.Name) 255 } 256 } 257 } 258 259 // Force invalidate, check callbacks 260 for _, entry := range testEntries { 261 lookup := testLookups[0].Lookup 262 key := testLookups[0].Fields(entry) 263 264 t.Logf("Cache.Invalidate(%s,%v)", lookup.Name, key) 265 c.Invalidate(lookup.Name, key...) 266 267 if !findInCallbacks(callbacks, entry) { 268 t.Errorf("invalidate callback unexpectedly not called for: %s,%v", lookup.Name, key) 269 } 270 271 for _, lookup := range testLookups { 272 key := lookup.Fields(entry) 273 if c.Has(lookup.Lookup.Name, key...) { 274 t.Errorf("key unexpected found in cache: %s,%v", lookup.Lookup.Name, key) 275 } 276 } 277 } 278 279 // Reset callbacks 280 callbacks = callbacks[:0] 281 282 // Re-add all entries to cache 283 for i := range testEntries { 284 t.Logf("Cache.Store(%+v)", testEntries[i]) 285 286 if err := c.Store(&(testEntries[i]), func() error { 287 return nil 288 }); err != nil { 289 t.Fatalf("placing entry failed: %v", err) 290 } 291 } 292 }