github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/test_runs.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 api 6 7 import ( 8 "encoding/json" 9 "errors" 10 "net/http" 11 "strings" 12 13 "github.com/web-platform-tests/wpt.fyi/shared" 14 ) 15 16 // nolint:gosec // TODO: Fix gosec lint error (G101) 17 const nextPageTokenHeaderName = "wpt-next-page" 18 const paginationTokenFeatureFlagName = "paginationTokens" 19 20 // apiTestRunsHandler is responsible for emitting test-run JSON for all the runs at a given SHA. 21 // 22 // URL Params: 23 // sha: SHA[0:10] of the repo when the tests were executed (or 'latest') 24 func apiTestRunsHandler(w http.ResponseWriter, r *http.Request) { 25 ctx := r.Context() 26 log := shared.GetLogger(ctx) 27 store := shared.NewAppEngineDatastore(ctx, true) 28 aeAPI := shared.NewAppEngineAPI(ctx) 29 q := r.URL.Query() 30 ids, err := shared.ParseRunIDsParam(q) 31 if err != nil { 32 http.Error(w, err.Error(), http.StatusBadRequest) 33 34 return 35 } 36 37 pr, err := shared.ParsePRParam(q) 38 if err != nil { 39 http.Error(w, err.Error(), http.StatusBadRequest) 40 } 41 42 var testRuns shared.TestRuns 43 var nextPageToken string 44 // nolint:nestif // TODO: Fix nestif lint error 45 if len(ids) > 0 { 46 testRuns, err = ids.LoadTestRuns(store) 47 if errors.Is(err, shared.ErrNoSuchEntity) { 48 w.WriteHeader(http.StatusNotFound) 49 err = nil 50 } 51 } else { 52 var filters shared.TestRunFilter 53 filters, err = shared.ParseTestRunFilterParams(r.URL.Query()) 54 if err != nil { 55 http.Error(w, err.Error(), http.StatusBadRequest) 56 57 return 58 } 59 if pr != nil && aeAPI.IsFeatureEnabled("runsByPRNumber") { 60 filters.SHAs = getPRCommits(aeAPI, *pr) 61 if len(filters.SHAs) < 1 { 62 log.Warningf("PR %v returned no commits from GitHub", *pr) 63 } else { 64 log.Infof("PR %v returned %v commits: %s", *pr, len(filters.SHAs), strings.Join(filters.SHAs.ShortSHAs(), ",")) 65 } 66 } 67 var runsByProduct shared.TestRunsByProduct 68 runsByProduct, err = LoadTestRunsForFilters(store, filters) 69 70 if err == nil { 71 testRuns = runsByProduct.AllRuns() 72 if aeAPI.IsFeatureEnabled(paginationTokenFeatureFlagName) { 73 nextPage := filters.NextPage(runsByProduct) 74 if nextPage != nil { 75 nextPageToken, _ = nextPage.Token() 76 } 77 } 78 } 79 } 80 81 if err != nil { 82 http.Error(w, err.Error(), http.StatusInternalServerError) 83 84 return 85 } else if len(testRuns) == 0 { 86 w.WriteHeader(http.StatusNotFound) 87 _, err = w.Write([]byte("[]")) 88 if err != nil { 89 log.Warningf("Failed to write data in api/runs handler: %s", err.Error()) 90 } 91 92 return 93 } 94 95 if nextPageToken != "" { 96 w.Header().Add(nextPageTokenHeaderName, nextPageToken) 97 } 98 99 testRunsBytes, err := json.Marshal(testRuns) 100 if err != nil { 101 http.Error(w, err.Error(), http.StatusInternalServerError) 102 103 return 104 } 105 106 _, err = w.Write(testRunsBytes) 107 if err != nil { 108 log.Warningf("Failed to write data in api/runs handler: %s", err.Error()) 109 } 110 } 111 112 // LoadTestRunKeysForFilters deciphers the filters and executes a corresponding 113 // query to load the TestRun keys. 114 func LoadTestRunKeysForFilters(store shared.Datastore, filters shared.TestRunFilter) ( 115 result shared.KeysByProduct, 116 err error, 117 ) { 118 q := store.TestRunQuery() 119 limit := filters.MaxCount 120 offset := filters.Offset 121 from := filters.From 122 // Default to a single, latest run when not using any "more than one results" filters. 123 if limit == nil && from == nil && len(filters.SHAs) < 2 { 124 one := 1 125 limit = &one 126 } 127 products := filters.GetProductsOrDefault() 128 129 // When ?aligned=true, make sure to show results for the same aligned run (executed for all browsers). 130 if filters.SHAs.EmptyOrLatest() && filters.Aligned != nil && *filters.Aligned { 131 shas, shaKeys, err := q.GetAlignedRunSHAs(products, filters.Labels, from, filters.To, limit, filters.Offset) 132 if err != nil { 133 return result, err 134 } 135 if len(shas) < 1 { 136 // Bail out early - can't find any complete runs. 137 return result, nil 138 } 139 keys := make(shared.KeysByProduct, len(products)) 140 for _, sha := range shas { 141 for i := range shaKeys[sha] { 142 keys[i].Keys = append(keys[i].Keys, shaKeys[sha][i].Keys...) 143 } 144 } 145 146 return keys, err 147 } 148 149 return q.LoadTestRunKeys(products, filters.Labels, filters.SHAs, from, filters.To, limit, offset) 150 } 151 152 // LoadTestRunsForFilters deciphers the filters and executes a corresponding query to load 153 // the TestRuns. 154 func LoadTestRunsForFilters( 155 store shared.Datastore, 156 filters shared.TestRunFilter, 157 ) (result shared.TestRunsByProduct, err error) { 158 var keys shared.KeysByProduct 159 if keys, err = LoadTestRunKeysForFilters(store, filters); err != nil { 160 return nil, err 161 } 162 163 return store.TestRunQuery().LoadTestRunsByKeys(keys) 164 } 165 166 func getPRCommits(aeAPI shared.AppEngineAPI, pr int) shared.SHAs { 167 log := shared.GetLogger(aeAPI.Context()) 168 169 githubClient, err := aeAPI.GetGitHubClient() 170 if err != nil { 171 log.Errorf("Failed to get github client: %s", err.Error()) 172 173 return nil 174 } 175 commits, _, err := githubClient.PullRequests.ListCommits( 176 aeAPI.Context(), 177 shared.WPTRepoOwner, 178 shared.WPTRepoName, 179 pr, 180 nil, 181 ) 182 if err != nil || commits == nil { 183 log.Errorf("Failed to fetch PR #%v: %s", pr, err.Error()) 184 185 return nil 186 } 187 shas := make([]string, len(commits)) 188 for i := range commits { 189 shas[i] = commits[i].GetSHA() 190 } 191 192 return shas 193 }