github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/shared/test_run_filter.go (about)

     1  // Copyright 2018 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/base64"
     9  	"encoding/json"
    10  	"fmt"
    11  	"net/url"
    12  	"strconv"
    13  	"time"
    14  
    15  	mapset "github.com/deckarep/golang-set"
    16  )
    17  
    18  // SHAs is a helper type for a slice of commit/revision SHAs.
    19  type SHAs []string
    20  
    21  // EmptyOrLatest returns whether the shas slice is empty, or only contains
    22  // one item, which is the latest keyword.
    23  func (s SHAs) EmptyOrLatest() bool {
    24  	return len(s) < 1 || len(s) == 1 && IsLatest(s[0])
    25  }
    26  
    27  // FirstOrLatest returns the first sha in the slice, or the latest keyword.
    28  func (s SHAs) FirstOrLatest() string {
    29  	if s.EmptyOrLatest() {
    30  		return LatestSHA
    31  	}
    32  	return s[0]
    33  }
    34  
    35  // ShortSHAs returns an array of the given SHAs' first 7-chars.
    36  func (s SHAs) ShortSHAs() []string {
    37  	short := make([]string, len(s))
    38  	for i, long := range s {
    39  		short[i] = long[:7]
    40  	}
    41  	return short
    42  }
    43  
    44  // TestRunFilter represents the ways TestRun entities can be filtered in
    45  // the webapp and api.
    46  type TestRunFilter struct {
    47  	SHAs     SHAs         `json:"shas,omitempty"`
    48  	Labels   mapset.Set   `json:"labels,omitempty"`
    49  	Aligned  *bool        `json:"aligned,omitempty"`
    50  	From     *time.Time   `json:"from,omitempty"`
    51  	To       *time.Time   `json:"to,omitempty"`
    52  	MaxCount *int         `json:"maxcount,omitempty"`
    53  	Offset   *int         `json:"offset,omitempty"` // Used for paginating with MaxCount.
    54  	Products ProductSpecs `json:"products,omitempty"`
    55  	View     *string      `json:"view,omitempty"`
    56  }
    57  
    58  type testRunFilterNoCustomMarshalling TestRunFilter
    59  type marshallableTestRunFilter struct {
    60  	testRunFilterNoCustomMarshalling
    61  	Labels []string `json:"labels,omitempty"`
    62  }
    63  
    64  // MarshalJSON treats the set as an array so it can be marshalled.
    65  func (filter TestRunFilter) MarshalJSON() ([]byte, error) {
    66  	m := marshallableTestRunFilter{
    67  		testRunFilterNoCustomMarshalling: testRunFilterNoCustomMarshalling(filter),
    68  	}
    69  	m.Labels = ToStringSlice(filter.Labels)
    70  	return json.Marshal(m)
    71  }
    72  
    73  // UnmarshalJSON parses an array so that TestRunFilter can be unmarshalled.
    74  func (filter *TestRunFilter) UnmarshalJSON(data []byte) error {
    75  	var m marshallableTestRunFilter
    76  	if err := json.Unmarshal(data, &m); err != nil {
    77  		return err
    78  	}
    79  	*filter = TestRunFilter(m.testRunFilterNoCustomMarshalling)
    80  	filter.Labels = NewSetFromStringSlice(m.Labels)
    81  	return nil
    82  }
    83  
    84  // IsDefaultQuery returns whether the params are just an empty query (or,
    85  // the equivalent defaults of an empty query).
    86  func (filter TestRunFilter) IsDefaultQuery() bool {
    87  	return filter.SHAs.EmptyOrLatest() &&
    88  		(filter.Labels == nil || filter.Labels.Cardinality() < 1) &&
    89  		(filter.Aligned == nil) &&
    90  		(filter.From == nil) &&
    91  		(filter.MaxCount == nil || *filter.MaxCount == 1) &&
    92  		(len(filter.Products) < 1) &&
    93  		(filter.View == nil)
    94  }
    95  
    96  // OrDefault returns the current filter, or, if it is a default query, returns
    97  // the query used by default in wpt.fyi.
    98  func (filter TestRunFilter) OrDefault() TestRunFilter {
    99  	// TODO(smcgruer): OrAlignedStableRuns is not the default query in
   100  	// wpt.fyi, and has not been for many years (ever since the
   101  	// experimentalByDefault flag was turned on). Usage of this method
   102  	// should be audited.
   103  	return filter.OrAlignedStableRuns()
   104  }
   105  
   106  // OrAlignedStableRuns returns the current filter, or, if it is a default query, returns
   107  // a query for stable runs, with an aligned SHA.
   108  func (filter TestRunFilter) OrAlignedStableRuns() TestRunFilter {
   109  	if !filter.IsDefaultQuery() {
   110  		return filter
   111  	}
   112  	aligned := true
   113  	filter.Aligned = &aligned
   114  	filter.Labels = mapset.NewSetWith(StableLabel)
   115  	return filter
   116  }
   117  
   118  // OrExperimentalRuns returns the current filter, or, if it is a default query, returns
   119  // a query for the latest experimental runs.
   120  func (filter TestRunFilter) OrExperimentalRuns() TestRunFilter {
   121  	if !filter.IsDefaultQuery() {
   122  		return filter
   123  	}
   124  	filter.Labels = mapset.NewSetWith(ExperimentalLabel)
   125  	return filter
   126  }
   127  
   128  // MasterOnly returns the current filter, ensuring it has with the master-only
   129  // restriction (a label of "master").
   130  func (filter TestRunFilter) MasterOnly() TestRunFilter {
   131  	if filter.Labels == nil {
   132  		filter.Labels = mapset.NewSet()
   133  	}
   134  	filter.Labels.Add(MasterLabel)
   135  	return filter
   136  }
   137  
   138  // IsDefaultProducts returns whether the params products are empty, or the
   139  // equivalent of the default product set.
   140  func (filter TestRunFilter) IsDefaultProducts() bool {
   141  	if len(filter.Products) == 0 {
   142  		return true
   143  	}
   144  	def := GetDefaultProducts()
   145  	if len(filter.Products) != len(def) {
   146  		return false
   147  	}
   148  	for i := range def {
   149  		if def[i] != filter.Products[i] {
   150  			return false
   151  		}
   152  	}
   153  	return true
   154  }
   155  
   156  // GetProductsOrDefault parses the 'products' (and legacy 'browsers') params, returning
   157  // the ordered list of products to include, or a default list.
   158  func (filter TestRunFilter) GetProductsOrDefault() (products ProductSpecs) {
   159  	return filter.Products.OrDefault()
   160  }
   161  
   162  // ToQuery converts the filter set to a url.Values (set of query params).
   163  func (filter TestRunFilter) ToQuery() (q url.Values) {
   164  	u := url.URL{}
   165  	q = u.Query()
   166  	if !filter.SHAs.EmptyOrLatest() {
   167  		for _, sha := range filter.SHAs {
   168  			q.Add("sha", sha)
   169  		}
   170  	}
   171  	if filter.Labels != nil && filter.Labels.Cardinality() > 0 {
   172  		for label := range filter.Labels.Iter() {
   173  			q.Add("label", label.(string))
   174  		}
   175  	}
   176  	if len(filter.Products) > 0 {
   177  		for _, p := range filter.Products {
   178  			q.Add("product", p.String())
   179  		}
   180  	}
   181  	if filter.Aligned != nil {
   182  		q.Set("aligned", strconv.FormatBool(*filter.Aligned))
   183  	}
   184  	if filter.MaxCount != nil {
   185  		q.Set("max-count", fmt.Sprintf("%v", *filter.MaxCount))
   186  	}
   187  	if filter.Offset != nil {
   188  		q.Set("offset", fmt.Sprintf("%v", *filter.Offset))
   189  	}
   190  	if filter.From != nil {
   191  		q.Set("from", filter.From.Format(time.RFC3339))
   192  	}
   193  	if filter.To != nil {
   194  		q.Set("to", filter.To.Format(time.RFC3339))
   195  	}
   196  	if filter.View != nil {
   197  		q.Set("view", *filter.View)
   198  	}
   199  	return q
   200  }
   201  
   202  // NextPage returns a filter for the next page of results that
   203  // would match the current filter, based on the given results that were
   204  // loaded.
   205  func (filter TestRunFilter) NextPage(loadedRuns TestRunsByProduct) *TestRunFilter {
   206  	if filter.MaxCount != nil {
   207  		// We only have another page if N results were returned for a max of N.
   208  		anyMaxedOut := false
   209  		for _, v := range loadedRuns {
   210  			if len(v.TestRuns) >= *filter.MaxCount {
   211  				anyMaxedOut = true
   212  			}
   213  		}
   214  		if anyMaxedOut {
   215  			offset := *filter.MaxCount
   216  			if filter.Offset != nil {
   217  				offset += *filter.Offset
   218  			}
   219  			filter.Offset = &offset
   220  			return &filter
   221  		}
   222  	}
   223  	if filter.From != nil {
   224  		from := *filter.From
   225  		var to time.Time
   226  		if filter.To != nil {
   227  			to = *filter.To
   228  		} else {
   229  			to = time.Now()
   230  		}
   231  		span := to.Sub(from)
   232  		newFrom := from.Add(-span)
   233  		newTo := from.Add(-time.Millisecond)
   234  		filter.To = &newTo
   235  		filter.From = &newFrom
   236  		return &filter
   237  	}
   238  	return nil
   239  }
   240  
   241  // Token returns a base64 encoded copy of the filter.
   242  func (filter TestRunFilter) Token() (string, error) {
   243  	bytes, err := json.Marshal(filter)
   244  	if err != nil {
   245  		return "", err
   246  	}
   247  	return base64.URLEncoding.EncodeToString(bytes), nil
   248  }