github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/cache/index/index_test.go (about) 1 //go:build small 2 // +build small 3 4 // Copyright 2018 The WPT Dashboard Project. All rights reserved. 5 // Use of this source code is governed by a BSD-style license that can be 6 // found in the LICENSE file. 7 8 package index 9 10 import ( 11 "errors" 12 "strconv" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/web-platform-tests/wpt.fyi/api/query" 18 19 "github.com/golang/mock/gomock" 20 "github.com/stretchr/testify/assert" 21 "github.com/web-platform-tests/wpt.fyi/shared" 22 metrics "github.com/web-platform-tests/wpt.fyi/shared/metrics" 23 ) 24 25 func TestInvalidNumShards(t *testing.T) { 26 ctrl := gomock.NewController(t) 27 defer ctrl.Finish() 28 loader := NewMockReportLoader(ctrl) 29 _, err := NewShardedWPTIndex(loader, 0) 30 assert.NotNil(t, err) 31 _, err = NewShardedWPTIndex(loader, -1) 32 } 33 34 func TestEvictEmpty(t *testing.T) { 35 ctrl := gomock.NewController(t) 36 defer ctrl.Finish() 37 loader := NewMockReportLoader(ctrl) 38 i, err := NewShardedWPTIndex(loader, 1) 39 assert.Nil(t, err) 40 _, err = i.EvictRuns(0.0) 41 assert.NotNil(t, err) 42 } 43 44 func TestIngestRun_zeroID(t *testing.T) { 45 ctrl := gomock.NewController(t) 46 defer ctrl.Finish() 47 loader := NewMockReportLoader(ctrl) 48 i, err := NewShardedWPTIndex(loader, 1) 49 assert.Nil(t, err) 50 assert.NotNil(t, i.IngestRun(shared.TestRun{ID: 0})) 51 } 52 53 func TestIngestRun_double(t *testing.T) { 54 ctrl := gomock.NewController(t) 55 defer ctrl.Finish() 56 loader := NewMockReportLoader(ctrl) 57 i, err := NewShardedWPTIndex(loader, 1) 58 assert.Nil(t, err) 59 run := shared.TestRun{ 60 ID: 1, 61 RawResultsURL: "http://example.com/results.json", 62 } 63 results := &metrics.TestResultsReport{} 64 loader.EXPECT().Load(run).Return(results, nil) 65 assert.Nil(t, i.IngestRun(run)) 66 assert.NotNil(t, i.IngestRun(run)) 67 } 68 69 func TestIngestRun_concurrent(t *testing.T) { 70 ctrl := gomock.NewController(t) 71 defer ctrl.Finish() 72 loader := NewMockReportLoader(ctrl) 73 i, err := NewShardedWPTIndex(loader, 1) 74 assert.Nil(t, err) 75 run := shared.TestRun{ 76 ID: 1, 77 RawResultsURL: "http://example.com/results.json", 78 } 79 80 // Wait for 2 goroutines to finish. Gate second goroutine's IngestRun() 81 // invocation with channel that receives value after first goroutine has 82 // started to ingest the run. 83 var wg sync.WaitGroup 84 startSecondIngestRun := make(chan bool) 85 wg.Add(2) 86 go func() { 87 defer wg.Done() 88 loader.EXPECT().Load(run).DoAndReturn(func(shared.TestRun) (*metrics.TestResultsReport, error) { 89 // Now that Load(run) has been invoked, i's implementation should have 90 // already marked run as in-flight. Trigger second attempt to ingest run, 91 // and pause a little to let it error-out. 92 startSecondIngestRun <- true 93 time.Sleep(time.Millisecond * 10) 94 return &metrics.TestResultsReport{}, nil 95 }) 96 i.IngestRun(run) 97 }() 98 go func() { 99 defer wg.Done() 100 <-startSecondIngestRun 101 // Expect error during second concurrent attempt to ingest run. 102 assert.NotNil(t, i.IngestRun(run)) 103 }() 104 wg.Wait() 105 } 106 107 func TestIngestRun_loaderError(t *testing.T) { 108 ctrl := gomock.NewController(t) 109 defer ctrl.Finish() 110 loader := NewMockReportLoader(ctrl) 111 i, err := NewShardedWPTIndex(loader, 1) 112 assert.Nil(t, err) 113 run := shared.TestRun{ 114 ID: 1, 115 RawResultsURL: "http://example.com/results.json", 116 } 117 loaderErr := errors.New("Failed to load test results") 118 loader.EXPECT().Load(run).Return(nil, loaderErr) 119 assert.Equal(t, loaderErr, i.IngestRun(run)) 120 } 121 122 func TestEvictNonEmpty(t *testing.T) { 123 ctrl := gomock.NewController(t) 124 defer ctrl.Finish() 125 loader := NewMockReportLoader(ctrl) 126 i, err := NewShardedWPTIndex(loader, 1) 127 assert.Nil(t, err) 128 run := shared.TestRun{ 129 ID: 1, 130 RawResultsURL: "http://example.com/results.json", 131 } 132 results := &metrics.TestResultsReport{ 133 Results: []*metrics.TestResults{ 134 { 135 Test: "a", 136 Status: "PASS", 137 Subtests: []metrics.SubTest{}, 138 }, 139 { 140 Test: "b", 141 Status: "OK", 142 Subtests: []metrics.SubTest{ 143 { 144 Name: "sub", 145 Status: "FAIL", 146 }, 147 }, 148 }, 149 }, 150 } 151 loader.EXPECT().Load(run).Return(results, nil) 152 assert.Nil(t, i.IngestRun(run)) 153 n, err := i.EvictRuns(0.0) 154 assert.Nil(t, err) 155 assert.Equal(t, 1, n) 156 } 157 158 func TestEvictMultiple(t *testing.T) { 159 ctrl := gomock.NewController(t) 160 defer ctrl.Finish() 161 loader := NewMockReportLoader(ctrl) 162 i, err := NewShardedWPTIndex(loader, 1) 163 assert.Nil(t, err) 164 165 run1 := shared.TestRun{ 166 ID: 1, 167 RawResultsURL: "http://example.com/results1.json", 168 } 169 results1 := &metrics.TestResultsReport{ 170 Results: []*metrics.TestResults{ 171 { 172 Test: "a", 173 Status: "PASS", 174 Subtests: []metrics.SubTest{}, 175 }, 176 { 177 Test: "b", 178 Status: "OK", 179 Subtests: []metrics.SubTest{ 180 { 181 Name: "sub", 182 Status: "FAIL", 183 }, 184 }, 185 }, 186 }, 187 } 188 run2 := shared.TestRun{ 189 ID: 2, 190 RawResultsURL: "http://example.com/results2.json", 191 } 192 results2 := &metrics.TestResultsReport{ 193 Results: []*metrics.TestResults{ 194 { 195 Test: "a", 196 Status: "FAIL", 197 Subtests: []metrics.SubTest{}, 198 }, 199 { 200 Test: "b", 201 Status: "OK", 202 Subtests: []metrics.SubTest{ 203 { 204 Name: "sub", 205 Status: "TIMEOUT", 206 }, 207 }, 208 }, 209 }, 210 } 211 run3 := shared.TestRun{ 212 ID: 3, 213 RawResultsURL: "http://example.com/results2.json", 214 } 215 results3 := &metrics.TestResultsReport{ 216 Results: []*metrics.TestResults{ 217 { 218 Test: "a", 219 Status: "PASS", 220 Subtests: []metrics.SubTest{}, 221 }, 222 { 223 Test: "b", 224 Status: "TIMEOUT", 225 Subtests: []metrics.SubTest{}, 226 }, 227 }, 228 } 229 230 loader.EXPECT().Load(run1).Return(results1, nil) 231 loader.EXPECT().Load(run2).Return(results2, nil) 232 loader.EXPECT().Load(run3).Return(results3, nil) 233 234 assert.Nil(t, i.IngestRun(run1)) 235 assert.Nil(t, i.IngestRun(run2)) 236 assert.Nil(t, i.IngestRun(run3)) 237 238 n, err := i.EvictRuns(0.7) 239 assert.Nil(t, err) 240 assert.Equal(t, 2, n) 241 } 242 243 func TestSync(t *testing.T) { 244 ctrl := gomock.NewController(t) 245 defer ctrl.Finish() 246 loader := NewMockReportLoader(ctrl) 247 i, err := NewShardedWPTIndex(loader, 1) 248 assert.Nil(t, err) 249 250 // Populate data with predictable set of two results for each run. 251 loader.EXPECT().Load(gomock.Any()).DoAndReturn(func(run shared.TestRun) (*metrics.TestResultsReport, error) { 252 strID := strconv.FormatInt(run.ID, 10) 253 strStatus := shared.TestStatus(run.ID % 7).String() 254 return &metrics.TestResultsReport{ 255 Results: []*metrics.TestResults{ 256 { 257 Test: "shared", 258 Status: strStatus, 259 }, 260 { 261 Test: "test" + strID, 262 Status: "PASS", 263 }, 264 }, 265 }, nil 266 }).AnyTimes() 267 268 // Baseline before running things in parallel: Index already contains 8 runs. 269 i.IngestRun(makeRun(1)) 270 i.IngestRun(makeRun(2)) 271 i.IngestRun(makeRun(3)) 272 i.IngestRun(makeRun(4)) 273 i.IngestRun(makeRun(5)) 274 i.IngestRun(makeRun(6)) 275 i.IngestRun(makeRun(7)) 276 i.IngestRun(makeRun(8)) 277 278 // Eight times (from run IDs 9 through 16), in parallel: 279 // - Evict one run, 280 // - Add one run, 281 // - Attempt one query (that may fail to bind if it references an already 282 // already evicted run). 283 var wg sync.WaitGroup 284 for j := 9; j <= 16; j++ { 285 wg.Add(1) 286 go func(n int) { 287 defer wg.Done() 288 n, err := i.EvictRuns(0.0) 289 assert.Nil(t, err) 290 assert.Equal(t, 1, n) 291 }(j) 292 wg.Add(1) 293 go func(id int64) { 294 defer wg.Done() 295 i.IngestRun(makeRun(id)) 296 }(int64(j)) 297 wg.Add(1) 298 go func(n int) { 299 defer wg.Done() 300 runs := []shared.TestRun{ 301 makeRun(int64(n - 1)), 302 makeRun(int64(n - 2)), 303 makeRun(int64(n - 3)), 304 makeRun(int64(n - 4)), 305 } 306 c := shared.ParseProductSpecUnsafe("Chrome") 307 plan, err := i.Bind(runs, query.TestStatusEq{ 308 Product: &c, 309 Status: shared.TestStatusPass, 310 }.BindToRuns(runs...)) 311 if err != nil { 312 return 313 } 314 315 plan.Execute(runs, query.AggregationOpts{}) 316 }(j) 317 } 318 wg.Wait() 319 320 // Number of runs should now be 8 + 8 - 8 = 8. 321 // Shards, taken together, should contain data for two predictable run results 322 // for each run still in the index. (See loader.EXPECT()...DoAndReturn(...) 323 // callback above for predictable test names and values.) 324 325 // TODO: Should Index have a Runs() getter for purposes such as this check? 326 idx, ok := i.(*shardedWPTIndex) 327 assert.True(t, ok) 328 329 assert.Equal(t, 8, len(idx.runs)) 330 sharedTestID, err := computeTestID("shared", nil) 331 assert.Nil(t, err) 332 numResults := 0 333 for _, s := range idx.shards { 334 // TODO: Should Results have a getter for purposes such as this check? 335 results, ok := s.results.(*resultsMap) 336 assert.True(t, ok) 337 numRuns := 0 338 results.byRunTest.Range(func(key, value interface{}) bool { 339 numRuns++ 340 return true 341 }) 342 assert.Equal(t, 8, numRuns) 343 344 for _, run := range idx.runs { 345 value, ok := results.byRunTest.Load(RunID(run.ID)) 346 assert.True(t, ok) 347 // TODO: Should Results have a getter for purposes such as this check? 348 res, ok := value.(*runResultsMap) 349 assert.True(t, ok) 350 351 strID := strconv.FormatInt(run.ID, 10) 352 expectedTestID, err := computeTestID("test"+strID, nil) 353 assert.Nil(t, err) 354 355 for testID, resultID := range res.byTest { 356 // Either test is the "shared test" with varied result values across 357 // runs or it is the "test-specific test" with name `test<test ID>` and 358 // result value of "PASS". 359 assert.True(t, sharedTestID == testID || (expectedTestID == testID && resultID == ResultID(shared.TestStatusPass))) 360 numResults++ 361 } 362 } 363 } 364 365 // Total number of results is 8 runs * 2 results per run = 16. 366 assert.Equal(t, 16, numResults) 367 } 368 369 var browsers = []string{ 370 "Chrome", 371 "Edge", 372 "Firefox", 373 "Safari", 374 } 375 376 func makeRun(id int64) shared.TestRun { 377 browserName := browsers[id%int64(len(browsers))] 378 return shared.TestRun{ 379 ID: id, 380 ProductAtRevision: shared.ProductAtRevision{ 381 Product: shared.Product{ 382 BrowserName: browserName, 383 }, 384 }, 385 } 386 } 387 388 // TODO: Add synchronization test to check for race conditions once Bind+Execute 389 // are fully implemented over indexes and filters.