| // 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 result holds types that describe CTS test results. | |
| package result | |
| import ( | |
| "bufio" | |
| "fmt" | |
| "io" | |
| "path/filepath" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/container" | |
| "dawn.googlesource.com/dawn/tools/src/cts/query" | |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" | |
| ) | |
| // Result holds the result of a CTS test | |
| type Result struct { | |
| Query query.Query | |
| Tags Tags | |
| Status Status | |
| Duration time.Duration | |
| // If true, this result may be exonerated if there are other | |
| // results with the same query and tags that have MayExonerate: false | |
| MayExonerate bool | |
| } | |
| // Format writes the Result to the fmt.State | |
| // The Result is printed as a single line, in the form: | |
| // | |
| // <query> <tags> <status> | |
| // | |
| // This matches the order in which results are sorted. | |
| func (r Result) Format(f fmt.State, verb rune) { | |
| if len(r.Tags) > 0 { | |
| fmt.Fprintf(f, "%v %v %v %v %v", r.Query, TagsToString(r.Tags), r.Status, r.Duration, r.MayExonerate) | |
| } else { | |
| fmt.Fprintf(f, "%v %v %v %v", r.Query, r.Status, r.Duration, r.MayExonerate) | |
| } | |
| } | |
| // String returns the result as a string | |
| func (r Result) String() string { | |
| sb := strings.Builder{} | |
| fmt.Fprint(&sb, r) | |
| return sb.String() | |
| } | |
| // Compare compares the relative order of r and o, returning: | |
| // | |
| // -1 if r should come before o | |
| // 1 if r should come after o | |
| // 0 if r and o are identical | |
| // | |
| // Note: Result.Duration is not considered in comparison. | |
| func (r Result) Compare(o Result) int { | |
| a, b := r, o | |
| switch a.Query.Compare(b.Query) { | |
| case -1: | |
| return -1 | |
| case 1: | |
| return 1 | |
| } | |
| ta := strings.Join(a.Tags.List(), TagDelimiter) | |
| tb := strings.Join(b.Tags.List(), TagDelimiter) | |
| switch { | |
| case ta < tb: | |
| return -1 | |
| case ta > tb: | |
| return 1 | |
| case a.Status < b.Status: | |
| return -1 | |
| case a.Status > b.Status: | |
| return 1 | |
| } | |
| return 0 | |
| } | |
| // Parse parses the result from a string of the form: | |
| // | |
| // <query> <tags> <status> | |
| // | |
| // <tags> may be omitted if there were no tags. | |
| // | |
| // Tests are separated into sections where the section name | |
| // appears at the end of the list as just a line with | |
| // | |
| // <section-name> | |
| func Parse(in string) (ExecutionMode, Result, error) { | |
| line := in | |
| token := func() string { | |
| for i, c := range line { | |
| if c != ' ' { | |
| line = line[i:] | |
| break | |
| } | |
| } | |
| for i, c := range line { | |
| if c == ' ' { | |
| tok := line[:i] | |
| line = line[i:] | |
| return tok | |
| } | |
| } | |
| tok := line | |
| line = "" | |
| return tok | |
| } | |
| a := token() | |
| b := token() | |
| c := token() | |
| d := token() | |
| e := token() | |
| if a != "" && b == "" && token() == "" { | |
| return ExecutionMode(a), Result{}, nil | |
| } | |
| if a == "" || b == "" || c == "" || d == "" || token() != "" { | |
| return "", Result{}, fmt.Errorf("unable to parse result '%v'", in) | |
| } | |
| query := query.Parse(a) | |
| if e == "" { | |
| status := Status(b) | |
| duration, err := time.ParseDuration(c) | |
| if err != nil { | |
| return "", Result{}, fmt.Errorf("unable to parse result '%v': %w", in, err) | |
| } | |
| mayExonerate, err := strconv.ParseBool(d) | |
| if err != nil { | |
| return "", Result{}, fmt.Errorf("unable to parse result '%v': %w", in, err) | |
| } | |
| return "", Result{query, nil, status, duration, mayExonerate}, nil | |
| } else { | |
| tags := StringToTags(b) | |
| status := Status(c) | |
| duration, err := time.ParseDuration(d) | |
| if err != nil { | |
| return "", Result{}, fmt.Errorf("unable to parse result '%v': %w", in, err) | |
| } | |
| mayExonerate, err := strconv.ParseBool(e) | |
| if err != nil { | |
| return "", Result{}, fmt.Errorf("unable to parse result '%v': %w", in, err) | |
| } | |
| return "", Result{query, tags, status, duration, mayExonerate}, nil | |
| } | |
| } | |
| // List is a list of results | |
| type List []Result | |
| // The mode the tests were run in, "core" or" "compat" | |
| type ExecutionMode string | |
| // Lists of test results by execution mode. | |
| type ResultsByExecutionMode map[ExecutionMode]List | |
| // TransformTags returns the list of results with the tags transformed using f. | |
| // TransformTags assumes that f will return the same output for the same input. | |
| func (l List) TransformTags(f func(Tags) Tags) List { | |
| cache := map[string]Tags{} | |
| out := List{} | |
| for _, r := range l { | |
| key := TagsToString(r.Tags) | |
| tags, cached := cache[key] | |
| if !cached { | |
| tags = f(r.Tags.Clone()) | |
| cache[key] = tags | |
| } | |
| out = append(out, Result{ | |
| Query: r.Query, | |
| Tags: tags, | |
| Status: r.Status, | |
| Duration: r.Duration, | |
| MayExonerate: r.MayExonerate, | |
| }) | |
| } | |
| return out | |
| } | |
| // ReplaceDuplicates returns a new list with duplicate test results replaced. | |
| // When a duplicate is found, the function f is called with the duplicate | |
| // results. The returned status will be used as the replaced result. | |
| // Merged results will use the average (mean) duration of the duplicates. | |
| func (l List) ReplaceDuplicates(f func(Statuses) Status) List { | |
| type key struct { | |
| query query.Query | |
| tags string | |
| } | |
| // Collect all duplicates | |
| keyToIndices := map[key][]int{} // key to index | |
| for i, r := range l { | |
| k := key{r.Query, TagsToString(r.Tags)} | |
| keyToIndices[k] = append(keyToIndices[k], i) | |
| } | |
| // Filter out exonerated results | |
| for key, indices := range keyToIndices { | |
| keptIndices := []int{} | |
| for _, i := range indices { | |
| // Copy all indices which are not exonerated into keptIndices. | |
| if !l[i].MayExonerate { | |
| keptIndices = append(keptIndices, i) | |
| } | |
| } | |
| // Change indices to only the kept ones. If keptIndices is empty, | |
| // then all results were marked with may_exonerate, and we keep all | |
| // of them. | |
| if len(keptIndices) > 0 { | |
| keyToIndices[key] = keptIndices | |
| } | |
| } | |
| // Resolve duplicates | |
| type StatusAndDuration struct { | |
| Status Status | |
| Duration time.Duration | |
| } | |
| merged := map[key]StatusAndDuration{} | |
| for key, indices := range keyToIndices { | |
| statuses := NewStatuses() | |
| duration := time.Duration(0) | |
| for _, i := range indices { | |
| r := l[i] | |
| statuses.Add(r.Status) | |
| duration += r.Duration | |
| } | |
| status := func() Status { | |
| if len(statuses) > 1 { | |
| return f(statuses) | |
| } | |
| return statuses.One() | |
| }() | |
| duration = duration / time.Duration(len(indices)) | |
| merged[key] = StatusAndDuration{ | |
| Status: status, | |
| Duration: duration, | |
| } | |
| } | |
| // Rebuild list | |
| out := make(List, 0, len(keyToIndices)) | |
| for _, r := range l { | |
| k := key{r.Query, TagsToString(r.Tags)} | |
| if sd, ok := merged[k]; ok { | |
| out = append(out, Result{ | |
| Query: r.Query, | |
| Tags: r.Tags, | |
| Status: sd.Status, | |
| Duration: sd.Duration, | |
| MayExonerate: l[keyToIndices[k][0]].MayExonerate, | |
| }) | |
| delete(merged, k) // Remove from map to prevent duplicates | |
| } | |
| } | |
| return out | |
| } | |
| // Sort sorts the list | |
| func (l List) Sort() { | |
| sort.Slice(l, func(i, j int) bool { return l[i].Compare(l[j]) < 0 }) | |
| } | |
| // Filter returns the results that match the given predicate | |
| func (l List) Filter(f func(Result) bool) List { | |
| out := make(List, 0, len(l)) | |
| for _, r := range l { | |
| if f(r) { | |
| out = append(out, r) | |
| } | |
| } | |
| return out | |
| } | |
| // FilterByStatus returns the results that the given status | |
| func (l List) FilterByStatus(status Status) List { | |
| return l.Filter(func(r Result) bool { | |
| return r.Status == status | |
| }) | |
| } | |
| // FilterByTags returns the results that have all the given tags | |
| func (l List) FilterByTags(tags Tags) List { | |
| return l.Filter(func(r Result) bool { | |
| return r.Tags.ContainsAll(tags) | |
| }) | |
| } | |
| // FilterByQuery returns the results that match the given query | |
| func (l List) FilterByQuery(q query.Query) List { | |
| return l.Filter(func(r Result) bool { | |
| return q.Contains(r.Query) | |
| }) | |
| } | |
| // Statuses is a set of Status | |
| type Statuses = container.Set[Status] | |
| // NewStatuses returns a new status set with the provided statuses | |
| func NewStatuses(s ...Status) Statuses { return container.NewSet(s...) } | |
| // Statuses returns a set of all the statuses in the list | |
| func (l List) Statuses() Statuses { | |
| set := NewStatuses() | |
| for _, r := range l { | |
| set.Add(r.Status) | |
| } | |
| return set | |
| } | |
| // Load loads the result list from the file with the given path | |
| func Load(path string, fsReader oswrapper.FilesystemReader) (ResultsByExecutionMode, error) { | |
| file, err := fsReader.Open(path) | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer file.Close() | |
| results, err := Read(file) | |
| if err != nil { | |
| return nil, fmt.Errorf("while reading '%v': %w", path, err) | |
| } | |
| return results, nil | |
| } | |
| // Save saves the result list to the file with the given path | |
| func Save(path string, results ResultsByExecutionMode, fsWriter oswrapper.FilesystemWriter) error { | |
| dir := filepath.Dir(path) | |
| if err := fsWriter.MkdirAll(dir, 0777); err != nil { | |
| return err | |
| } | |
| file, err := fsWriter.Create(path) | |
| if err != nil { | |
| return err | |
| } | |
| defer file.Close() | |
| return Write(file, results) | |
| } | |
| // Read reads a result list from the given reader | |
| func Read(r io.Reader) (ResultsByExecutionMode, error) { | |
| scanner := bufio.NewScanner(r) | |
| results := ResultsByExecutionMode{} | |
| l := List{} | |
| for scanner.Scan() { | |
| section, r, err := Parse(scanner.Text()) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if section != "" { | |
| results[section] = l | |
| l = List{} | |
| } else { | |
| l = append(l, r) | |
| } | |
| } | |
| return results, nil | |
| } | |
| // Write writes a result list to the given writer | |
| func Write(w io.Writer, r ResultsByExecutionMode) error { | |
| for name, l := range r { | |
| for _, r := range l { | |
| if _, err := fmt.Fprintln(w, r); err != nil { | |
| return err | |
| } | |
| } | |
| if _, err := fmt.Fprintln(w, name); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // Merge merges and sorts multiple results lists. | |
| // Duplicates are removed using the Deduplicate() function. | |
| func Merge(lists ...List) List { | |
| n := 0 | |
| for _, l := range lists { | |
| n += len(l) | |
| } | |
| merged := make(List, 0, n) | |
| for _, l := range lists { | |
| merged = append(merged, l...) | |
| } | |
| out := merged.ReplaceDuplicates(Deduplicate) | |
| out.Sort() | |
| return out | |
| } | |
| // Deduplicate is the standard algorithm used to de-duplicating mixed results. | |
| // This function is expected to be handed to List.ReplaceDuplicates(). | |
| func Deduplicate(s Statuses) Status { | |
| // If all results have the same status, then use that | |
| if len(s) == 1 { | |
| return s.One() | |
| } | |
| // Mixed statuses. Replace with something appropriate. | |
| switch { | |
| // Crash + * = Crash | |
| case s.Contains(Crash): | |
| return Crash | |
| // Abort + * = Abort | |
| case s.Contains(Abort): | |
| return Abort | |
| // Unknown + * = Unknown | |
| case s.Contains(Unknown): | |
| return Unknown | |
| // RetryOnFailure + ~(Crash | Abort | Unknown) = RetryOnFailure | |
| case s.Contains(RetryOnFailure): | |
| return RetryOnFailure | |
| // Pass + ~(Crash | Abort | Unknown | RetryOnFailure | Slow) = RetryOnFailure | |
| case s.Contains(Pass): | |
| return RetryOnFailure | |
| } | |
| return Unknown | |
| } |