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  }