| // 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 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" |
| "io/ioutil" |
| "os" |
| "sort" |
| "strings" |
| |
| "dawn.googlesource.com/dawn/tools/src/cts/result" |
| ) |
| |
| // 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 |
| } |
| |
| // Tags holds the tag information parsed in the comments between the |
| // 'BEGIN TAG HEADER' and 'END TAG HEADER' markers. |
| // Tags are grouped in tag-sets. |
| type Tags struct { |
| // Map of tag-set name to tags |
| Sets []TagSet |
| // Map of tag name to tag-set and priority |
| ByName map[string]TagSetAndPriority |
| } |
| |
| // TagSet is a named collection of tags, parsed from the 'TAG HEADER' |
| type TagSet struct { |
| Name string // Name of the tag-set |
| Tags result.Tags // Tags belonging to the tag-set |
| } |
| |
| // TagSetAndPriority is used by the Tags.ByName map to identify which tag-set |
| // a tag belongs to. |
| type TagSetAndPriority struct { |
| // The tag-set that the tag belongs to. |
| Set string |
| // The declared order of tag in the set. |
| // An expectation may only list a single tag from any set. This priority |
| // is used to decide which tag(s) should be dropped when multiple tags are |
| // found in the same set. |
| Priority int |
| } |
| |
| // 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 |
| } |
| |
| // Expectations are a list of Expectation |
| type Expectations []Expectation |
| |
| // Load loads the expectation file at 'path', returning a Content. |
| func Load(path string) (Content, error) { |
| content, err := ioutil.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) error { |
| f, err := os.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 |
| } |
| |
| // EndsInBlankLine returns true if the Content ends with a blank line |
| func (c Content) EndsInBlankLine() bool { |
| return !c.Empty() && c.Chunks[len(c.Chunks)-1].IsBlankLine() |
| } |
| |
| // MaybeAddBlankLine appends a new blank line to the content, if the content |
| // does not already end in a blank line. |
| func (c *Content) MaybeAddBlankLine() { |
| if !c.Empty() && !c.EndsInBlankLine() { |
| c.Chunks = append(c.Chunks, Chunk{}) |
| } |
| } |
| |
| // Write writes the Content, in textual form, to the writer w. |
| func (c Content) Write(w io.Writer) error { |
| for _, chunk := range c.Chunks { |
| if len(chunk.Comments) == 0 && len(chunk.Expectations) == 0 { |
| if _, err := fmt.Fprintln(w); err != nil { |
| return err |
| } |
| continue |
| } |
| for _, comment := range chunk.Comments { |
| if _, err := fmt.Fprintln(w, comment); err != nil { |
| return err |
| } |
| } |
| for _, expectation := range chunk.Expectations { |
| parts := []string{} |
| if expectation.Bug != "" { |
| parts = append(parts, expectation.Bug) |
| } |
| if len(expectation.Tags) > 0 { |
| parts = append(parts, fmt.Sprintf("[ %v ]", strings.Join(expectation.Tags.List(), " "))) |
| } |
| parts = append(parts, expectation.Query) |
| parts = append(parts, fmt.Sprintf("[ %v ]", strings.Join(expectation.Status, " "))) |
| if expectation.Comment != "" { |
| parts = append(parts, expectation.Comment) |
| } |
| if _, err := fmt.Fprintln(w, strings.Join(parts, " ")); 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() |
| } |
| |
| // 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 |
| } |
| |
| // IsBlankLine returns true if the Chunk has no comments or expectations. |
| func (c Chunk) IsBlankLine() 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)) |
| for i, c := range c.Comments { |
| comments[i] = c |
| } |
| expectations := make([]Expectation, len(c.Expectations)) |
| for i, e := range c.Expectations { |
| expectations[i] = e.Clone() |
| } |
| return Chunk{comments, expectations} |
| } |
| |
| // Clone returns a deep-copy of the Tags |
| func (t Tags) Clone() Tags { |
| out := Tags{} |
| if t.ByName != nil { |
| out.ByName = make(map[string]TagSetAndPriority, len(t.ByName)) |
| for n, t := range t.ByName { |
| out.ByName[n] = t |
| } |
| } |
| if t.Sets != nil { |
| out.Sets = make([]TagSet, len(t.Sets)) |
| copy(out.Sets, t.Sets) |
| } |
| return out |
| } |
| |
| // 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, query, and tags (in that order). |
| func (a Expectation) Compare(b Expectation) int { |
| switch strings.Compare(a.Bug, b.Bug) { |
| case -1: |
| return -1 |
| case 1: |
| return 1 |
| } |
| switch strings.Compare(a.Query, b.Query) { |
| case -1: |
| return -1 |
| case 1: |
| return 1 |
| } |
| aTag := result.TagsToString(a.Tags) |
| bTag := result.TagsToString(b.Tags) |
| switch strings.Compare(aTag, bTag) { |
| case -1: |
| return -1 |
| case 1: |
| return 1 |
| } |
| return 0 |
| } |
| |
| func (l Expectations) Sort() { |
| sort.Slice(l, func(i, j int) bool { return l[i].Compare(l[j]) < 0 }) |
| } |