tools: Add src/cts/expectations Update()
Implement the logic to update an existing expectations file with new results. Bug: dawn:1342 Change-Id: Idcbad57946712539cc5f0d238f89a2adf42f5aa0 Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/87702 Reviewed-by: Dan Sinclair <dsinclair@chromium.org> Commit-Queue: Ben Clayton <bclayton@google.com>
This commit is contained in:
parent
9432887ce8
commit
27f480b7e6
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2022 The Dawn Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package expectations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Severity is an enumerator of diagnostic severity
|
||||||
|
type Severity string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Error Severity = "error"
|
||||||
|
Warning Severity = "warning"
|
||||||
|
Note Severity = "note"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Diagnostic holds a line, column, message and severity.
|
||||||
|
// Diagnostic also implements the 'error' interface.
|
||||||
|
type Diagnostic struct {
|
||||||
|
Severity Severity
|
||||||
|
Line int // 1-based
|
||||||
|
Column int // 1-based
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Diagnostic) String() string {
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
if e.Line > 0 {
|
||||||
|
fmt.Fprintf(sb, "%v", e.Line)
|
||||||
|
if e.Column > 0 {
|
||||||
|
fmt.Fprintf(sb, ":%v", e.Column)
|
||||||
|
}
|
||||||
|
sb.WriteString(" ")
|
||||||
|
}
|
||||||
|
sb.WriteString(string(e.Severity))
|
||||||
|
sb.WriteString(": ")
|
||||||
|
sb.WriteString(e.Message)
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the 'error' interface.
|
||||||
|
func (e Diagnostic) Error() string { return e.String() }
|
|
@ -15,7 +15,6 @@
|
||||||
package expectations
|
package expectations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
||||||
|
@ -26,19 +25,6 @@ const (
|
||||||
tagHeaderEnd = `END TAG HEADER`
|
tagHeaderEnd = `END TAG HEADER`
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyntaxError is the error type returned by Parse() when a syntax error is
|
|
||||||
// encountered.
|
|
||||||
type SyntaxError struct {
|
|
||||||
Line int // 1-based
|
|
||||||
Column int // 1-based
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error implements the 'error' interface.
|
|
||||||
func (e SyntaxError) Error() string {
|
|
||||||
return fmt.Sprintf("%v:%v: %v", e.Line, e.Column, e.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parses an expectations file, returning the Content
|
// Parse parses an expectations file, returning the Content
|
||||||
func Parse(body string) (Content, error) {
|
func Parse(body string) (Content, error) {
|
||||||
// LineType is an enumerator classifying the 'type' of the line.
|
// LineType is an enumerator classifying the 'type' of the line.
|
||||||
|
@ -130,11 +116,11 @@ func Parse(body string) (Content, error) {
|
||||||
// syntaxErr is a helper for returning a SyntaxError with the current
|
// syntaxErr is a helper for returning a SyntaxError with the current
|
||||||
// line and column index.
|
// line and column index.
|
||||||
syntaxErr := func(at Token, msg string) error {
|
syntaxErr := func(at Token, msg string) error {
|
||||||
column := at.start + 1
|
columnIdx := at.start + 1
|
||||||
if column == 1 {
|
if columnIdx == 1 {
|
||||||
column = len(l) + 1
|
columnIdx = len(l) + 1
|
||||||
}
|
}
|
||||||
return SyntaxError{lineIdx, column, msg}
|
return Diagnostic{Error, lineIdx, columnIdx, msg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// peek returns the next token without consuming it.
|
// peek returns the next token without consuming it.
|
||||||
|
|
|
@ -432,27 +432,27 @@ crbug.com/456 [ Mac ] ghi_jkl [ PASS ]
|
||||||
{
|
{
|
||||||
name: "err missing tag ']'",
|
name: "err missing tag ']'",
|
||||||
in: `[`,
|
in: `[`,
|
||||||
expectErr: "1:2: expected ']' for tags",
|
expectErr: "1:2 error: expected ']' for tags",
|
||||||
}, /////////////////////////////////////////////////////////////////////
|
}, /////////////////////////////////////////////////////////////////////
|
||||||
{
|
{
|
||||||
name: "err missing test query",
|
name: "err missing test query",
|
||||||
in: `[ a ]`,
|
in: `[ a ]`,
|
||||||
expectErr: "1:6: expected test query",
|
expectErr: "1:6 error: expected test query",
|
||||||
}, /////////////////////////////////////////////////////////////////////
|
}, /////////////////////////////////////////////////////////////////////
|
||||||
{
|
{
|
||||||
name: "err missing status EOL",
|
name: "err missing status EOL",
|
||||||
in: `[ a ] b`,
|
in: `[ a ] b`,
|
||||||
expectErr: "1:8: expected status",
|
expectErr: "1:8 error: expected status",
|
||||||
}, /////////////////////////////////////////////////////////////////////
|
}, /////////////////////////////////////////////////////////////////////
|
||||||
{
|
{
|
||||||
name: "err missing status comment",
|
name: "err missing status comment",
|
||||||
in: `[ a ] b # c`,
|
in: `[ a ] b # c`,
|
||||||
expectErr: "1:9: expected status",
|
expectErr: "1:9 error: expected status",
|
||||||
}, /////////////////////////////////////////////////////////////////////
|
}, /////////////////////////////////////////////////////////////////////
|
||||||
{
|
{
|
||||||
name: "err missing status ']'",
|
name: "err missing status ']'",
|
||||||
in: `[ a ] b [ c`,
|
in: `[ a ] b [ c`,
|
||||||
expectErr: "1:12: expected ']' for status",
|
expectErr: "1:12 error: expected ']' for status",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,592 @@
|
||||||
|
// Copyright 2022 The Dawn Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package expectations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/container"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/query"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update performs an incremental update on the expectations using the provided
|
||||||
|
// results.
|
||||||
|
//
|
||||||
|
// Update will:
|
||||||
|
// • Remove any expectation lines that have a query where no results match.
|
||||||
|
// • Remove expectations lines that are in a chunk which is not annotated with
|
||||||
|
// 'KEEP', and all test results have the status 'Pass'.
|
||||||
|
// • Remove chunks that have had all expectation lines removed.
|
||||||
|
// • Appends new chunks for flaky and failing tests which are not covered by
|
||||||
|
// existing expectation lines.
|
||||||
|
//
|
||||||
|
// Update returns a list of diagnostics for things that should be addressed.
|
||||||
|
func (c *Content) Update(results result.List) ([]Diagnostic, error) {
|
||||||
|
// Make a copy of the results. This code mutates the list.
|
||||||
|
results = append(result.List{}, results...)
|
||||||
|
|
||||||
|
// Replace statuses that the CTS runner doesn't recognize with 'Failure'
|
||||||
|
simplifyStatuses(results)
|
||||||
|
|
||||||
|
// Produce a list of tag sets.
|
||||||
|
// We reverse the declared order, as webgpu-cts/expectations.txt lists the
|
||||||
|
// most important first (OS, GPU, etc), and result.MinimalVariantTags will
|
||||||
|
// prioritize folding away the earlier tag-sets.
|
||||||
|
tagSets := make([]result.Tags, len(c.Tags.Sets))
|
||||||
|
for i, s := range c.Tags.Sets {
|
||||||
|
tagSets[len(tagSets)-i-1] = s.Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update those expectations!
|
||||||
|
u := updater{
|
||||||
|
in: *c,
|
||||||
|
out: Content{},
|
||||||
|
qt: newQueryTree(results),
|
||||||
|
tagSets: tagSets,
|
||||||
|
}
|
||||||
|
if err := u.build(); err != nil {
|
||||||
|
return nil, fmt.Errorf("while updating expectations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*c = u.out
|
||||||
|
return u.diags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updater holds the state used for updating the expectations
|
||||||
|
type updater struct {
|
||||||
|
in Content // the original expectations Content
|
||||||
|
out Content // newly built expectations Content
|
||||||
|
qt queryTree // the query tree
|
||||||
|
diags []Diagnostic // diagnostics raised during update
|
||||||
|
tagSets []result.Tags // reverse-ordered tag-sets of 'in'
|
||||||
|
}
|
||||||
|
|
||||||
|
// simplifyStatuses replaces all result statuses that are not 'Pass',
|
||||||
|
// 'RetryOnFailure', 'Slow', 'Skip' with 'Failure'.
|
||||||
|
func simplifyStatuses(results result.List) {
|
||||||
|
for i, r := range results {
|
||||||
|
switch r.Status {
|
||||||
|
case result.Pass, result.RetryOnFailure, result.Slow, result.Skip:
|
||||||
|
// keep
|
||||||
|
default:
|
||||||
|
results[i].Status = result.Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Status used to mark results that have been already handled by an
|
||||||
|
// expectation.
|
||||||
|
consumed result.Status = "<<consumed>>"
|
||||||
|
// Chunk comment for new flakes
|
||||||
|
newFlakesComment = "# New flakes. Please triage:"
|
||||||
|
// Chunk comment for new failures
|
||||||
|
newFailuresComment = "# New failures. Please triage:"
|
||||||
|
)
|
||||||
|
|
||||||
|
// queryTree holds tree of queries to all results (no filtering by tag or
|
||||||
|
// status). The queryTree is used to glob all the results that match a
|
||||||
|
// particular query.
|
||||||
|
type queryTree struct {
|
||||||
|
// All the results.
|
||||||
|
results result.List
|
||||||
|
// consumedAt is a list of line numbers for the i'th result in 'results'
|
||||||
|
// Initially all line numbers are 0. When a result is consumed the line
|
||||||
|
// number is set.
|
||||||
|
consumedAt []int
|
||||||
|
// Each tree node holds a list of indices to results.
|
||||||
|
tree query.Tree[[]int]
|
||||||
|
}
|
||||||
|
|
||||||
|
// newQueryTree builds the queryTree from the list of results.
|
||||||
|
func newQueryTree(results result.List) queryTree {
|
||||||
|
// Build a map of query to result indices
|
||||||
|
queryToIndices := map[query.Query][]int{}
|
||||||
|
for i, r := range results {
|
||||||
|
l := queryToIndices[r.Query]
|
||||||
|
l = append(l, i)
|
||||||
|
queryToIndices[r.Query] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the query tree to result indices
|
||||||
|
tree := query.Tree[[]int]{}
|
||||||
|
for query, indices := range queryToIndices {
|
||||||
|
if err := tree.Add(query, indices); err != nil {
|
||||||
|
// Unreachable: The only error we could get is duplicate data for
|
||||||
|
// the same query, which should be impossible.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
consumedAt := make([]int, len(results))
|
||||||
|
return queryTree{results, consumedAt, tree}
|
||||||
|
}
|
||||||
|
|
||||||
|
// glob returns the list of results matching the given tags under (or with) the
|
||||||
|
// given query.
|
||||||
|
func (qt *queryTree) glob(q query.Query) (result.List, error) {
|
||||||
|
glob, err := qt.tree.Glob(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("while gathering results for query '%v': %w", q, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := result.List{}
|
||||||
|
for _, indices := range glob {
|
||||||
|
for _, idx := range indices.Data {
|
||||||
|
out = append(out, qt.results[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// globAndCheckForCollisions returns the list of results matching the given tags
|
||||||
|
// under (or with) the given query.
|
||||||
|
// globAndCheckForCollisions will return an error if any of the results are
|
||||||
|
// already consumed by a non-zero line. The non-zero line distinguishes between
|
||||||
|
// results consumed by expectations declared in the input (non-zero line), vs
|
||||||
|
// those that were introduced by the update (zero line). We only want to error
|
||||||
|
// if there's a collision in user declared expectations.
|
||||||
|
func (qt *queryTree) globAndCheckForCollisions(q query.Query, t result.Tags) (result.List, error) {
|
||||||
|
glob, err := qt.tree.Glob(q)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := result.List{}
|
||||||
|
for _, indices := range glob {
|
||||||
|
for _, idx := range indices.Data {
|
||||||
|
if r := qt.results[idx]; r.Tags.ContainsAll(t) {
|
||||||
|
if at := qt.consumedAt[idx]; at > 0 {
|
||||||
|
if len(t) > 0 {
|
||||||
|
return nil, fmt.Errorf("%v %v collides with expectation at line %v", t, q, at)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%v collides with expectation at line %v", q, at)
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// markAsConsumed marks all the results matching the given tags
|
||||||
|
// under (or with) the given query, as consumed.
|
||||||
|
// line is used to record the line at which the results were consumed. If the
|
||||||
|
// results were consumed as part of generating new expectations then line should
|
||||||
|
// be 0. See queryTree.globAndCheckForCollisions().
|
||||||
|
func (qt *queryTree) markAsConsumed(q query.Query, t result.Tags, line int) {
|
||||||
|
if glob, err := qt.tree.Glob(q); err == nil {
|
||||||
|
for _, indices := range glob {
|
||||||
|
for _, idx := range indices.Data {
|
||||||
|
r := &qt.results[idx]
|
||||||
|
if r.Tags.ContainsAll(t) {
|
||||||
|
r.Status = consumed
|
||||||
|
qt.consumedAt[idx] = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build is the updater top-level function.
|
||||||
|
// build first appends to u.out all chunks from 'u.in' with expectations updated
|
||||||
|
// using the new results, and then appends any new expectations to u.out.
|
||||||
|
func (u *updater) build() error {
|
||||||
|
// Update all the existing chunks
|
||||||
|
for _, in := range u.in.Chunks {
|
||||||
|
out := u.chunk(in)
|
||||||
|
|
||||||
|
// If all chunk had expectations, but now they've gone, remove the chunk
|
||||||
|
if len(in.Expectations) > 0 && len(out.Expectations) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out.IsBlankLine() {
|
||||||
|
u.out.MaybeAddBlankLine()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.out.Chunks = append(u.out.Chunks, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit new expectations (flaky, failing)
|
||||||
|
if err := u.addNewExpectations(); err != nil {
|
||||||
|
return fmt.Errorf("failed to add new expectations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunk returns a new Chunk, based on 'in', with the expectations updated.
|
||||||
|
func (u *updater) chunk(in Chunk) Chunk {
|
||||||
|
if len(in.Expectations) == 0 {
|
||||||
|
return in // Just a comment / blank line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over any untriaged failures / flake chunks.
|
||||||
|
// We'll just rebuild them at the end.
|
||||||
|
if len(in.Comments) > 0 {
|
||||||
|
switch in.Comments[0] {
|
||||||
|
case newFailuresComment, newFlakesComment:
|
||||||
|
return Chunk{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keep := false // Does the chunk comment contain 'KEEP' ?
|
||||||
|
for _, l := range in.Comments {
|
||||||
|
if strings.Contains(l, "KEEP") {
|
||||||
|
keep = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin building the output chunk.
|
||||||
|
// Copy over the chunk's comments.
|
||||||
|
out := Chunk{Comments: in.Comments}
|
||||||
|
|
||||||
|
// Build the new chunk's expectations
|
||||||
|
for _, exIn := range in.Expectations {
|
||||||
|
exOut := u.expectation(exIn, keep)
|
||||||
|
out.Expectations = append(out.Expectations, exOut...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the expectations to keep things clean and tidy.
|
||||||
|
sort.Slice(out.Expectations, func(i, j int) bool {
|
||||||
|
switch {
|
||||||
|
case out.Expectations[i].Query < out.Expectations[j].Query:
|
||||||
|
return true
|
||||||
|
case out.Expectations[i].Query > out.Expectations[j].Query:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a := result.TagsToString(out.Expectations[i].Tags)
|
||||||
|
b := result.TagsToString(out.Expectations[j].Tags)
|
||||||
|
switch {
|
||||||
|
case a < b:
|
||||||
|
return true
|
||||||
|
case a > b:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// chunk returns a new list of Expectations, based on the Expectation 'in',
|
||||||
|
// using the new result data.
|
||||||
|
func (u *updater) expectation(in Expectation, keep bool) []Expectation {
|
||||||
|
// noResults is a helper for returning when the expectation has no test
|
||||||
|
// results.
|
||||||
|
// If the expectation has an expected 'Skip' result, then we're likely
|
||||||
|
// to be missing results (as the test was not run). In this situation
|
||||||
|
// the expectation is preserved, and no diagnostics are raised.
|
||||||
|
// If the expectation did not have a 'Skip' result, then a diagnostic will
|
||||||
|
// be raised and the expectation will be removed.
|
||||||
|
noResults := func() []Expectation {
|
||||||
|
if container.NewSet(in.Status...).Contains(string(result.Skip)) {
|
||||||
|
return []Expectation{in}
|
||||||
|
}
|
||||||
|
// Expectation does not have a 'Skip' result.
|
||||||
|
if len(in.Tags) > 0 {
|
||||||
|
u.diag(Warning, in.Line, "no results found for '%v' with tags %v", in.Query, in.Tags)
|
||||||
|
} else {
|
||||||
|
u.diag(Warning, in.Line, "no results found for '%v'", in.Query)
|
||||||
|
}
|
||||||
|
// Remove the no-results expectation
|
||||||
|
return []Expectation{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab all the results that match the expectation's query
|
||||||
|
q := query.Parse(in.Query)
|
||||||
|
|
||||||
|
// Glob the results for the expectation's query + tag combination.
|
||||||
|
// Ensure that none of these are already consumed.
|
||||||
|
results, err := u.qt.globAndCheckForCollisions(q, in.Tags)
|
||||||
|
// If we can't find any results for this query + tag combination, then bail.
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &query.ErrNoDataForQuery{}):
|
||||||
|
return noResults()
|
||||||
|
case err != nil:
|
||||||
|
u.diag(Error, in.Line, "%v", err)
|
||||||
|
return []Expectation{}
|
||||||
|
case len(results) == 0:
|
||||||
|
return noResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before returning, mark all the results as consumed.
|
||||||
|
// Note: this has to happen *after* we've generated the new expectations, as
|
||||||
|
// marking the results as 'consumed' will impact the logic of
|
||||||
|
// expectationsForRoot()
|
||||||
|
defer u.qt.markAsConsumed(q, in.Tags, in.Line)
|
||||||
|
|
||||||
|
if keep { // Expectation chunk was marked with 'KEEP'
|
||||||
|
// Add a diagnostic if all tests of the expectation were 'Pass'
|
||||||
|
if s := results.Statuses(); len(s) == 1 && s.One() == result.Pass {
|
||||||
|
if c := len(results); c > 1 {
|
||||||
|
u.diag(Note, in.Line, "all %d tests now pass", len(results))
|
||||||
|
} else {
|
||||||
|
u.diag(Note, in.Line, "test now passes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []Expectation{in}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the expectations for this query.
|
||||||
|
return u.expectationsForRoot(q, in.Line, in.Bug, in.Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewExpectations (potentially) appends to 'u.out' chunks for new flaky and
|
||||||
|
// failing tests.
|
||||||
|
func (u *updater) addNewExpectations() error {
|
||||||
|
// Scan the full result list to obtain all the test variants
|
||||||
|
// (unique tag combinations).
|
||||||
|
allVariants := u.qt.results.Variants()
|
||||||
|
|
||||||
|
// For each variant:
|
||||||
|
// • Build a query tree using the results filtered to the variant, and then
|
||||||
|
// reduce the tree.
|
||||||
|
// • Take all the reduced-tree leaf nodes, and add these to 'roots'.
|
||||||
|
// Once we've collected all the roots, we'll use these to build the
|
||||||
|
// expectations across the reduced set of tags.
|
||||||
|
roots := container.NewMap[string, query.Query]()
|
||||||
|
for _, variant := range allVariants {
|
||||||
|
// Build a tree from the results matching the given variant.
|
||||||
|
tree, err := u.qt.results.FilterByVariant(variant).StatusTree()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("while building tree for tags '%v': %w", variant, err)
|
||||||
|
}
|
||||||
|
// Reduce the tree.
|
||||||
|
tree.Reduce(treeReducer)
|
||||||
|
// Add all the reduced leaf nodes to 'roots'.
|
||||||
|
for _, qd := range tree.List() {
|
||||||
|
roots.Add(qd.Query.String(), qd.Query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build all the expectations for each of the roots.
|
||||||
|
expectations := []Expectation{}
|
||||||
|
for _, root := range roots.Values() {
|
||||||
|
expectations = append(expectations, u.expectationsForRoot(
|
||||||
|
root, // Root query
|
||||||
|
0, // Line number
|
||||||
|
"crbug.com/dawn/0000", // Bug
|
||||||
|
"", // Comment
|
||||||
|
)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bin the expectations by failure or flake.
|
||||||
|
flakes, failures := []Expectation{}, []Expectation{}
|
||||||
|
for _, r := range expectations {
|
||||||
|
if container.NewSet(r.Status...).Contains(string(result.RetryOnFailure)) {
|
||||||
|
flakes = append(flakes, r)
|
||||||
|
} else {
|
||||||
|
failures = append(failures, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chunks for any flakes and failures, in that order.
|
||||||
|
for _, group := range []struct {
|
||||||
|
results []Expectation
|
||||||
|
comment string
|
||||||
|
}{
|
||||||
|
{flakes, newFlakesComment},
|
||||||
|
{failures, newFailuresComment},
|
||||||
|
} {
|
||||||
|
if len(group.results) > 0 {
|
||||||
|
u.out.MaybeAddBlankLine()
|
||||||
|
u.out.Chunks = append(u.out.Chunks, Chunk{
|
||||||
|
Comments: []string{group.comment},
|
||||||
|
Expectations: group.results,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// expectationsForRoot builds a list of expectations that cover the failing
|
||||||
|
// tests for the results under root.
|
||||||
|
// The returned list of expectations is optimized by reducing queries to the
|
||||||
|
// most common root, and reducing tags to the smallest required set.
|
||||||
|
func (u *updater) expectationsForRoot(
|
||||||
|
root query.Query, // The sub-tree query root
|
||||||
|
line int, // The originating line, when producing diagnostics
|
||||||
|
bug string, // The bug to apply to all returned expectations
|
||||||
|
comment string, // The comment to apply to all returned expectations
|
||||||
|
) []Expectation {
|
||||||
|
results, err := u.qt.glob(root)
|
||||||
|
if err != nil {
|
||||||
|
u.diag(Error, line, "%v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using the full list of unfiltered tests, generate the minimal set of
|
||||||
|
// variants (tags) that uniquely classify the results with differing status.
|
||||||
|
minimalVariants := u.
|
||||||
|
cleanupTags(results).
|
||||||
|
MinimalVariantTags(u.tagSets)
|
||||||
|
|
||||||
|
// For each minimized variant...
|
||||||
|
reduced := result.List{}
|
||||||
|
for _, variant := range minimalVariants {
|
||||||
|
// Build a query tree from this variant...
|
||||||
|
tree := result.StatusTree{}
|
||||||
|
filtered := results.FilterByTags(variant)
|
||||||
|
for _, r := range filtered {
|
||||||
|
// Note: variants may overlap, but overlaped queries will have
|
||||||
|
// identical statuses, so we can just ignore the error for Add().
|
||||||
|
tree.Add(r.Query, r.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... and reduce the tree by collapsing sub-trees that have common
|
||||||
|
// statuses.
|
||||||
|
tree.ReduceUnder(root, treeReducer)
|
||||||
|
|
||||||
|
// Append the reduced tree nodes to the results list
|
||||||
|
for _, qs := range tree.List() {
|
||||||
|
reduced = append(reduced, result.Result{
|
||||||
|
Query: qs.Query,
|
||||||
|
Tags: variant,
|
||||||
|
Status: qs.Data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out any results that passed or have already been consumed
|
||||||
|
filtered := reduced.Filter(func(r result.Result) bool {
|
||||||
|
return r.Status != result.Pass && r.Status != consumed
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mark all the new expectation results as consumed.
|
||||||
|
for _, r := range filtered {
|
||||||
|
u.qt.markAsConsumed(r.Query, r.Tags, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the results to expectations.
|
||||||
|
return u.resultsToExpectations(filtered, bug, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resultsToExpectations returns a list of expectations from the given results.
|
||||||
|
// Each expectation will have the same query, tags and status as the input
|
||||||
|
// result, along with the specified bug and comment.
|
||||||
|
//
|
||||||
|
// If the result query target is a test without a wildcard, then the expectation
|
||||||
|
// will have a wildcard automatically appended. This is to satisfy a requirement
|
||||||
|
// of the expectation validator.
|
||||||
|
func (u *updater) resultsToExpectations(results result.List, bug, comment string) []Expectation {
|
||||||
|
results.Sort()
|
||||||
|
|
||||||
|
out := make([]Expectation, len(results))
|
||||||
|
for i, r := range results {
|
||||||
|
q := r.Query.String()
|
||||||
|
if r.Query.Target() == query.Tests && !r.Query.IsWildcard() {
|
||||||
|
// The expectation validator wants a trailing ':' for test queries
|
||||||
|
q += query.TargetDelimiter
|
||||||
|
}
|
||||||
|
out[i] = Expectation{
|
||||||
|
Bug: bug,
|
||||||
|
Tags: r.Tags,
|
||||||
|
Query: q,
|
||||||
|
Status: []string{string(r.Status)},
|
||||||
|
Comment: comment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupTags returns a copy of the provided results with:
|
||||||
|
// • All tags not found in the expectations list removed
|
||||||
|
// • All but the highest priority tag for any tag-set.
|
||||||
|
// The tag sets are defined by the `BEGIN TAG HEADER` / `END TAG HEADER`
|
||||||
|
// section at the top of the expectations file.
|
||||||
|
func (u *updater) cleanupTags(results result.List) result.List {
|
||||||
|
return results.TransformTags(func(t result.Tags) result.Tags {
|
||||||
|
type HighestPrioritySetTag struct {
|
||||||
|
tag string
|
||||||
|
priority int
|
||||||
|
}
|
||||||
|
// Set name to highest priority tag for that set
|
||||||
|
best := map[string]HighestPrioritySetTag{}
|
||||||
|
for tag := range t {
|
||||||
|
sp, ok := u.in.Tags.ByName[tag]
|
||||||
|
if ok {
|
||||||
|
if set := best[sp.Set]; sp.Priority >= set.priority {
|
||||||
|
best[sp.Set] = HighestPrioritySetTag{tag, sp.Priority}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = result.NewTags()
|
||||||
|
for _, ts := range best {
|
||||||
|
t.Add(ts.tag)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// treeReducer is a function that can be used by StatusTree.Reduce() to reduce
|
||||||
|
// tree nodes with the same status.
|
||||||
|
// treeReducer will collapse trees nodes if any of the following are true:
|
||||||
|
// • All child nodes have the same status
|
||||||
|
// • More than 75% of the child nodes have a non-pass status, and none of the
|
||||||
|
// children are consumed.
|
||||||
|
// • There are more than 20 child nodes with a non-pass status, and none of the
|
||||||
|
// children are consumed.
|
||||||
|
func treeReducer(statuses []result.Status) *result.Status {
|
||||||
|
counts := map[result.Status]int{}
|
||||||
|
for _, s := range statuses {
|
||||||
|
counts[s] = counts[s] + 1
|
||||||
|
}
|
||||||
|
if len(counts) == 1 {
|
||||||
|
return &statuses[0] // All the same status
|
||||||
|
}
|
||||||
|
if counts[consumed] > 0 {
|
||||||
|
return nil // Partially consumed trees cannot be merged
|
||||||
|
}
|
||||||
|
highestNonPassCount := 0
|
||||||
|
highestNonPassStatus := result.Failure
|
||||||
|
for s, n := range counts {
|
||||||
|
if s != result.Pass {
|
||||||
|
if percent := (100 * n) / len(statuses); percent > 75 {
|
||||||
|
// Over 75% of all the children are of non-pass status s.
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
if n > highestNonPassCount {
|
||||||
|
highestNonPassCount = n
|
||||||
|
highestNonPassStatus = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if highestNonPassCount > 20 {
|
||||||
|
// Over 20 child node failed.
|
||||||
|
return &highestNonPassStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// diag appends a new diagnostic to u.diags with the given severity, line and
|
||||||
|
// message.
|
||||||
|
func (u *updater) diag(severity Severity, line int, msg string, args ...interface{}) {
|
||||||
|
u.diags = append(u.diags, Diagnostic{
|
||||||
|
Severity: severity,
|
||||||
|
Line: line,
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,672 @@
|
||||||
|
// Copyright 2022 The Dawn Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package expectations_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/expectations"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/query"
|
||||||
|
"dawn.googlesource.com/dawn/tools/src/cts/result"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Q = query.Parse
|
||||||
|
|
||||||
|
func TestUpdate(t *testing.T) {
|
||||||
|
header := `# BEGIN TAG HEADER
|
||||||
|
# OS
|
||||||
|
# tags: [ os-a os-b os-c ]
|
||||||
|
# GPU
|
||||||
|
# tags: [ gpu-a gpu-b gpu-c ]
|
||||||
|
# END TAG HEADER
|
||||||
|
`
|
||||||
|
headerLines := strings.Count(header, "\n")
|
||||||
|
|
||||||
|
type Test struct {
|
||||||
|
name string
|
||||||
|
expectations string
|
||||||
|
results result.List
|
||||||
|
updated string
|
||||||
|
diagnostics []expectations.Diagnostic
|
||||||
|
err string
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "empty results",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "no results found",
|
||||||
|
expectations: `
|
||||||
|
crbug.com/a/123 a:missing,test,result:* [ Failure ]
|
||||||
|
crbug.com/a/123 [ tag ] another:missing,test,result:* [ Failure ]
|
||||||
|
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Warning,
|
||||||
|
Line: headerLines + 2,
|
||||||
|
Message: "no results found for 'a:missing,test,result:*'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Severity: expectations.Warning,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "no results found for 'another:missing,test,result:*' with tags [tag]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "no results found KEEP",
|
||||||
|
expectations: `
|
||||||
|
# KEEP
|
||||||
|
crbug.com/a/123 a:missing,test,result:* [ Failure ]
|
||||||
|
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Warning,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "no results found for 'a:missing,test,result:*'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "no results found Skip",
|
||||||
|
expectations: `
|
||||||
|
crbug.com/a/123 a:missing,test,result:* [ Skip ]
|
||||||
|
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("some:other,test:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
crbug.com/a/123 a:missing,test,result:* [ Skip ]
|
||||||
|
|
||||||
|
some:other,test:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "simple expectation collision",
|
||||||
|
expectations: `
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Error,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "a:b,c:* collides with expectation at line 8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "simple expectation with tags",
|
||||||
|
expectations: `
|
||||||
|
[ os-a ] a:b,c:* [ Failure ]
|
||||||
|
[ gpu-b ] a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Error,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "[gpu-b] a:b,c:* collides with expectation at line 8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "simple expectation collision KEEP",
|
||||||
|
expectations: `
|
||||||
|
# KEEP
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# KEEP
|
||||||
|
a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Error,
|
||||||
|
Line: headerLines + 4,
|
||||||
|
Message: "a:b,c:* collides with expectation at line 9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "collision with child-expectation",
|
||||||
|
expectations: `
|
||||||
|
a:b:x:* [ Failure ]
|
||||||
|
a:b:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b:x:*"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b:y:*"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
a:b:x:* [ Failure ]
|
||||||
|
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 a:b:y:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Error,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "a:b:* collides with expectation at line 8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "collision with parent-expectation",
|
||||||
|
expectations: `
|
||||||
|
a:b:* [ Failure ]
|
||||||
|
a:b:x:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b:x:*"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b:y:*"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
a:b:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Error,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "a:b:x:* collides with expectation at line 8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "expectation test now passes",
|
||||||
|
expectations: `
|
||||||
|
crbug.com/a/123 [ gpu-a os-a ] a:b,c:* [ Failure ]
|
||||||
|
crbug.com/a/123 [ gpu-b os-b ] a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Abort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
crbug.com/a/123 [ os-b ] a:b,c:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "expectation case now passes",
|
||||||
|
expectations: `
|
||||||
|
crbug.com/a/123 [ gpu-a os-a ] a:b,c:d [ Failure ]
|
||||||
|
crbug.com/a/123 [ gpu-b os-b ] a:b,c:d [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Abort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
crbug.com/a/123 [ os-b ] a:b,c:d: [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "expectation case now passes KEEP - single",
|
||||||
|
expectations: `
|
||||||
|
# KEEP
|
||||||
|
crbug.com/a/123 [ gpu-a os-a ] a:b,c:d [ Failure ]
|
||||||
|
crbug.com/a/123 [ gpu-b os-b ] a:b,c:d [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:d"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Abort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# KEEP
|
||||||
|
crbug.com/a/123 [ gpu-a os-a ] a:b,c:d [ Failure ]
|
||||||
|
crbug.com/a/123 [ gpu-b os-b ] a:b,c:d [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Note,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "test now passes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "expectation case now passes KEEP - multiple",
|
||||||
|
expectations: `
|
||||||
|
# KEEP
|
||||||
|
crbug.com/a/123 a:b,c:d:* [ Failure ]
|
||||||
|
`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{Query: Q("a:b,c:d:a"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:d:b"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:d:c"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:d:d"), Status: result.Pass},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# KEEP
|
||||||
|
crbug.com/a/123 a:b,c:d:* [ Failure ]
|
||||||
|
`,
|
||||||
|
diagnostics: []expectations.Diagnostic{
|
||||||
|
{
|
||||||
|
Severity: expectations.Note,
|
||||||
|
Line: headerLines + 3,
|
||||||
|
Message: "all 4 tests now pass",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "new test results",
|
||||||
|
expectations: `# A comment`,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_a:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Abort,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_a:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-b"),
|
||||||
|
Status: result.Abort,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_c:case=4;*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Crash,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_c:case=5;*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.RetryOnFailure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_b;case=5;*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-b"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_b:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.Skip,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_b:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-a"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("suite:dir_a,dir_b:test_c:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-a"),
|
||||||
|
Status: result.RetryOnFailure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `# A comment
|
||||||
|
|
||||||
|
# New flakes. Please triage:
|
||||||
|
crbug.com/dawn/0000 [ gpu-a os-a ] suite:dir_a,dir_b:test_c:* [ RetryOnFailure ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-b os-b ] suite:dir_a,dir_b:test_c:case=5;* [ RetryOnFailure ]
|
||||||
|
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 [ gpu-b os-a ] suite:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-a os-a ] suite:dir_a,dir_b:test_a:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-a os-a ] suite:dir_a,dir_b:test_b:* [ Skip ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-b os-b ] suite:dir_a,dir_b:test_c:case=4;* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "filter unknown tags",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-a", "gpu-x"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-b", "gpu-x"),
|
||||||
|
Status: result.Crash,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-x", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-x", "gpu-a"),
|
||||||
|
Status: result.Crash,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-c", "gpu-c"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 [ gpu-a ] a:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-b ] a:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ os-a ] a:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ os-b ] a:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "prioritize tag sets",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-a", "os-c", "gpu-b"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("gpu-a", "os-b", "gpu-c"),
|
||||||
|
Status: result.Failure,
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q("a:b,c:*"),
|
||||||
|
Tags: result.NewTags("os-c", "gpu-c"),
|
||||||
|
Status: result.Pass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 [ gpu-b os-c ] a:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 [ gpu-c os-b ] a:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "merge when over 75% of children fail",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{Query: Q("a:b,c:t0:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t1:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t2:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t3:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t4:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t5:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t6:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t7:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t8:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t9:*"), Status: result.Failure},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 a:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "don't merge when under 75% of children fail",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{
|
||||||
|
result.Result{Query: Q("a:b,c:t0:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t1:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t2:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t3:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t4:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t5:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t6:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t7:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t8:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t9:*"), Status: result.Failure},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 a:b,c:t0:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t2:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t3:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t5:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t6:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t8:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t9:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "merge when over 20 children fail",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{ // 21 failures, 70% fail
|
||||||
|
result.Result{Query: Q("a:b,c:t00:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t01:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t02:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t03:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t04:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t05:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t06:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t07:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t08:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t09:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t10:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t11:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t12:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t13:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t14:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t15:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t16:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t17:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t18:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t19:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t20:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t21:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t22:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t23:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t24:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t25:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t26:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t27:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t28:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t29:*"), Status: result.Failure},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 a:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
name: "dont merge when under 21 children fail",
|
||||||
|
expectations: ``,
|
||||||
|
results: result.List{ // 20 failures, 66% fail
|
||||||
|
result.Result{Query: Q("a:b,c:t00:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t01:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t02:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t03:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t04:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t05:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t06:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t07:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t08:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t09:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t10:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t11:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t12:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t13:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t14:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t15:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t16:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t17:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t18:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t19:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t20:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t21:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t22:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t23:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t24:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t25:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t26:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t27:*"), Status: result.Pass},
|
||||||
|
result.Result{Query: Q("a:b,c:t28:*"), Status: result.Failure},
|
||||||
|
result.Result{Query: Q("a:b,c:t29:*"), Status: result.Failure},
|
||||||
|
},
|
||||||
|
updated: `
|
||||||
|
# New failures. Please triage:
|
||||||
|
crbug.com/dawn/0000 a:b,c:t00:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t02:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t04:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t05:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t06:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t08:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t09:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t10:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t13:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t15:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t16:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t18:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t19:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t20:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t22:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t23:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t25:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t26:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t28:* [ Failure ]
|
||||||
|
crbug.com/dawn/0000 a:b,c:t29:* [ Failure ]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
ex, err := expectations.Parse(header + test.expectations)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("'%v': expectations.Parse():\n%v", test.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
errMsg := ""
|
||||||
|
diagnostics, err := ex.Update(test.results)
|
||||||
|
if err != nil {
|
||||||
|
errMsg = err.Error()
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(errMsg, test.err); diff != "" {
|
||||||
|
t.Errorf("'%v': expectations.Update() error:\n%v", test.name, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(diagnostics, test.diagnostics); diff != "" {
|
||||||
|
t.Errorf("'%v': diagnostics were not as expected:\n%v", test.name, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(
|
||||||
|
strings.Split(ex.String(), "\n"),
|
||||||
|
strings.Split(header+test.updated, "\n")); diff != "" {
|
||||||
|
t.Errorf("'%v': updated was not as expected:\n%v", test.name, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -221,6 +221,14 @@ func (l List) FilterByTags(tags Tags) List {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterByVariant returns the results that exactly match the given tags
|
||||||
|
func (l List) FilterByVariant(tags Tags) List {
|
||||||
|
str := TagsToString(tags)
|
||||||
|
return l.Filter(func(r Result) bool {
|
||||||
|
return len(r.Tags) == len(tags) && TagsToString(r.Tags) == str
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Statuses is a set of Status
|
// Statuses is a set of Status
|
||||||
type Statuses = container.Set[Status]
|
type Statuses = container.Set[Status]
|
||||||
|
|
||||||
|
|
|
@ -782,6 +782,96 @@ func TestFilterByTags(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterByVariant(t *testing.T) {
|
||||||
|
type Test struct {
|
||||||
|
results result.List
|
||||||
|
tags result.Tags
|
||||||
|
expect result.List
|
||||||
|
}
|
||||||
|
for _, test := range []Test{
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`a`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`b`),
|
||||||
|
Status: result.Failure,
|
||||||
|
Tags: result.NewTags("y"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`c`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x", "y"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: result.NewTags("x", "y"),
|
||||||
|
expect: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`c`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x", "y"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`a`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`b`),
|
||||||
|
Status: result.Failure,
|
||||||
|
Tags: result.NewTags("y"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`c`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x", "y"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: result.NewTags("x"),
|
||||||
|
expect: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`a`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ //////////////////////////////////////////////////////////////////////
|
||||||
|
results: result.List{
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`a`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`b`),
|
||||||
|
Status: result.Failure,
|
||||||
|
Tags: result.NewTags("y"),
|
||||||
|
},
|
||||||
|
result.Result{
|
||||||
|
Query: Q(`c`),
|
||||||
|
Status: result.Pass,
|
||||||
|
Tags: result.NewTags("x", "y"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: result.NewTags("q"),
|
||||||
|
expect: result.List{},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
got := test.results.FilterByVariant(test.tags)
|
||||||
|
if diff := cmp.Diff(got, test.expect); diff != "" {
|
||||||
|
t.Errorf("Results:\n%v\nFilterByVariant(%v) was not as expected:\n%v", test.results, test.tags, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStatuses(t *testing.T) {
|
func TestStatuses(t *testing.T) {
|
||||||
type Test struct {
|
type Test struct {
|
||||||
results result.List
|
results result.List
|
||||||
|
|
Loading…
Reference in New Issue