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 }