| // Copyright 2022 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // Package resultsdb provides helpers for interfacing with resultsdb |
| package resultsdb |
| |
| import ( |
| "context" |
| "fmt" |
| "strings" |
| |
| "cloud.google.com/go/bigquery" |
| "dawn.googlesource.com/dawn/tools/src/buildbucket" |
| "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 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 |
| // methods. |
| type BigQueryClient struct { |
| client *bigquery.Client |
| } |
| |
| // QueryResult contains all of the data for a single test result from a ResultDB |
| // BigQuery query. |
| type QueryResult struct { |
| TestId string |
| Status string |
| Tags []TagPair |
| Duration float64 |
| } |
| |
| // TagPair is a key/value pair representing a ResultDB tag. |
| type TagPair struct { |
| Key string |
| Value string |
| } |
| |
| // DefaultQueryProject is the default BigQuery project to use when running |
| // queries. |
| const DefaultQueryProject string = "chrome-unexpected-pass-data" |
| |
| // NewBigQueryClient creates a client for running BigQuery queries. The |
| // intention is for this to be used for querying ResultDB tables, but there is |
| // nothing ResultDB-specific about the resulting client. |
| func NewBigQueryClient(ctx context.Context, project string) (*BigQueryClient, error) { |
| client, err := bigquery.NewClient(ctx, project) |
| if err != nil { |
| return nil, err |
| } |
| // By default, results are retrieved in chunks as they're iterated over, but |
| // that results in slow performance. Enabling the Storage API allows us to get |
| // all results at once, resulting in a ~8-10x speed increase. |
| err = client.EnableStorageReadClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return &BigQueryClient{client}, nil |
| } |
| |
| // QueryTestResults fetches the test results for the given builds using |
| // BigQuery. |
| // |
| // 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 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. |
| baseQuery := ` |
| SELECT |
| 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")` |
| |
| 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(baseQuery, strings.Join(buildIds, ","), testPrefix) |
| |
| q := bq.client.Query(query) |
| iter, err := q.Read(ctx) |
| if err != nil { |
| return err |
| } |
| |
| var row QueryResult |
| for { |
| err := iter.Next(&row) |
| if err == iterator.Done { |
| break |
| } |
| if err != nil { |
| return err |
| } |
| |
| if err := f(&row); err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |