|
1 | 1 | package plugin |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "errors" |
| 5 | + "sync" |
4 | 6 | "testing" |
5 | 7 | "time" |
| 8 | + |
| 9 | + "github.com/grafana/grafana-plugin-sdk-go/backend" |
6 | 10 | ) |
7 | 11 |
|
8 | 12 | func TestTypedCache_BasicOperations(t *testing.T) { |
@@ -65,3 +69,237 @@ func TestTypedCache_WithStructKeys(t *testing.T) { |
65 | 69 | t.Errorf("Expected 200, got %d", value) |
66 | 70 | } |
67 | 71 | } |
| 72 | + |
| 73 | +func TestTypedCacheWithLoader_GetOrWait_CacheHit(t *testing.T) { |
| 74 | + // Create a typed cache |
| 75 | + cache := NewTypedCache[string, string](5*time.Minute, 10*time.Minute) |
| 76 | + |
| 77 | + // Pre-populate the cache |
| 78 | + cache.Set("test-key", "cached-value") |
| 79 | + |
| 80 | + // Create a loader that should not be called |
| 81 | + loader := func(d *SiftDatasource, ctx backend.PluginContext, key string) (string, error) { |
| 82 | + t.Error("Loader should not be called when value is in cache") |
| 83 | + return "", errors.New("should not be called") |
| 84 | + } |
| 85 | + |
| 86 | + // Create cache with loader |
| 87 | + cacheWithLoader := NewTypedCacheWithLoader(cache, loader, func(k string) string { return k }) |
| 88 | + |
| 89 | + // Mock datasource and context |
| 90 | + mockDatasource := &SiftDatasource{} |
| 91 | + mockContext := backend.PluginContext{} |
| 92 | + |
| 93 | + // Test GetOrWait - should return cached value |
| 94 | + result, err := cacheWithLoader.GetOrWait(mockDatasource, mockContext, "test-key") |
| 95 | + |
| 96 | + if err != nil { |
| 97 | + t.Errorf("Expected no error, got %v", err) |
| 98 | + } |
| 99 | + if result != "cached-value" { |
| 100 | + t.Errorf("Expected 'cached-value', got %s", result) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +func TestTypedCacheWithLoader_GetOrWait_CacheMiss(t *testing.T) { |
| 105 | + // Create an empty typed cache |
| 106 | + cache := NewTypedCache[string, string](5*time.Minute, 10*time.Minute) |
| 107 | + |
| 108 | + // Create a loader that returns a value |
| 109 | + loaderCalled := false |
| 110 | + loader := func(d *SiftDatasource, ctx backend.PluginContext, key string) (string, error) { |
| 111 | + loaderCalled = true |
| 112 | + if key == "test-key" { |
| 113 | + return "loaded-value", nil |
| 114 | + } |
| 115 | + return "", errors.New("unexpected key") |
| 116 | + } |
| 117 | + |
| 118 | + // Create cache with loader |
| 119 | + cacheWithLoader := NewTypedCacheWithLoader(cache, loader, func(k string) string { return k }) |
| 120 | + |
| 121 | + // Mock datasource and context |
| 122 | + mockDatasource := &SiftDatasource{} |
| 123 | + mockContext := backend.PluginContext{} |
| 124 | + |
| 125 | + // Test GetOrWait - should call loader and return loaded value |
| 126 | + result, err := cacheWithLoader.GetOrWait(mockDatasource, mockContext, "test-key") |
| 127 | + |
| 128 | + if err != nil { |
| 129 | + t.Errorf("Expected no error, got %v", err) |
| 130 | + } |
| 131 | + if result != "loaded-value" { |
| 132 | + t.Errorf("Expected 'loaded-value', got %s", result) |
| 133 | + } |
| 134 | + if !loaderCalled { |
| 135 | + t.Error("Expected loader to be called") |
| 136 | + } |
| 137 | + |
| 138 | + // Verify value was cached |
| 139 | + cachedValue, found := cache.Get("test-key") |
| 140 | + if !found { |
| 141 | + t.Error("Expected value to be cached") |
| 142 | + } |
| 143 | + if cachedValue != "loaded-value" { |
| 144 | + t.Errorf("Expected cached value 'loaded-value', got %s", cachedValue) |
| 145 | + } |
| 146 | +} |
| 147 | + |
| 148 | +func TestTypedCacheWithLoader_GetOrWait_LoaderError(t *testing.T) { |
| 149 | + // Create an empty typed cache |
| 150 | + cache := NewTypedCache[string, string](5*time.Minute, 10*time.Minute) |
| 151 | + |
| 152 | + // Create a loader that returns an error |
| 153 | + expectedError := errors.New("loader failed") |
| 154 | + loader := func(d *SiftDatasource, ctx backend.PluginContext, key string) (string, error) { |
| 155 | + return "", expectedError |
| 156 | + } |
| 157 | + |
| 158 | + // Create cache with loader |
| 159 | + cacheWithLoader := NewTypedCacheWithLoader(cache, loader, func(k string) string { return k }) |
| 160 | + |
| 161 | + // Mock datasource and context |
| 162 | + mockDatasource := &SiftDatasource{} |
| 163 | + mockContext := backend.PluginContext{} |
| 164 | + |
| 165 | + // Test GetOrWait - should return error from loader |
| 166 | + result, err := cacheWithLoader.GetOrWait(mockDatasource, mockContext, "test-key") |
| 167 | + |
| 168 | + if err != expectedError { |
| 169 | + t.Errorf("Expected error %v, got %v", expectedError, err) |
| 170 | + } |
| 171 | + if result != "" { |
| 172 | + t.Errorf("Expected empty result on error, got %s", result) |
| 173 | + } |
| 174 | + |
| 175 | + // Verify value was not cached on error |
| 176 | + _, found := cache.Get("test-key") |
| 177 | + if found { |
| 178 | + t.Error("Expected value not to be cached on loader error") |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +func TestTypedCacheWithLoader_GetOrWait_ConcurrentLoads(t *testing.T) { |
| 183 | + // Create an empty typed cache |
| 184 | + cache := NewTypedCache[string, string](5*time.Minute, 10*time.Minute) |
| 185 | + |
| 186 | + // Create a loader that simulates slow loading |
| 187 | + loaderCallCount := 0 |
| 188 | + var loaderMutex sync.Mutex |
| 189 | + loader := func(d *SiftDatasource, ctx backend.PluginContext, key string) (string, error) { |
| 190 | + loaderMutex.Lock() |
| 191 | + loaderCallCount++ |
| 192 | + loaderMutex.Unlock() |
| 193 | + |
| 194 | + // Simulate slow operation |
| 195 | + time.Sleep(100 * time.Millisecond) |
| 196 | + return "loaded-value", nil |
| 197 | + } |
| 198 | + |
| 199 | + // Create cache with loader |
| 200 | + cacheWithLoader := NewTypedCacheWithLoader(cache, loader, func(k string) string { return k }) |
| 201 | + |
| 202 | + // Mock datasource and context |
| 203 | + mockDatasource := &SiftDatasource{} |
| 204 | + mockContext := backend.PluginContext{} |
| 205 | + |
| 206 | + // Launch multiple concurrent GetOrWait calls |
| 207 | + const numGoroutines = 5 |
| 208 | + var wg sync.WaitGroup |
| 209 | + results := make([]string, numGoroutines) |
| 210 | + errors := make([]error, numGoroutines) |
| 211 | + |
| 212 | + for i := 0; i < numGoroutines; i++ { |
| 213 | + wg.Add(1) |
| 214 | + go func(index int) { |
| 215 | + defer wg.Done() |
| 216 | + result, err := cacheWithLoader.GetOrWait(mockDatasource, mockContext, "test-key") |
| 217 | + results[index] = result |
| 218 | + errors[index] = err |
| 219 | + }(i) |
| 220 | + } |
| 221 | + |
| 222 | + wg.Wait() |
| 223 | + |
| 224 | + // Verify all calls succeeded and returned the same value |
| 225 | + for i := 0; i < numGoroutines; i++ { |
| 226 | + if errors[i] != nil { |
| 227 | + t.Errorf("Goroutine %d got error: %v", i, errors[i]) |
| 228 | + } |
| 229 | + if results[i] != "loaded-value" { |
| 230 | + t.Errorf("Goroutine %d got result %s, expected 'loaded-value'", i, results[i]) |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + // Verify loader was called only once despite multiple concurrent requests |
| 235 | + loaderMutex.Lock() |
| 236 | + if loaderCallCount != 1 { |
| 237 | + t.Errorf("Expected loader to be called exactly once, but was called %d times", loaderCallCount) |
| 238 | + } |
| 239 | + loaderMutex.Unlock() |
| 240 | + |
| 241 | + // Verify value was cached |
| 242 | + cachedValue, found := cache.Get("test-key") |
| 243 | + if !found { |
| 244 | + t.Error("Expected value to be cached") |
| 245 | + } |
| 246 | + if cachedValue != "loaded-value" { |
| 247 | + t.Errorf("Expected cached value 'loaded-value', got %s", cachedValue) |
| 248 | + } |
| 249 | +} |
| 250 | + |
| 251 | +func TestTypedCacheWithLoader_GetOrWait_WithComplexTypes(t *testing.T) { |
| 252 | + // Test with complex key and value types |
| 253 | + type TestKey struct { |
| 254 | + AssetID string |
| 255 | + RunID string |
| 256 | + } |
| 257 | + |
| 258 | + type TestValue struct { |
| 259 | + Data string |
| 260 | + Timestamp time.Time |
| 261 | + } |
| 262 | + |
| 263 | + // Create cache with complex types |
| 264 | + cache := NewTypedCache[string, TestValue](5*time.Minute, 10*time.Minute) |
| 265 | + |
| 266 | + // Create a loader |
| 267 | + loader := func(d *SiftDatasource, ctx backend.PluginContext, key TestKey) (TestValue, error) { |
| 268 | + return TestValue{ |
| 269 | + Data: "complex-data-" + key.AssetID, |
| 270 | + Timestamp: time.Now(), |
| 271 | + }, nil |
| 272 | + } |
| 273 | + |
| 274 | + // Key conversion function |
| 275 | + keyToComparable := func(k TestKey) string { |
| 276 | + return k.AssetID + ":" + k.RunID |
| 277 | + } |
| 278 | + |
| 279 | + // Create cache with loader |
| 280 | + cacheWithLoader := NewTypedCacheWithLoader(cache, loader, keyToComparable) |
| 281 | + |
| 282 | + // Mock datasource and context |
| 283 | + mockDatasource := &SiftDatasource{} |
| 284 | + mockContext := backend.PluginContext{} |
| 285 | + |
| 286 | + // Test with complex key |
| 287 | + testKey := TestKey{AssetID: "asset123", RunID: "run456"} |
| 288 | + result, err := cacheWithLoader.GetOrWait(mockDatasource, mockContext, testKey) |
| 289 | + |
| 290 | + if err != nil { |
| 291 | + t.Errorf("Expected no error, got %v", err) |
| 292 | + } |
| 293 | + if result.Data != "complex-data-asset123" { |
| 294 | + t.Errorf("Expected 'complex-data-asset123', got %s", result.Data) |
| 295 | + } |
| 296 | + |
| 297 | + // Verify caching with the converted key |
| 298 | + cachedValue, found := cache.Get("asset123:run456") |
| 299 | + if !found { |
| 300 | + t.Error("Expected value to be cached with converted key") |
| 301 | + } |
| 302 | + if cachedValue.Data != "complex-data-asset123" { |
| 303 | + t.Errorf("Expected cached data 'complex-data-asset123', got %s", cachedValue.Data) |
| 304 | + } |
| 305 | +} |
0 commit comments