github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/test_run_query.go (about) 1 // Copyright 2019 The WPT Dashboard Project. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 //go:generate mockgen -destination sharedtest/test_run_query_mock.go -package sharedtest github.com/web-platform-tests/wpt.fyi/shared TestRunQuery 6 7 package shared 8 9 import ( 10 "errors" 11 "fmt" 12 "sort" 13 "strconv" 14 "time" 15 16 mapset "github.com/deckarep/golang-set" 17 ) 18 19 var errNoProducts = errors.New("No products specified in request to load test runs") 20 21 // TestRunQuery abstracts complex queries of TestRun entities. 22 type TestRunQuery interface { 23 // LoadTestRuns loads the test runs for the TestRun entities for the given parameters. 24 // It is encapsulated because we cannot run single queries with multiple inequality 25 // filters, so must load the keys and merge the results. 26 LoadTestRuns( 27 products []ProductSpec, 28 labels mapset.Set, 29 revisions []string, 30 from *time.Time, 31 to *time.Time, 32 limit, 33 offset *int) (result TestRunsByProduct, err error) 34 35 // LoadTestRunKeys loads the keys for the TestRun entities for the given parameters. 36 // It is encapsulated because we cannot run single queries with multiple inequality 37 // filters, so must load the keys and merge the results. 38 LoadTestRunKeys( 39 products []ProductSpec, 40 labels mapset.Set, 41 revisions []string, 42 from *time.Time, 43 to *time.Time, 44 limit *int, 45 offset *int) (result KeysByProduct, err error) 46 47 // LoadTestRunsByKeys loads test runs by keys and sets their IDs. 48 LoadTestRunsByKeys(KeysByProduct) (result TestRunsByProduct, err error) 49 50 // GetAlignedRunSHAs returns an array of the SHA[0:10] for runs that 51 // exists for all the given products, ordered by most-recent, as well as a map 52 // of those SHAs to a KeysByProduct map of products to the TestRun keys, for the 53 // runs in the aligned run. 54 GetAlignedRunSHAs( 55 products ProductSpecs, 56 labels mapset.Set, 57 from, 58 to *time.Time, 59 limit *int, 60 offset *int) (shas []string, keys map[string]KeysByProduct, err error) 61 } 62 63 type testRunQueryImpl struct { 64 store Datastore 65 } 66 67 // NewTestRunQuery creates a concrete TestRunQuery backed by a Datastore interface. 68 func NewTestRunQuery(store Datastore) TestRunQuery { 69 return testRunQueryImpl{store} 70 } 71 72 func (t testRunQueryImpl) LoadTestRuns( 73 products []ProductSpec, 74 labels mapset.Set, 75 revisions []string, 76 from *time.Time, 77 to *time.Time, 78 limit, 79 offset *int) (result TestRunsByProduct, err error) { 80 if len(products) == 0 { 81 return nil, errNoProducts 82 } 83 84 keys, err := t.LoadTestRunKeys(products, labels, revisions, from, to, limit, offset) 85 if err != nil { 86 return nil, err 87 } 88 return t.LoadTestRunsByKeys(keys) 89 } 90 91 func (t testRunQueryImpl) LoadTestRunsByKeys(keysByProduct KeysByProduct) (result TestRunsByProduct, err error) { 92 result = TestRunsByProduct{} 93 for _, kbp := range keysByProduct { 94 runs := make(TestRuns, len(kbp.Keys)) 95 if err := t.store.GetMulti(kbp.Keys, runs); err != nil { 96 return nil, err 97 } 98 result = append(result, ProductTestRuns{ 99 Product: kbp.Product, 100 TestRuns: runs, 101 }) 102 } 103 104 // Append the keys as ID 105 for i, kbp := range keysByProduct { 106 result[i].TestRuns.SetTestRunIDs(GetTestRunIDs(kbp.Keys)) 107 } 108 return result, err 109 } 110 111 func (t testRunQueryImpl) LoadTestRunKeys( 112 products []ProductSpec, 113 labels mapset.Set, 114 revisions []string, 115 from *time.Time, 116 to *time.Time, 117 limit *int, 118 offset *int) (result KeysByProduct, err error) { 119 log := GetLogger(t.store.Context()) 120 result = make(KeysByProduct, len(products)) 121 baseQuery := t.store.NewQuery("TestRun") 122 if offset != nil { 123 baseQuery = baseQuery.Offset(*offset) 124 } 125 if labels != nil { 126 labels.Remove("") // Ensure the empty string isn't present. 127 for i := range labels.Iter() { 128 baseQuery = baseQuery.Filter("Labels =", i.(string)) 129 } 130 } 131 var globalIDFilter mapset.Set 132 if len(revisions) > 1 || len(revisions) == 1 && !IsLatest(revisions[0]) { 133 globalIDFilter = mapset.NewSet() 134 for _, sha := range revisions { 135 var ids mapset.Set 136 if ids, err = loadIDsForRevision(t.store, baseQuery, sha); err != nil { 137 return nil, err 138 } 139 globalIDFilter = globalIDFilter.Union(ids) 140 } 141 log.Debugf("Found %d keys across %d revisions", globalIDFilter.Cardinality(), len(revisions)) 142 } 143 144 for i, product := range products { 145 var productIDFilter = merge(globalIDFilter, nil) 146 query := baseQuery.Filter("BrowserName =", product.BrowserName) 147 if product.Labels != nil { 148 for i := range product.Labels.Iter() { 149 query = query.Filter("Labels =", i.(string)) 150 } 151 } 152 if !IsLatest(product.Revision) { 153 var revIDFilter mapset.Set 154 if revIDFilter, err = loadIDsForRevision(t.store, query, product.Revision); err != nil { 155 return nil, err 156 } 157 log.Debugf("Found %v keys for %s@%s", revIDFilter.Cardinality(), product.BrowserName, product.Revision) 158 productIDFilter = merge(productIDFilter, revIDFilter) 159 } 160 if product.BrowserVersion != "" { 161 var versionIDs mapset.Set 162 if versionIDs, err = loadIDsForBrowserVersion(t.store, query, product.BrowserVersion); err != nil { 163 return nil, err 164 } 165 log.Debugf("Found %v keys for %s", versionIDs.Cardinality(), product.BrowserVersion) 166 productIDFilter = merge(productIDFilter, versionIDs) 167 } 168 169 // If we have a specific set of possibilities, it's much cheaper to 170 // turn the query on its head (filter the entities). 171 var keys []Key 172 if productIDFilter != nil { 173 keys, err = clientSideFilter(t.store, product, productIDFilter, from, to, limit) 174 if err != nil { 175 return nil, err 176 } 177 } else { 178 // Otherwise, just run a "GetAll" filter. Expensive. 179 log.Debugf("Falling back to GetAll datastore query.") 180 // TODO(lukebjerring): Indexes + filtering for OS + version. 181 query = query.Order("-TimeStart") 182 if from != nil { 183 query = query.Filter("TimeStart >=", *from) 184 } 185 if to != nil { 186 query = query.Filter("TimeStart <", *to) 187 } 188 max := MaxCountMaxValue 189 if limit != nil && *limit < MaxCountMaxValue { 190 max = *limit 191 } 192 keys, err = t.store.GetAll(query.KeysOnly().Limit(max), nil) 193 if err != nil { 194 return nil, err 195 } 196 log.Debugf("Loaded %v results for %s", len(keys), product.String()) 197 } 198 199 log.Debugf("Found %v results for %s", len(keys), product.String()) 200 result[i] = ProductTestRunKeys{ 201 Product: product, 202 Keys: keys, 203 } 204 } 205 return result, nil 206 } 207 208 func clientSideFilter( 209 store Datastore, 210 product ProductSpec, 211 productIDFilter mapset.Set, 212 from, 213 to *time.Time, 214 limit *int) (keys []Key, err error) { 215 log := GetLogger(store.Context()) 216 capacity := productIDFilter.Cardinality() 217 if productIDFilter.Cardinality() > MaxKeysPerLookup { 218 log.Warningf("%d viable runs exceed the lookup limit %d", productIDFilter.Cardinality(), MaxKeysPerLookup) 219 capacity = MaxKeysPerLookup 220 } 221 log.Debugf("Loading %d viable runs to filter.", capacity) 222 keys = make([]Key, 0, capacity) 223 for key := range productIDFilter.Iter() { 224 keys = append(keys, store.NewIDKey("TestRun", key.(int64))) 225 if len(keys) == capacity { 226 // FIXME: This might produce incomplete results. 227 // https://github.com/web-platform-tests/wpt.fyi/pull/1914 228 break 229 } 230 } 231 runs := make(TestRuns, len(keys)) 232 err = store.GetMulti(keys, runs) 233 if err != nil { 234 return nil, err 235 } 236 runs.SetTestRunIDs(GetTestRunIDs(keys)) 237 // TestRuns sorted by TimeStart asc by default 238 sort.Sort(sort.Reverse(runs)) 239 keys = make([]Key, 0) 240 for _, run := range runs { 241 if !product.Matches(run) || 242 from != nil && !from.Before(run.TimeStart) || 243 to != nil && !run.TimeStart.Before(*to) { 244 continue 245 } 246 keys = append(keys, store.NewIDKey("TestRun", run.ID)) 247 } 248 if limit != nil && len(keys) >= *limit { 249 keys = keys[:*limit] 250 } else if len(keys) >= MaxCountMaxValue { 251 keys = keys[:MaxCountMaxValue] 252 } 253 return keys, nil 254 } 255 256 func (t testRunQueryImpl) GetAlignedRunSHAs( 257 products ProductSpecs, 258 labels mapset.Set, 259 from, 260 to *time.Time, 261 limit *int, 262 offset *int) (shas []string, keys map[string]KeysByProduct, err error) { 263 if limit == nil { 264 maxMax := MaxCountMaxValue 265 limit = &maxMax 266 } 267 query := t.store. 268 NewQuery("TestRun"). 269 Order("-TimeStart") 270 271 if labels != nil { 272 for i := range labels.Iter() { 273 query = query.Filter("Labels =", i.(string)) 274 } 275 } 276 if from != nil { 277 query = query.Filter("TimeStart >=", *from) 278 } 279 if to != nil { 280 query = query.Filter("TimeStart <", *to) 281 } 282 283 productsBySHA := make(map[string]mapset.Set) 284 keyCollector := make(map[string]KeysByProduct) 285 keys = make(map[string]KeysByProduct) 286 done := mapset.NewSet() 287 it := query.Run(t.store) 288 for { 289 var testRun TestRun 290 var key Key 291 matchingProduct := -1 292 key, err := it.Next(&testRun) 293 if err == t.store.Done() { 294 break 295 } else if err != nil { 296 return nil, nil, err 297 } else { 298 for i := range products { 299 if products[i].Matches(testRun) { 300 matchingProduct = i 301 break 302 } 303 } 304 } 305 if matchingProduct < 0 { 306 continue 307 } 308 if _, ok := productsBySHA[testRun.Revision]; !ok { 309 productsBySHA[testRun.Revision] = mapset.NewSet() 310 keyCollector[testRun.Revision] = make(KeysByProduct, len(products)) 311 } 312 set := productsBySHA[testRun.Revision] 313 if set.Contains(matchingProduct) { 314 continue 315 } 316 set.Add(matchingProduct) 317 keyCollector[testRun.Revision][matchingProduct].Keys = []Key{key} 318 if set.Cardinality() == len(products) && !done.Contains(testRun.Revision) { 319 if offset == nil || done.Cardinality() >= *offset { 320 shas = append(shas, testRun.Revision) 321 } 322 done.Add(testRun.Revision) 323 keys[testRun.Revision] = keyCollector[testRun.Revision] 324 if len(shas) >= *limit { 325 return shas, keys, nil 326 } 327 } 328 } 329 return shas, keys, err 330 } 331 332 // merge gives the set of elements present in both of the given sets (Intersect). 333 // If one of the sets is nil, returns a set with the contents of the non-nil set. 334 // If both sets are nil, returns nil. 335 func merge(s1, s2 mapset.Set) mapset.Set { 336 if s1 == nil && s2 == nil { 337 return nil 338 } else if s1 == nil { 339 return merge(s2, nil) 340 } else if s2 == nil { 341 return mapset.NewSetWith(s1.ToSlice()...) 342 } 343 return s1.Intersect(s2) 344 } 345 346 func contains(s []string, x string) bool { 347 for _, v := range s { 348 if v == x { 349 return true 350 } 351 } 352 return false 353 } 354 355 // Loads any keys for a revision prefix or full string match 356 func loadIDsForRevision(store Datastore, query Query, sha string) (result mapset.Set, err error) { 357 log := GetLogger(store.Context()) 358 var revQuery Query 359 if len(sha) < 40 { 360 log.Debugf("Finding revisions %s <= SHA < %s", sha, sha+"g") 361 revQuery = query. 362 Order("FullRevisionHash"). 363 Limit(MaxCountMaxValue). 364 Filter("FullRevisionHash >=", sha). 365 Filter("FullRevisionHash <", sha+"g") // g > f 366 } else { 367 log.Debugf("Finding exact revision %s", sha) 368 revQuery = query.Filter("FullRevisionHash =", sha[:40]) 369 } 370 371 var keys []Key 372 if keys, err = store.GetAll(revQuery.KeysOnly(), nil); err != nil { 373 return nil, err 374 } 375 result = mapset.NewSet() 376 for _, id := range GetTestRunIDs(keys) { 377 result.Add(id) 378 } 379 return result, nil 380 } 381 382 // Loads any keys for a full string match or a version prefix (Between [version].* and [version].9*). 383 // Entries in the set are the int64 value of the keys. 384 func loadIDsForBrowserVersion(store Datastore, query Query, version string) (result mapset.Set, err error) { 385 result = mapset.NewSet() 386 // By prefix 387 var keys []Key 388 versionQuery := VersionPrefix(query, "BrowserVersion", version, true) 389 if keys, err = store.GetAll(versionQuery.KeysOnly(), nil); err != nil { 390 return nil, err 391 } 392 for _, id := range GetTestRunIDs(keys) { 393 result.Add(id) 394 } 395 // By exact match 396 if keys, err = store.GetAll(query.Filter("BrowserVersion =", version).KeysOnly(), nil); err != nil { 397 return nil, err 398 } 399 for _, id := range GetTestRunIDs(keys) { 400 result.Add(id) 401 } 402 return result, nil 403 } 404 405 // VersionPrefix returns the given query with a prefix filter on the given 406 // field name, using the >= and < filters. 407 func VersionPrefix(query Query, fieldName, versionPrefix string, desc bool) Query { 408 order := fieldName 409 if desc { 410 order = "-" + order 411 } 412 return query. 413 Limit(MaxCountMaxValue). 414 Order(order). 415 Filter(fieldName+" >=", fmt.Sprintf("%s.", versionPrefix)). 416 Filter(fieldName+" <=", fmt.Sprintf("%s.%c", versionPrefix, '9'+1)) 417 } 418 419 func getTestRunRedisKey(id int64) string { 420 return "TEST_RUN-" + strconv.FormatInt(id, 10) 421 }