[tools][cts] Add unsuppressed failing result funcs
Adds copies of ResultDB and result retrieving functions that only look
for cases of unsuppressed failing tests. These are currently unused
outside of tests, but will be used as part of the CTS roller
simplification.
The code is shared with the previous (all results) versions by moving
the existing implementations to private helper functions that take
relevant function references.
Bug: 372730248
Change-Id: I6acfcef66a6823d7f605a29ef49270bb572bf674
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/210375
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Auto-Submit: Brian Sheedy <bsheedy@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Commit-Queue: Brian Sheedy <bsheedy@google.com>
diff --git a/tools/src/cmd/cts/common/results.go b/tools/src/cmd/cts/common/results.go
index bc50695..710312f 100644
--- a/tools/src/cmd/cts/common/results.go
+++ b/tools/src/cmd/cts/common/results.go
@@ -51,6 +51,17 @@
rdbpb "go.chromium.org/luci/resultdb/proto/v1"
)
+// These function signatures are to facilitate code reuse between the code paths
+// for getting all results and for only getting unsuppressed failures.
+// For MostRecentResultsForChange/MostRecentUnsuppressedFailingResultsForChange.
+type mostRecentResultsFunc = func(context.Context, Config, string, *gerrit.Gerrit, *buildbucket.Buildbucket, resultsdb.Querier, int) (result.ResultsByExecutionMode, gerrit.Patchset, error)
+
+// For CacheResults/CacheUnsuppressedFailingResults.
+type cacheResultsFunc = func(context.Context, Config, gerrit.Patchset, string, resultsdb.Querier, BuildsByName) (result.ResultsByExecutionMode, error)
+
+// For GetRawResults/GetRawUnsuppressedFailingResults.
+type getRawResultsFunc = func(context.Context, Config, resultsdb.Querier, BuildsByName) (result.ResultsByExecutionMode, error)
+
// ResultSource describes the source of CTS test results.
// ResultSource is commonly used by command line flags for specifying from
// where the results should be loaded / fetched.
@@ -79,6 +90,20 @@
// GetResults will update the ResultSource with the inferred patchset, if a file
// and specific patchset was not specified.
func (r *ResultSource) GetResults(ctx context.Context, cfg Config, auth auth.Options) (result.ResultsByExecutionMode, error) {
+ return r.getResultsImpl(ctx, cfg, auth, MostRecentResultsForChange, CacheResults)
+}
+
+// GetUnsuppressedFailingResults loads or fetches the unsuppressed failing
+// results, based on the values of r.
+// GetUnsuppressedFailingResults will update the ResultSource with the inferred
+// patchset if a file and specific patchset was not specified.
+func (r *ResultSource) GetUnsuppressedFailingResults(ctx context.Context, cfg Config, auth auth.Options) (result.ResultsByExecutionMode, error) {
+ return r.getResultsImpl(ctx, cfg, auth, MostRecentUnsuppressedFailingResultsForChange, CacheUnsuppressedFailingResults)
+}
+
+// Helper function to share the implementation between GetResults and GetUnsuppressedFailingResults.
+func (r *ResultSource) getResultsImpl(ctx context.Context, cfg Config, auth auth.Options,
+ getRecentResults mostRecentResultsFunc, cacheResults cacheResultsFunc) (result.ResultsByExecutionMode, error) {
// Check that File and Patchset weren't both specified
ps := &r.Patchset
if r.File != "" && ps.Change != 0 {
@@ -115,7 +140,7 @@
}
fmt.Printf("scanning for latest patchset of %v...\n", latest.Number)
var resultsByExecutionMode result.ResultsByExecutionMode
- resultsByExecutionMode, *ps, err = MostRecentResultsForChange(ctx, cfg, r.CacheDir, gerrit, bb, client, latest.Number)
+ resultsByExecutionMode, *ps, err = getRecentResults(ctx, cfg, r.CacheDir, gerrit, bb, client, latest.Number)
if err != nil {
return nil, err
}
@@ -145,7 +170,7 @@
return nil, err
}
- resultsByExecutionMode, err := CacheResults(ctx, cfg, *ps, r.CacheDir, client, builds)
+ resultsByExecutionMode, err := cacheResults(ctx, cfg, *ps, r.CacheDir, client, builds)
if err != nil {
return nil, err
}
@@ -166,10 +191,42 @@
client resultsdb.Querier,
builds BuildsByName) (result.ResultsByExecutionMode, error) {
+ return cacheResultsImpl(ctx, cfg, ps, cacheDir, "", client, builds, GetRawResults)
+}
+
+// CacheUnsuppressedFailingResults looks in the cache at 'cacheDir' for the
+// unsuppressed failing results for the given patchset. If the cache contains
+// the results, then these are loaded, transformed with CleanResults(), and
+// returned.
+// If the cache does not contain the results, then they are fetched using
+// GetRawUnsuppressedFailingResults(), saved to the cache directory, transformed
+// with CleanResults(), and then returned.
+func CacheUnsuppressedFailingResults(
+ ctx context.Context,
+ cfg Config,
+ ps gerrit.Patchset,
+ cacheDir string,
+ client resultsdb.Querier,
+ builds BuildsByName) (result.ResultsByExecutionMode, error) {
+
+ return cacheResultsImpl(ctx, cfg, ps, cacheDir, "-unsuppressed-failures", client, builds, GetRawUnsuppressedFailingResults)
+}
+
+// Helper function to share the implementation between CacheResults and CacheUnsuppressedFailingResults.
+func cacheResultsImpl(
+ ctx context.Context,
+ cfg Config,
+ ps gerrit.Patchset,
+ cacheDir string,
+ fileSuffix string,
+ client resultsdb.Querier,
+ builds BuildsByName,
+ getRawResults getRawResultsFunc) (result.ResultsByExecutionMode, error) {
+
var cachePath string
if cacheDir != "" {
dir := fileutils.ExpandHome(cacheDir)
- path := filepath.Join(dir, strconv.Itoa(ps.Change), fmt.Sprintf("ps-%v.txt", ps.Patchset))
+ path := filepath.Join(dir, strconv.Itoa(ps.Change), fmt.Sprintf("ps-%v%v.txt", ps.Patchset, fileSuffix))
if _, err := os.Stat(path); err == nil {
log.Printf("loading cached results from cl %v ps %v...", ps.Change, ps.Patchset)
return result.Load(path)
@@ -178,7 +235,7 @@
}
log.Printf("fetching results from cl %v ps %v...", ps.Change, ps.Patchset)
- resultsByExecutionMode, err := GetRawResults(ctx, cfg, client, builds)
+ resultsByExecutionMode, err := getRawResults(ctx, cfg, client, builds)
if err != nil {
return nil, err
}
@@ -205,7 +262,30 @@
client resultsdb.Querier,
builds BuildsByName) (result.ResultsByExecutionMode, error) {
- resultsByExecutionMode, err := GetRawResults(ctx, cfg, client, builds)
+ return getResultsImpl(ctx, cfg, client, builds, GetRawResults)
+}
+
+// GetUnsuppressedFailingResults calls GetRawUnsuppressedFailingResults() to
+// fetch the unsuppressed failing results from ResultDB and applies
+// CleanResults(). GetUnsuppressedFailingResults() does not trigger new builds.
+func GetUnsuppressedFailingResults(
+ ctx context.Context,
+ cfg Config,
+ client resultsdb.Querier,
+ builds BuildsByName) (result.ResultsByExecutionMode, error) {
+
+ return getResultsImpl(ctx, cfg, client, builds, GetRawUnsuppressedFailingResults)
+}
+
+// Helper function to share the implementation between GetResults and GetUnsuppressedFailingResults.
+func getResultsImpl(
+ ctx context.Context,
+ cfg Config,
+ client resultsdb.Querier,
+ builds BuildsByName,
+ getRawResults getRawResultsFunc) (result.ResultsByExecutionMode, error) {
+
+ resultsByExecutionMode, err := getRawResults(ctx, cfg, client, builds)
if err != nil {
return nil, err
}
@@ -228,6 +308,30 @@
client resultsdb.Querier,
builds BuildsByName) (result.ResultsByExecutionMode, error) {
+ return getRawResultsImpl(ctx, cfg, client, builds, client.QueryTestResults)
+}
+
+// GetRawUnsuppressedFailingResults fetches the unsuppressed failing results
+// from ResultDB, without applying CleanResults().
+// GetRawUnsuppressedFailingResults does not trigger new builds.
+func GetRawUnsuppressedFailingResults(
+ ctx context.Context,
+ cfg Config,
+ client resultsdb.Querier,
+ builds BuildsByName) (result.ResultsByExecutionMode, error) {
+
+ return getRawResultsImpl(ctx, cfg, client, builds, client.QueryUnsuppressedFailingTestResults)
+}
+
+// Helper function to share the implementation between GetRawResults and
+// GetRawUnsuppressedFailingResults.
+func getRawResultsImpl(
+ ctx context.Context,
+ cfg Config,
+ client resultsdb.Querier,
+ builds BuildsByName,
+ queryFunc resultsdb.QueryFunc) (result.ResultsByExecutionMode, error) {
+
fmt.Printf("fetching results from resultdb...")
lastPrintedDot := time.Now()
@@ -254,7 +358,7 @@
results := result.List{}
for _, prefix := range test.Prefixes {
- err := client.QueryTestResults(ctx, builds.ids(), prefix, func(r *resultsdb.QueryResult) error {
+ err := queryFunc(ctx, builds.ids(), prefix, func(r *resultsdb.QueryResult) error {
if time.Since(lastPrintedDot) > 5*time.Second {
lastPrintedDot = time.Now()
fmt.Printf(".")
@@ -353,6 +457,36 @@
client resultsdb.Querier,
change int) (result.ResultsByExecutionMode, gerrit.Patchset, error) {
+ return mostRecentResultsForChangeImpl(ctx, cfg, cacheDir, g, bb, client, change, CacheResults)
+}
+
+// MostRecentUnsuppressedFailingResultsForChange returns the unsuppressed
+// failing results from the most recent patchset that has build results. If no
+// results can be found for the entire change, then an error is returned.
+func MostRecentUnsuppressedFailingResultsForChange(
+ ctx context.Context,
+ cfg Config,
+ cacheDir string,
+ g *gerrit.Gerrit,
+ bb *buildbucket.Buildbucket,
+ client resultsdb.Querier,
+ change int) (result.ResultsByExecutionMode, gerrit.Patchset, error) {
+
+ return mostRecentResultsForChangeImpl(ctx, cfg, cacheDir, g, bb, client, change, CacheUnsuppressedFailingResults)
+}
+
+// Helper function to share the implementation between MostRecentResultsForChange
+// and MostRecentUnsuppressedFailingResultsForChange.
+func mostRecentResultsForChangeImpl(
+ ctx context.Context,
+ cfg Config,
+ cacheDir string,
+ g *gerrit.Gerrit,
+ bb *buildbucket.Buildbucket,
+ client resultsdb.Querier,
+ change int,
+ cacheResults cacheResultsFunc) (result.ResultsByExecutionMode, gerrit.Patchset, error) {
+
ps, err := LatestPatchset(g, change)
if err != nil {
return nil, gerrit.Patchset{}, nil
@@ -368,7 +502,7 @@
return nil, gerrit.Patchset{}, err
}
- results, err := CacheResults(ctx, cfg, ps, cacheDir, client, builds)
+ results, err := cacheResults(ctx, cfg, ps, cacheDir, client, builds)
if err != nil {
return nil, gerrit.Patchset{}, err
}
diff --git a/tools/src/cmd/cts/common/results_test.go b/tools/src/cmd/cts/common/results_test.go
index 5e041f6..e696cfd 100644
--- a/tools/src/cmd/cts/common/results_test.go
+++ b/tools/src/cmd/cts/common/results_test.go
@@ -41,10 +41,13 @@
// TODO(crbug.com/342554800): Add test coverage for:
// ResultSource.GetResults (requires breaking out into helper functions)
-// CacheResults
+// ResultSource.GetUnsuppressedFailingResults (ditto)
+// CacheResults (requires os abstraction crbug.com/344014313)
+// CacheUnsuppressedFailingResults (ditto)
// LatestCTSRoll
// LatestPatchset
-// MostRecentResultsForChange
+// MostRecentResultsForChange (requires os abstraction crbug.com/344014313)
+// MostRecentUnsuppressedFailingResultsForChange (ditto)
/*******************************************************************************
* Fake implementations
@@ -52,7 +55,8 @@
// A fake version of dawn/tools/src/resultsdb's BigQueryClient.
type mockedBigQueryClient struct {
- returnValues []resultsdb.QueryResult
+ returnValues []resultsdb.QueryResult
+ unsuppressedFailureReturnValues []resultsdb.QueryResult
}
func (bq mockedBigQueryClient) QueryTestResults(
@@ -65,6 +69,17 @@
return nil
}
+func (bq mockedBigQueryClient) QueryUnsuppressedFailingTestResults(
+ ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f func(*resultsdb.QueryResult) error) error {
+
+ for _, result := range bq.unsuppressedFailureReturnValues {
+ if err := f(&result); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
/*******************************************************************************
* GetResults tests
******************************************************************************/
@@ -170,6 +185,110 @@
}
/*******************************************************************************
+ * GetUnsuppressedFailingResults tests
+ ******************************************************************************/
+
+func generateGoodGetUnsuppressedFailingResultsInputs() (context.Context, Config, *mockedBigQueryClient, BuildsByName) {
+ ctx := context.Background()
+
+ cfg := Config{
+ Tests: []TestConfig{
+ TestConfig{
+ ExecutionMode: result.ExecutionMode("execution_mode"),
+ Prefixes: []string{"prefix"},
+ },
+ },
+ }
+
+ client := &mockedBigQueryClient{
+ unsuppressedFailureReturnValues: []resultsdb.QueryResult{
+ resultsdb.QueryResult{
+ TestId: "prefix_test",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{},
+ Duration: 1.0,
+ },
+ },
+ }
+
+ builds := make(BuildsByName)
+
+ return ctx, cfg, client, builds
+}
+
+// Tests that valid results are properly cleaned, sorted, and returned.
+func TestGetUnsuppressedFailingResultsHappyPath(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+
+ cfg.Tag.Remove = []string{
+ "remove_me",
+ }
+
+ client.unsuppressedFailureReturnValues = []resultsdb.QueryResult{
+ resultsdb.QueryResult{
+ TestId: "prefix_test_2",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "typ_tag",
+ Value: "remove_me",
+ },
+ resultsdb.TagPair{
+ Key: "typ_tag",
+ Value: "win",
+ },
+ },
+ Duration: 2.0,
+ },
+ resultsdb.QueryResult{
+ TestId: "prefix_test_1",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "typ_tag",
+ Value: "linux",
+ },
+ },
+ Duration: 1.0,
+ },
+ }
+
+ expectedResultsList := result.List{
+ result.Result{
+ Query: query.Parse("_test_1"),
+ Status: result.Failure,
+ Tags: result.NewTags("linux"),
+ Duration: time.Second,
+ MayExonerate: false,
+ },
+ result.Result{
+ Query: query.Parse("_test_2"),
+ Status: result.Failure,
+ Tags: result.NewTags("win"),
+ Duration: 2 * time.Second,
+ MayExonerate: false,
+ },
+ }
+
+ expectedResults := make(result.ResultsByExecutionMode)
+ expectedResults["execution_mode"] = expectedResultsList
+
+ results, err := GetUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, err)
+ assert.Equal(t, results, expectedResults)
+}
+
+// Tests that errors from GetRawResults are properly surfaced.
+func TestGetUnsuppressedFailingResultsGetRawResultsErrorSurfaced(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+ client.unsuppressedFailureReturnValues[0].TestId = "bad_test"
+
+ results, err := GetUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, results)
+ assert.ErrorContains(t, err, "Test ID bad_test did not start with prefix even though query should have filtered.")
+}
+
+/*******************************************************************************
* GetRawResults tests
******************************************************************************/
@@ -302,6 +421,138 @@
}
/*******************************************************************************
+ * GetRawUnsuppressedFailingResults tests
+ ******************************************************************************/
+
+// Tests that valid results are properly parsed and returned.
+func TestGetRawUnsuppressedFailingResultsHappyPath(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+ client.unsuppressedFailureReturnValues = []resultsdb.QueryResult{
+ resultsdb.QueryResult{
+ TestId: "prefix_test_1",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{},
+ Duration: 1.0,
+ },
+ resultsdb.QueryResult{
+ TestId: "prefix_test_2",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "javascript_duration",
+ Value: "0.5s",
+ },
+ },
+ Duration: 2.0,
+ },
+ resultsdb.QueryResult{
+ TestId: "prefix_test_3",
+ Status: "FAIL",
+ Tags: []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "may_exonerate",
+ Value: "true",
+ },
+ },
+ Duration: 3.0,
+ },
+ resultsdb.QueryResult{
+ TestId: "prefix_test_4",
+ Status: "SomeStatus",
+ Tags: []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "typ_tag",
+ Value: "linux",
+ },
+ resultsdb.TagPair{
+ Key: "typ_tag",
+ Value: "intel",
+ },
+ },
+ Duration: 4.0,
+ },
+ }
+
+ expectedResultsList := result.List{
+ result.Result{
+ Query: query.Parse("_test_1"),
+ Status: result.Failure,
+ Tags: result.NewTags(),
+ Duration: time.Second,
+ MayExonerate: false,
+ },
+ result.Result{
+ Query: query.Parse("_test_2"),
+ Status: result.Failure,
+ Tags: result.NewTags(),
+ Duration: 500 * time.Millisecond,
+ MayExonerate: false,
+ },
+ result.Result{
+ Query: query.Parse("_test_3"),
+ Status: result.Failure,
+ Tags: result.NewTags(),
+ Duration: 3 * time.Second,
+ MayExonerate: true,
+ },
+ result.Result{
+ Query: query.Parse("_test_4"),
+ Status: result.Unknown,
+ Tags: result.NewTags("linux", "intel"),
+ Duration: 4 * time.Second,
+ MayExonerate: false,
+ },
+ }
+
+ expectedResults := make(result.ResultsByExecutionMode)
+ expectedResults["execution_mode"] = expectedResultsList
+
+ results, err := GetRawUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, err)
+ assert.Equal(t, results, expectedResults)
+}
+
+// Tests that a mismatched prefix results in an error.
+func TestGetRawUnsuppressedFailingResultsPrefixMismatch(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+ client.unsuppressedFailureReturnValues[0].TestId = "bad_test"
+
+ results, err := GetRawUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, results)
+ assert.ErrorContains(t, err, "Test ID bad_test did not start with prefix even though query should have filtered.")
+}
+
+// Tests that a JavaScript duration that cannot be parsed results in an error.
+func TestGetRawUnsuppressedFailingResultsBadJavaScriptDuration(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+ client.unsuppressedFailureReturnValues[0].Tags = []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "javascript_duration",
+ Value: "1000foo",
+ },
+ }
+
+ results, err := GetRawUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, results)
+ assert.ErrorContains(t, err, `time: unknown unit "foo" in duration "1000foo"`)
+}
+
+// Tests that a non-boolean may_exonerate value results in an error.
+func TestGetRawUnsuppressedFailingResultsBadMayExonerate(t *testing.T) {
+ ctx, cfg, client, builds := generateGoodGetUnsuppressedFailingResultsInputs()
+ client.unsuppressedFailureReturnValues[0].Tags = []resultsdb.TagPair{
+ resultsdb.TagPair{
+ Key: "may_exonerate",
+ Value: "yesnt",
+ },
+ }
+
+ results, err := GetRawUnsuppressedFailingResults(ctx, cfg, client, builds)
+ assert.Nil(t, results)
+ assert.ErrorContains(t, err, `strconv.ParseBool: parsing "yesnt": invalid syntax`)
+}
+
+/*******************************************************************************
* CleanResults tests
******************************************************************************/
@@ -450,7 +701,7 @@
}
// Tests that statuses without explicit priority default to Failure.
-func TestCleanResultsReplacEresultDefault(t *testing.T) {
+func TestCleanResultsReplaceResultDefault(t *testing.T) {
cfg := Config{}
statuses := []result.Status{result.Pass, result.RetryOnFailure, result.Skip, result.Unknown}
for i, leftStatus := range statuses {
diff --git a/tools/src/resultsdb/resultsdb.go b/tools/src/resultsdb/resultsdb.go
index 9c6d972..91c48a4 100644
--- a/tools/src/resultsdb/resultsdb.go
+++ b/tools/src/resultsdb/resultsdb.go
@@ -38,8 +38,12 @@
"google.golang.org/api/iterator"
)
+type RowHandler = func(*QueryResult) error
+type QueryFunc = func(context.Context, []buildbucket.BuildID, string, RowHandler) error
+
type Querier interface {
- QueryTestResults(ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f func(*QueryResult) error) error
+ QueryTestResults(ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f RowHandler) error
+ QueryUnsuppressedFailingTestResults(ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f RowHandler) error
}
// BigQueryClient is a wrapper around bigquery.Client so that we can define new
@@ -91,26 +95,79 @@
// f is called once per result and is expected to handle any processing or
// storage of results.
func (bq BigQueryClient) QueryTestResults(
- ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f func(*QueryResult) error) error {
+ ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f RowHandler) error {
// test_id gets renamed since the column names need to match the struct names
// unless we want to get results in a generic bigquery.Value slice and
// manually copy data over.
- base_query := `
+ baseQuery := `
SELECT
- test_id AS testid,
- status,
- tags,
- duration
+ test_id AS testid,
+ status,
+ tags,
+ duration
FROM ` + "`chrome-luci-data.chromium.gpu_try_test_results`" + ` tr
WHERE
- exported.id IN UNNEST([%s])
- AND STARTS_WITH(tr.test_id, "%v")`
+ exported.id IN UNNEST([%s])
+ AND STARTS_WITH(tr.test_id, "%v")`
+ return bq.runQueryForBuilds(ctx, baseQuery, builds, testPrefix, f)
+}
+
+// QueryUnsuppressedFailingTestResults fetches the test results for the given
+// builds using BigQuery that both:
+// 1. Failed in some way
+// 2. Did not have an existing failure suppression in place
+//
+// f is called once per result and is expected to handle any processing or
+// sorage of results.
+func (bq BigQueryClient) QueryUnsuppressedFailingTestResults(
+ ctx context.Context, builds []buildbucket.BuildID, testPrefix string, f RowHandler) error {
+ // We use a subquery since we need to calculate the typ expectations for
+ // filtering, but don't actually need them in the end results.
+ // test_id gets renamed since the column names need to match the struct names
+ // unless we want to get results as a generic bigquery.Value slice and
+ // manually copy data over.
+ baseQuery := `
+ WITH
+ failing_results AS (
+ SELECT
+ test_id AS testid,
+ status,
+ tags,
+ duration,
+ ARRAY(
+ SELECT value
+ FROM tr.tags
+ WHERE key = "raw_typ_expectation") as typ_expectations
+ FROM ` + "`chrome-luci-data.chromium.gpu_try_test_results`" + ` tr
+ WHERE
+ exported.id IN UNNEST([%s])
+ AND STARTS_WITH(tr.test_id, "%v")
+ AND status != "SKIP"
+ AND status != "PASS"
+ )
+ SELECT
+ *
+ EXCEPT
+ (typ_expectations)
+ FROM
+ failing_results
+ WHERE
+ ARRAY_LENGTH(typ_expectations) = 1
+ AND typ_expectations[0] = "Pass"`
+
+ return bq.runQueryForBuilds(ctx, baseQuery, builds, testPrefix, f)
+}
+
+// runQueryForBuilds is a helper function for running queries limited to a set
+// of builds and prefix. See callers of this function for additional information.
+func (bq BigQueryClient) runQueryForBuilds(
+ ctx context.Context, baseQuery string, builds []buildbucket.BuildID, testPrefix string, f RowHandler) error {
var buildIds []string
for _, id := range builds {
buildIds = append(buildIds, fmt.Sprintf(`"build-%v"`, id))
}
- query := fmt.Sprintf(base_query, strings.Join(buildIds, ","), testPrefix)
+ query := fmt.Sprintf(baseQuery, strings.Join(buildIds, ","), testPrefix)
q := bq.client.Query(query)
iter, err := q.Read(ctx)