github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/models.go (about) 1 // Copyright 2017 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 package shared 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "time" 11 12 "cloud.google.com/go/datastore" 13 mapset "github.com/deckarep/golang-set" 14 ) 15 16 // Product uniquely defines a browser version, running on an OS version. 17 type Product struct { 18 BrowserName string `json:"browser_name"` 19 BrowserVersion string `json:"browser_version"` 20 OSName string `json:"os_name"` 21 OSVersion string `json:"os_version"` 22 } 23 24 func (p Product) String() string { 25 s := p.BrowserName 26 if p.BrowserVersion != "" { 27 s = fmt.Sprintf("%s-%s", s, p.BrowserVersion) 28 } 29 if p.OSName != "" { 30 s = fmt.Sprintf("%s-%s", s, p.OSName) 31 if p.OSVersion != "" { 32 s = fmt.Sprintf("%s-%s", s, p.OSVersion) 33 } 34 } 35 return s 36 } 37 38 // ByBrowserName is a []Product sortable by BrowserName values. 39 type ByBrowserName []Product 40 41 func (e ByBrowserName) Len() int { return len(e) } 42 func (e ByBrowserName) Swap(i, j int) { e[i], e[j] = e[j], e[i] } 43 func (e ByBrowserName) Less(i, j int) bool { return e[i].BrowserName < e[j].BrowserName } 44 45 // Version is a struct for a parsed version string. 46 type Version struct { 47 Major int 48 Minor *int 49 Build *int 50 Revision *int 51 Channel string 52 } 53 54 func (v Version) String() string { 55 s := fmt.Sprintf("%v", v.Major) 56 if v.Minor != nil { 57 s = fmt.Sprintf("%s.%v", s, *v.Minor) 58 } 59 if v.Build != nil { 60 s = fmt.Sprintf("%s.%v", s, *v.Build) 61 } 62 if v.Revision != nil { 63 s = fmt.Sprintf("%s.%v", s, *v.Revision) 64 } 65 if v.Channel != "" { 66 s = fmt.Sprintf("%s%s", s, v.Channel) 67 } 68 return s 69 } 70 71 // ProductAtRevision defines a WPT run for a specific product, at a 72 // specific hash of the WPT repo. 73 type ProductAtRevision struct { 74 Product 75 76 // The first 10 characters of the SHA1 of the tested WPT revision. 77 // 78 // Deprecated: The authoritative git revision indicator is FullRevisionHash. 79 Revision string `json:"revision"` 80 81 // The complete SHA1 hash of the tested WPT revision. 82 FullRevisionHash string `json:"full_revision_hash"` 83 } 84 85 func (p ProductAtRevision) String() string { 86 return fmt.Sprintf("%s@%s", p.Product.String(), p.Revision) 87 } 88 89 // TestRun stores metadata for a test run (produced by run/run.py) 90 type TestRun struct { 91 ID int64 `json:"id" datastore:"-"` 92 93 ProductAtRevision 94 95 // URL for summary of results, which is derived from raw results. 96 ResultsURL string `json:"results_url"` 97 98 // Time when the test run metadata was first created. 99 CreatedAt time.Time `json:"created_at"` 100 101 // Time when the test run started. 102 TimeStart time.Time `json:"time_start"` 103 104 // Time when the test run ended. 105 TimeEnd time.Time `json:"time_end"` 106 107 // URL for raw results JSON object. Resembles the JSON output of the 108 // wpt report tool. 109 RawResultsURL string `json:"raw_results_url"` 110 111 // Labels for the test run. 112 Labels []string `json:"labels"` 113 } 114 115 // IsExperimental returns true if the run is labelled experimental. 116 func (r TestRun) IsExperimental() bool { 117 return r.hasLabel(ExperimentalLabel) 118 } 119 120 // IsPRBase returns true if the run is labelled experimental. 121 func (r TestRun) IsPRBase() bool { 122 return r.hasLabel(PRBaseLabel) 123 } 124 125 func (r TestRun) hasLabel(label string) bool { 126 return StringSliceContains(r.Labels, label) 127 } 128 129 // Channel return the channel label, if any, for the given run. 130 func (r TestRun) Channel() string { 131 for _, label := range r.Labels { 132 switch label { 133 case StableLabel, 134 BetaLabel, 135 ExperimentalLabel: 136 return label 137 } 138 } 139 return "" 140 } 141 142 // Load is part of the datastore.PropertyLoadSaver interface. 143 // We use it to reset all time to UTC and trim their monotonic clock. 144 func (r *TestRun) Load(ps []datastore.Property) error { 145 if err := datastore.LoadStruct(r, ps); err != nil { 146 return err 147 } 148 r.CreatedAt = r.CreatedAt.UTC().Round(0) 149 r.TimeStart = r.TimeStart.UTC().Round(0) 150 r.TimeEnd = r.TimeEnd.UTC().Round(0) 151 return nil 152 } 153 154 // Save is part of the datastore.PropertyLoadSaver interface. 155 // Delegate to the default behaviour. 156 func (r *TestRun) Save() ([]datastore.Property, error) { 157 return datastore.SaveStruct(r) 158 } 159 160 // PendingTestRunStage represents the stage of a test run in its life cycle. 161 type PendingTestRunStage int 162 163 // Constant enums for PendingTestRunStage 164 const ( 165 StageGitHubQueued PendingTestRunStage = 100 166 StageGitHubInProgress PendingTestRunStage = 200 167 StageCIRunning PendingTestRunStage = 300 168 StageCIFinished PendingTestRunStage = 400 169 StageGitHubSuccess PendingTestRunStage = 500 170 StageGitHubFailure PendingTestRunStage = 550 171 StageWptFyiReceived PendingTestRunStage = 600 172 StageWptFyiProcessing PendingTestRunStage = 700 173 StageValid PendingTestRunStage = 800 174 StageInvalid PendingTestRunStage = 850 175 StageEmpty PendingTestRunStage = 851 176 StageDuplicate PendingTestRunStage = 852 177 ) 178 179 func (s PendingTestRunStage) String() string { 180 switch s { 181 case StageGitHubQueued: 182 return "GITHUB_QUEUED" 183 case StageGitHubInProgress: 184 return "GITHUB_IN_PROGRESS" 185 case StageCIRunning: 186 return "CI_RUNNING" 187 case StageCIFinished: 188 return "CI_FINISHED" 189 case StageGitHubSuccess: 190 return "GITHUB_SUCCESS" 191 case StageGitHubFailure: 192 return "GITHUB_FAILURE" 193 case StageWptFyiReceived: 194 return "WPTFYI_RECEIVED" 195 case StageWptFyiProcessing: 196 return "WPTFYI_PROCESSING" 197 case StageValid: 198 return "VALID" 199 case StageInvalid: 200 return "INVALID" 201 case StageEmpty: 202 return "EMPTY" 203 case StageDuplicate: 204 return "DUPLICATE" 205 } 206 return "" 207 } 208 209 // MarshalJSON is the custom JSON marshaler for PendingTestRunStage. 210 func (s PendingTestRunStage) MarshalJSON() ([]byte, error) { 211 return json.Marshal(s.String()) 212 } 213 214 // UnmarshalJSON is the custom JSON unmarshaler for PendingTestRunStage. 215 func (s *PendingTestRunStage) UnmarshalJSON(b []byte) error { 216 var str string 217 if err := json.Unmarshal(b, &str); err != nil { 218 return err 219 } 220 switch str { 221 case "GITHUB_QUEUED": 222 *s = StageGitHubQueued 223 case "GITHUB_IN_PROGRESS": 224 *s = StageGitHubInProgress 225 case "CI_RUNNING": 226 *s = StageCIRunning 227 case "CI_FINISHED": 228 *s = StageCIFinished 229 case "GITHUB_SUCCESS": 230 *s = StageGitHubSuccess 231 case "GITHUB_FAILURE": 232 *s = StageGitHubFailure 233 case "WPTFYI_RECEIVED": 234 *s = StageWptFyiReceived 235 case "WPTFYI_PROCESSING": 236 *s = StageWptFyiProcessing 237 case "VALID": 238 *s = StageValid 239 case "INVALID": 240 *s = StageInvalid 241 case "EMPTY": 242 *s = StageEmpty 243 case "DUPLICATE": 244 *s = StageDuplicate 245 default: 246 return fmt.Errorf("unknown stage: %s", str) 247 } 248 if s.String() != str { 249 return fmt.Errorf("enum conversion error: %s != %s", s.String(), str) 250 } 251 return nil 252 } 253 254 // PendingTestRun represents a TestRun that has started, but is not yet 255 // completed. 256 type PendingTestRun struct { 257 ID int64 `json:"id" datastore:"-"` 258 ProductAtRevision 259 CheckRunID int64 `json:"check_run_id" datastore:",omitempty"` 260 Uploader string `json:"uploader"` 261 Error string `json:"error" datastore:",omitempty"` 262 Stage PendingTestRunStage `json:"stage"` 263 264 Created time.Time `json:"created"` 265 Updated time.Time `json:"updated"` 266 } 267 268 // Transition sets Stage to next if the transition is allowed; otherwise an 269 // error is returned. 270 func (s *PendingTestRun) Transition(next PendingTestRunStage) error { 271 if next == 0 || s.Stage > next { 272 return fmt.Errorf("cannot transition from %s to %s", s.Stage.String(), next.String()) 273 } 274 s.Stage = next 275 return nil 276 } 277 278 // Load is part of the datastore.PropertyLoadSaver interface. 279 // We use it to reset all time to UTC and trim their monotonic clock. 280 func (s *PendingTestRun) Load(ps []datastore.Property) error { 281 if err := datastore.LoadStruct(s, ps); err != nil { 282 return err 283 } 284 s.Created = s.Created.UTC().Round(0) 285 s.Updated = s.Updated.UTC().Round(0) 286 return nil 287 } 288 289 // Save is part of the datastore.PropertyLoadSaver interface. 290 // Delegate to the default behaviour. 291 func (s *PendingTestRun) Save() ([]datastore.Property, error) { 292 return datastore.SaveStruct(s) 293 } 294 295 // PendingTestRunByUpdated sorts the pending test runs by updated (asc) 296 type PendingTestRunByUpdated []PendingTestRun 297 298 func (a PendingTestRunByUpdated) Len() int { return len(a) } 299 func (a PendingTestRunByUpdated) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 300 func (a PendingTestRunByUpdated) Less(i, j int) bool { return a[i].Updated.Before(a[j].Updated) } 301 302 // TestHistoryEntry formats Test History data for the datastore. 303 type TestHistoryEntry struct { 304 BrowserName string 305 RunID string 306 Date string 307 TestName string 308 SubtestName string 309 Status string 310 } 311 312 // CheckSuite entities represent a GitHub check request that has been noted by 313 // wpt.fyi, and will cause creation of a completed check_run when results arrive 314 // for the PR. 315 type CheckSuite struct { 316 // SHA of the revision that requested a check suite. 317 SHA string `json:"sha"` 318 // The GitHub app ID for the custom wpt.fyi check. 319 AppID int64 `json:"app_id"` 320 // The GitHub app installation ID for custom wpt.fyi check 321 InstallationID int64 `json:"installation"` 322 Owner string `json:"owner"` // Owner username 323 Repo string `json:"repo"` 324 PRNumbers []int `json:"pr_numbers"` 325 } 326 327 // LabelsSet creates a set from the run's labels. 328 func (r TestRun) LabelsSet() mapset.Set { 329 runLabels := mapset.NewSet() 330 for _, label := range r.Labels { 331 runLabels.Add(label) 332 } 333 return runLabels 334 } 335 336 // TestRuns is a helper type for an array of TestRun entities. 337 type TestRuns []TestRun 338 339 func (t TestRuns) Len() int { return len(t) } 340 func (t TestRuns) Less(i, j int) bool { return t[i].TimeStart.Before(t[j].TimeStart) } 341 func (t TestRuns) Swap(i, j int) { t[i], t[j] = t[j], t[i] } 342 343 // SetTestRunIDs sets the ID field for each run, from the given ids. 344 func (t TestRuns) SetTestRunIDs(ids TestRunIDs) { 345 for i := 0; i < len(ids) && i < len(t); i++ { 346 t[i].ID = ids[i] 347 } 348 } 349 350 // GetTestRunIDs gets an array of the IDs for the TestRun entities in the array. 351 func (t TestRuns) GetTestRunIDs() TestRunIDs { 352 ids := make([]int64, len(t)) 353 for i, run := range t { 354 ids[i] = run.ID 355 } 356 return ids 357 } 358 359 // OldestRunTimeStart returns the TimeStart of the oldest run in the set. 360 func (t TestRuns) OldestRunTimeStart() time.Time { 361 if len(t) < 1 { 362 return time.Time{} 363 } 364 oldest := time.Now() 365 for _, run := range t { 366 if run.TimeStart.Before(oldest) { 367 oldest = run.TimeStart 368 } 369 } 370 return oldest 371 } 372 373 // ProductTestRuns is a tuple of a product and test runs loaded for it. 374 type ProductTestRuns struct { 375 Product ProductSpec 376 TestRuns TestRuns 377 } 378 379 // TestRunsByProduct is an array of tuples of {product, matching runs}, returned 380 // when a TestRun query is executed. 381 type TestRunsByProduct []ProductTestRuns 382 383 // AllRuns returns an array of all the loaded runs. 384 func (t TestRunsByProduct) AllRuns() TestRuns { 385 var runs TestRuns 386 for _, p := range t { 387 runs = append(runs, p.TestRuns...) 388 } 389 return runs 390 } 391 392 // First returns the first TestRun 393 func (t TestRunsByProduct) First() *TestRun { 394 all := t.AllRuns() 395 if len(all) > 0 { 396 return &all[0] 397 } 398 return nil 399 } 400 401 // ProductTestRunKeys is a tuple of a product and test run keys loaded for it. 402 type ProductTestRunKeys struct { 403 Product ProductSpec 404 Keys []Key 405 } 406 407 // KeysByProduct is an array of tuples of {product, matching keys}, returned 408 // when a TestRun key query is executed. 409 type KeysByProduct []ProductTestRunKeys 410 411 // AllKeys returns an array of all the loaded keys. 412 func (t KeysByProduct) AllKeys() []Key { 413 var keys []Key 414 for _, v := range t { 415 keys = append(keys, v.Keys...) 416 } 417 return keys 418 } 419 420 // TestRunIDs is a helper for an array of TestRun IDs. 421 type TestRunIDs []int64 422 423 // GetTestRunIDs extracts the TestRunIDs from loaded datastore keys. 424 func GetTestRunIDs(keys []Key) TestRunIDs { 425 result := make(TestRunIDs, len(keys)) 426 for i := range keys { 427 result[i] = keys[i].IntID() 428 } 429 return result 430 } 431 432 // GetKeys returns a slice of keys for the TestRunIDs in the given datastore. 433 func (ids TestRunIDs) GetKeys(store Datastore) []Key { 434 keys := make([]Key, len(ids)) 435 for i := range ids { 436 keys[i] = store.NewIDKey("TestRun", ids[i]) 437 } 438 return keys 439 } 440 441 // LoadTestRuns is a helper for fetching the TestRuns from the datastore, 442 // for the gives TestRunIDs. 443 func (ids TestRunIDs) LoadTestRuns(store Datastore) (testRuns TestRuns, err error) { 444 if len(ids) > 0 { 445 keys := ids.GetKeys(store) 446 testRuns = make(TestRuns, len(keys)) 447 if err = store.GetMulti(keys, testRuns); err != nil { 448 return testRuns, err 449 } 450 testRuns.SetTestRunIDs(ids) 451 } 452 return testRuns, err 453 } 454 455 // Browser holds objects that appear in browsers.json 456 type Browser struct { 457 InitiallyLoaded bool `json:"initially_loaded"` 458 CurrentlyRun bool `json:"currently_run"` 459 BrowserName string `json:"browser_name"` 460 BrowserVersion string `json:"browser_version"` 461 OSName string `json:"os_name"` 462 OSVersion string `json:"os_version"` 463 Sauce bool `json:"sauce"` 464 } 465 466 // Token is used for test result uploads. 467 type Token struct { 468 Secret string `json:"secret"` 469 } 470 471 // Uploader is a username/password combo accepted by 472 // the results receiver. 473 type Uploader struct { 474 Username string 475 Password string 476 } 477 478 // Flag represents an enviroment feature flag's default state. 479 type Flag struct { 480 Name string `datastore:"-"` // Name is the key in datastore. 481 Enabled bool 482 } 483 484 // LegacySearchRunResult is the results data from legacy test summarys. These 485 // summaries contain a "pass count" and a "total count", where the test itself 486 // counts as 1, and each subtest counts as 1. The "pass count" contains any 487 // status values that are "PASS" or "OK". 488 type LegacySearchRunResult struct { 489 // Passes is the number of test results in a PASS/OK state. 490 Passes int `json:"passes"` 491 // Total is the total number of test results for this run/file pair. 492 Total int `json:"total"` 493 // Status represents either the test status or harness status. 494 // This will be an empty string for old summaries. 495 Status string `json:"status"` 496 // NewAggProcess represents whether the summary was created with the old 497 // or new aggregation process. 498 NewAggProcess bool `json:"newAggProcess"` 499 } 500 501 // SearchResult contains data regarding a particular test file over a collection 502 // of runs. The runs are identified externally in a parallel slice (see 503 // SearchResponse). 504 type SearchResult struct { 505 // Test is the name of a test; this often corresponds to a test file path in 506 // the WPT source reposiory. 507 Test string `json:"test"` 508 // LegacyStatus is the results data from legacy test summaries. These 509 // summaries contain a "pass count" and a "total count", where the test itself 510 // counts as 1, and each subtest counts as 1. The "pass count" contains any 511 // status values that are "PASS" or "OK". 512 LegacyStatus []LegacySearchRunResult `json:"legacy_status,omitempty"` 513 514 // Interoperability scores. For N browsers, we have an array of 515 // N+1 items, where the index X is the number of items passing in exactly 516 // X of the N browsers. e.g. for 4 browsers, [0/4, 1/4, 2/4, 3/4, 4/4]. 517 Interop []int `json:"interop,omitempty"` 518 519 // Subtests (names) which are included in the LegacyStatus summary. 520 Subtests []string `json:"subtests,omitempty"` 521 522 // Diff count of subtests which are included in the LegacyStatus summary. 523 Diff TestDiff `json:"diff,omitempty"` 524 } 525 526 // SearchResponse contains a response to search API calls, including specific 527 // runs whose results were searched and the search results themselves. 528 type SearchResponse struct { 529 // Runs is the specific runs for which results were retrieved. Each run, in 530 // order, corresponds to a Status entry in each SearchResult in Results. 531 Runs []TestRun `json:"runs"` 532 // IgnoredRuns is any runs that the client requested to be included in the 533 // query, but were not included. This optional field may be non-nil if, for 534 // example, results are being served from an incompelte cache of runs and some 535 // runs described in the query request are not resident in the cache. 536 IgnoredRuns []TestRun `json:"ignored_runs,omitempty"` 537 // Results is the collection of test results, grouped by test file name. 538 Results []SearchResult `json:"results"` 539 // MetadataResponse is a response to a wpt-metadata query. 540 MetadataResponse MetadataResults `json:"metadata,omitempty"` 541 }