github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/query.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 query 6 7 import ( 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 "sync" 16 17 "github.com/web-platform-tests/wpt.fyi/shared" 18 ) 19 20 // SummaryResult is the format of the data from summary files generated with the 21 // newest aggregation method. 22 type SummaryResult struct { 23 // Status represents the 1-2 character abbreviation for the status of the test. 24 Status string `json:"s"` 25 // Counts represents the subtest counts (passes and total). 26 Counts []int `json:"c"` 27 } 28 29 // summary is the golang type for the JSON format in pass/total summary files. 30 type summary map[string]SummaryResult 31 32 type queryHandler struct { 33 store shared.Datastore 34 dataSource shared.CachedStore 35 } 36 37 // ErrBadSummaryVersion occurs when the summary file URL is not the correct version. 38 var ErrBadSummaryVersion = errors.New("invalid/unsupported summary version") 39 40 func (qh queryHandler) processInput(w http.ResponseWriter, r *http.Request) ( 41 *shared.QueryFilter, 42 shared.TestRuns, 43 []summary, 44 error, 45 ) { 46 filters, err := shared.ParseQueryFilterParams(r.URL.Query()) 47 if err != nil { 48 http.Error(w, err.Error(), http.StatusBadRequest) 49 50 return nil, nil, nil, err 51 } 52 53 testRuns, filters, err := qh.getRunsAndFilters(filters) 54 if err != nil { 55 http.Error(w, err.Error(), http.StatusNotFound) 56 57 return nil, nil, nil, err 58 } 59 60 summaries, err := qh.loadSummaries(testRuns) 61 if err != nil { 62 http.Error(w, err.Error(), http.StatusInternalServerError) 63 64 return nil, nil, nil, err 65 } 66 67 return &filters, testRuns, summaries, nil 68 } 69 70 func (qh queryHandler) validateSummaryVersions(v url.Values, logger shared.Logger) error { 71 filters, err := shared.ParseQueryFilterParams(v) 72 if err != nil { 73 return err 74 } 75 testRuns, _, err := qh.getRunsAndFilters(filters) 76 if err != nil { 77 return err 78 } 79 80 for _, testRun := range testRuns { 81 summaryURL := shared.GetResultsURL(testRun, "") 82 if !qh.summaryIsValid(summaryURL) { 83 logger.Infof("summary URL has invalid suffix: %s", summaryURL) 84 85 return fmt.Errorf("%w for URL %s", ErrBadSummaryVersion, summaryURL) 86 } 87 } 88 89 return nil 90 } 91 92 func (qh queryHandler) summaryIsValid(summaryURL string) bool { 93 // All new summary URLs end with "-summary_v2.json.gz". Any others are invalid. 94 return strings.HasSuffix(summaryURL, "-summary_v2.json.gz") 95 } 96 97 func (qh queryHandler) getRunsAndFilters(in shared.QueryFilter) (shared.TestRuns, shared.QueryFilter, error) { 98 filters := in 99 var testRuns shared.TestRuns 100 var err error 101 102 if filters.RunIDs == nil || len(filters.RunIDs) == 0 { 103 var runFilters shared.TestRunFilter 104 var sha string 105 var err error 106 limit := 1 107 products := runFilters.GetProductsOrDefault() 108 runsByProduct, err := qh.store.TestRunQuery().LoadTestRuns( 109 products, runFilters.Labels, []string{sha}, runFilters.From, runFilters.To, &limit, nil) 110 if err != nil { 111 return testRuns, filters, err 112 } 113 114 testRuns = runsByProduct.AllRuns() 115 filters.RunIDs = make([]int64, 0, len(testRuns)) 116 for _, testRun := range testRuns { 117 filters.RunIDs = append(filters.RunIDs, testRun.ID) 118 } 119 } else { 120 ids := shared.TestRunIDs(filters.RunIDs) 121 testRuns = make(shared.TestRuns, len(ids)) 122 err = qh.store.GetMulti(ids.GetKeys(qh.store), testRuns) 123 if err != nil { 124 return testRuns, filters, err 125 } 126 testRuns.SetTestRunIDs(ids) 127 } 128 129 return testRuns, filters, nil 130 } 131 132 func (qh queryHandler) loadSummaries(testRuns shared.TestRuns) ([]summary, error) { 133 var err error 134 summaries := make([]summary, len(testRuns)) 135 136 var wg sync.WaitGroup 137 for i, testRun := range testRuns { 138 wg.Add(1) 139 140 go func(i int, testRun shared.TestRun) { 141 defer wg.Done() 142 143 var data []byte 144 s := summary{} 145 data, loadErr := qh.loadSummary(testRun) 146 if err == nil && loadErr != nil { 147 err = fmt.Errorf("Failed to load test run %v: %s", testRun.ID, loadErr.Error()) 148 149 return 150 } 151 // Try to unmarshal the json using the new aggregation structure. 152 marshalErr := json.Unmarshal(data, &s) 153 if err == nil && marshalErr != nil { 154 err = marshalErr 155 156 return 157 } 158 summaries[i] = s 159 }(i, testRun) 160 } 161 wg.Wait() 162 163 return summaries, err 164 } 165 166 func (qh queryHandler) loadSummary(testRun shared.TestRun) ([]byte, error) { 167 mkey := getSummaryFileRedisKey(testRun) 168 url := shared.GetResultsURL(testRun, "") 169 var data []byte 170 err := qh.dataSource.Get(mkey, url, &data) 171 172 return data, err 173 } 174 175 func getSummaryFileRedisKey(testRun shared.TestRun) string { 176 return "RESULTS_SUMMARY_v2-" + strconv.FormatInt(testRun.ID, 10) 177 } 178 179 func isRequestCacheable(r *http.Request) bool { 180 if r.Method == http.MethodGet { 181 ids, err := shared.ParseRunIDsParam(r.URL.Query()) 182 183 return err == nil && len(ids) > 0 184 } 185 186 if r.Method == http.MethodPost { 187 ids, err := shared.ExtractRunIDsBodyParam(r, true) 188 189 return err == nil && len(ids) > 0 190 } 191 192 return false 193 }