| // 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 expectations provides types and helpers for parsing, updating and | |
| // writing WebGPU expectations files. | |
| // | |
| // See <dawn>/webgpu-cts/expectations.txt for more information. | |
| package expectations | |
| import ( | |
| "fmt" | |
| "io" | |
| "reflect" | |
| "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" | |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" | |
| "dawn.googlesource.com/dawn/tools/src/reducedglob" | |
| ) | |
| // Content holds the full content of an expectations file. | |
| type Content struct { | |
| Chunks []Chunk | |
| Tags Tags | |
| } | |
| // Chunk is an optional comment followed by a run of expectations. | |
| // A chunk ends at the first blank line, or at the transition from an | |
| // expectation to a line-comment. | |
| type Chunk struct { | |
| Comments []string // Line comments at the top of the chunk | |
| Expectations Expectations // Expectations for the chunk | |
| } | |
| // ExpectationType is a Type + enum for whether an Expectation's Query contains globs or not. | |
| type ExpectationType int | |
| const ( | |
| UNDETERMINED ExpectationType = iota | |
| EXACT | |
| GLOB | |
| ) | |
| // Expectation holds a single expectation line | |
| type Expectation struct { | |
| Line int // The 1-based line number of the expectation | |
| Bug string // The associated bug URL for this expectation | |
| Tags result.Tags // Tags used to filter the expectation | |
| Query string // The CTS query | |
| Status []string // The expected result status | |
| Comment string // Optional comment at end of line | |
| expectationType ExpectationType // Cached value of whether |Query| is an exact match or not | |
| globMatcher *reducedglob.ReducedGlob // Cached matcher for the case where expectationType == GLOB | |
| } | |
| // Expectations are a list of Expectation | |
| type Expectations []Expectation | |
| // Load loads the expectation file at 'path', returning a Content. | |
| func Load(path string, fsReader oswrapper.FilesystemReader) (Content, error) { | |
| content, err := fsReader.ReadFile(path) | |
| if err != nil { | |
| return Content{}, err | |
| } | |
| ex, err := Parse(path, string(content)) | |
| if err != nil { | |
| return Content{}, err | |
| } | |
| return ex, nil | |
| } | |
| // Save saves the Content file to 'path'. | |
| func (c *Content) Save(path string, fsWriter oswrapper.FilesystemWriter) error { | |
| f, err := fsWriter.Create(path) | |
| if err != nil { | |
| return err | |
| } | |
| defer f.Close() | |
| return c.Write(f) | |
| } | |
| // Clone makes a deep-copy of the Content. | |
| func (c *Content) Clone() Content { | |
| chunks := make([]Chunk, len(c.Chunks)) | |
| for i, c := range c.Chunks { | |
| chunks[i] = c.Clone() | |
| } | |
| return Content{chunks, c.Tags.Clone()} | |
| } | |
| // Empty returns true if the Content has no chunks. | |
| func (c *Content) Empty() bool { | |
| return len(c.Chunks) == 0 | |
| } | |
| // Write writes the Content, in textual form, to the writer w. | |
| func (c *Content) Write(w io.Writer) error { | |
| for i, chunk := range c.Chunks { | |
| if i > 0 { | |
| if _, err := fmt.Fprintln(w); err != nil { | |
| return err | |
| } | |
| } | |
| for _, comment := range chunk.Comments { | |
| if _, err := fmt.Fprintln(w, comment); err != nil { | |
| return err | |
| } | |
| } | |
| for _, expectation := range chunk.Expectations { | |
| if _, err := fmt.Fprintln(w, expectation.AsExpectationFileString()); err != nil { | |
| return err | |
| } | |
| } | |
| } | |
| return nil | |
| } | |
| // String returns the Content as a string. | |
| func (c *Content) String() string { | |
| sb := strings.Builder{} | |
| c.Write(&sb) | |
| return sb.String() | |
| } | |
| // Format sorts each chunk of the Content in place. | |
| func (c *Content) Format() { | |
| for _, chunk := range c.Chunks { | |
| chunk.Expectations.Sort() | |
| } | |
| } | |
| // RemoveExpectationsForUnknownTests modifies the Content in place so that all | |
| // contained Expectations apply to tests in the given testlist. | |
| func (c *Content) RemoveExpectationsForUnknownTests(testlist *[]query.Query) error { | |
| // Converting into a set allows us to much more efficiently check if a | |
| // non-wildcard expectation is for a valid test. | |
| knownTestNames := container.NewSet[string]() | |
| for _, testQuery := range *testlist { | |
| knownTestNames.Add(testQuery.ExpectationFileString()) | |
| } | |
| prunedChunkSlice := make([]Chunk, 0) | |
| for _, chunk := range c.Chunks { | |
| prunedChunk := chunk.Clone() | |
| // If we don't have any expectations already, just add the chunk back | |
| // immediately to avoid removing comments, especially the header. | |
| if prunedChunk.IsCommentOnly() { | |
| prunedChunkSlice = append(prunedChunkSlice, prunedChunk) | |
| continue | |
| } | |
| prunedChunk.Expectations = make(Expectations, 0) | |
| for _, expectation := range chunk.Expectations { | |
| // We don't actually parse the query string into a Query since wildcards | |
| // are treated differently between expectations and CTS queries. | |
| if expectation.IsGlobExpectation() { | |
| for testName := range knownTestNames { | |
| if expectation.AppliesToTest(testName) { | |
| prunedChunk.Expectations = append(prunedChunk.Expectations, expectation) | |
| break | |
| } | |
| } | |
| } else { | |
| // We could technically use AppliesToTest() here like we do for glob | |
| // expectations, but Contains() will be faster due to use of a set. | |
| if knownTestNames.Contains(expectation.Query) { | |
| prunedChunk.Expectations = append(prunedChunk.Expectations, expectation) | |
| } | |
| } | |
| } | |
| if len(prunedChunk.Expectations) > 0 { | |
| prunedChunkSlice = append(prunedChunkSlice, prunedChunk) | |
| } | |
| } | |
| c.Chunks = prunedChunkSlice | |
| return nil | |
| } | |
| // IsCommentOnly returns true if the Chunk contains comments and no expectations. | |
| func (c Chunk) IsCommentOnly() bool { | |
| return len(c.Comments) > 0 && len(c.Expectations) == 0 | |
| } | |
| // Clone returns a deep-copy of the Chunk | |
| func (c Chunk) Clone() Chunk { | |
| comments := make([]string, len(c.Comments)) | |
| copy(comments, c.Comments) | |
| expectations := make([]Expectation, len(c.Expectations)) | |
| for i, e := range c.Expectations { | |
| expectations[i] = e.Clone() | |
| } | |
| return Chunk{comments, expectations} | |
| } | |
| func (c Chunk) ContainedWithinList(chunkList *[]Chunk) bool { | |
| for _, otherChunk := range *chunkList { | |
| if reflect.DeepEqual(c, otherChunk) { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // IsGlobExpectation returns whether the Expectation is a glob expectation or | |
| // not. Glob-iness is cached after the first call. | |
| func (e *Expectation) IsGlobExpectation() bool { | |
| if e.expectationType != UNDETERMINED { | |
| return e.expectationType == GLOB | |
| } | |
| // Count the total number of escaped and unescaped wildcard characters. If | |
| // they do not match, then that means we have at least one glob, which means | |
| // this is a glob expectation. | |
| numEscapedWildcards := strings.Count(e.Query, reducedglob.ESCAPED_WILDCARD) | |
| numNonEscapedWildcards := strings.Count(e.Query, reducedglob.UNESCAPED_WILDCARD) | |
| if numEscapedWildcards == numNonEscapedWildcards { | |
| e.expectationType = EXACT | |
| return false | |
| } | |
| e.expectationType = GLOB | |
| return true | |
| } | |
| // ensureGlobMatcherIsSet creates and caches a reducedglob.ReducedGlob for the | |
| // Expectation's Query field. Should only be called in cases where | |
| // IsGlobExpectation() returns true. | |
| func (e *Expectation) ensureGlobMatcherIsSet() { | |
| if e.globMatcher != nil { | |
| return | |
| } | |
| if e.expectationType != GLOB { | |
| panic("ensureGlobMatcherIsSet should only be ever be called when the for glob expectations") | |
| } | |
| e.globMatcher = reducedglob.NewReducedGlob(e.Query) | |
| } | |
| // AppliesToResult returns whether the Expectation applies to the test + config | |
| // represented by the Result. | |
| func (e *Expectation) AppliesToResult(r result.Result) bool { | |
| // Tags apply as long as the Expectation's tags are a subset of the Result's | |
| // tags. | |
| tagsApply := r.Tags.ContainsAll(e.Tags) | |
| queryApplies := e.AppliesToTest(r.Query.ExpectationFileString()) | |
| return tagsApply && queryApplies | |
| } | |
| // AppliesToTest returns whether the Expectation applies to the test |name|. | |
| // This does NOT take into account the tags contained within the Expectation, | |
| // only whether the name matches. | |
| func (e *Expectation) AppliesToTest(name string) bool { | |
| // The query is a glob expectation, we need to perform a more complex | |
| // comparison. Otherwise, we can just check for an exact match. | |
| if e.IsGlobExpectation() { | |
| e.ensureGlobMatcherIsSet() | |
| return e.globMatcher.Matchcase(name) | |
| } else { | |
| return e.Query == name | |
| } | |
| } | |
| // AsExpectationFileString returns the human-readable form of the expectation | |
| // that matches the syntax of the expectation files. | |
| func (e *Expectation) AsExpectationFileString() string { | |
| parts := []string{} | |
| if e.Bug != "" { | |
| parts = append(parts, e.Bug) | |
| } | |
| if len(e.Tags) > 0 { | |
| parts = append(parts, fmt.Sprintf("[ %v ]", strings.Join(e.Tags.List(), " "))) | |
| } | |
| parts = append(parts, e.Query) | |
| parts = append(parts, fmt.Sprintf("[ %v ]", strings.Join(e.Status, " "))) | |
| if e.Comment != "" { | |
| parts = append(parts, e.Comment) | |
| } | |
| return strings.Join(parts, " ") | |
| } | |
| // Clone makes a deep-copy of the Expectation. | |
| func (e *Expectation) Clone() Expectation { | |
| out := Expectation{ | |
| Line: e.Line, | |
| Bug: e.Bug, | |
| Query: e.Query, | |
| Comment: e.Comment, | |
| } | |
| if e.Tags != nil { | |
| out.Tags = e.Tags.Clone() | |
| } | |
| if e.Status != nil { | |
| out.Status = append([]string{}, e.Status...) | |
| } | |
| return out | |
| } | |
| // Compare compares the relative order of a and b, returning: | |
| // | |
| // -1 if a should come before b | |
| // 1 if a should come after b | |
| // 0 if a and b are identical | |
| // | |
| // Note: Only comparing bug, tags, and query (in that order). | |
| func (e *Expectation) Compare(b Expectation) int { | |
| switch strings.Compare(e.Bug, b.Bug) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| switch strings.Compare(result.TagsToString(e.Tags), result.TagsToString(b.Tags)) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| switch strings.Compare(e.Query, b.Query) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| return 0 | |
| } | |
| // ComparePrioritizeQuery is the same as Compare, but compares in the following | |
| // order: query, tags, bug. | |
| func (e *Expectation) ComparePrioritizeQuery(other Expectation) int { | |
| switch strings.Compare(e.Query, other.Query) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| switch strings.Compare(result.TagsToString(e.Tags), result.TagsToString(other.Tags)) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| switch strings.Compare(e.Bug, other.Bug) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| return 0 | |
| } | |
| // Sort sorts the expectations in-place | |
| func (e Expectations) Sort() { | |
| sort.Slice(e, func(i, j int) bool { return e[i].Compare(e[j]) < 0 }) | |
| } | |
| // SortPrioritizeQuery sorts the expectations in-place, prioritizing the query for | |
| // sorting order. | |
| func (e Expectations) SortPrioritizeQuery() { | |
| sort.Slice(e, func(i, j int) bool { return e[i].ComparePrioritizeQuery(e[j]) < 0 }) | |
| } |