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  }