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  }