| // 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 cov | |
| import ( | |
| "fmt" | |
| "sort" | |
| "strings" | |
| ) | |
| type treeFile struct { | |
| tcm TestCoverageMap | |
| spangroups map[SpanGroupID]SpanGroup | |
| allSpans SpanList | |
| } | |
| func newTreeFile() *treeFile { | |
| return &treeFile{ | |
| tcm: TestCoverageMap{}, | |
| spangroups: map[SpanGroupID]SpanGroup{}, | |
| } | |
| } | |
| // Tree represents source code coverage across a tree of different processes. | |
| // Each tree node is addressed by a Path. | |
| type Tree struct { | |
| initialized bool | |
| strings Strings | |
| spans map[Span]SpanID | |
| testRoot Test | |
| files map[string]*treeFile | |
| } | |
| func (t *Tree) init() { | |
| if !t.initialized { | |
| t.strings.m = map[string]StringID{} | |
| t.spans = map[Span]SpanID{} | |
| t.testRoot = newTest() | |
| t.files = map[string]*treeFile{} | |
| t.initialized = true | |
| } | |
| } | |
| // Spans returns all the spans used by the tree | |
| func (t *Tree) Spans() SpanList { | |
| out := make(SpanList, len(t.spans)) | |
| for span, id := range t.spans { | |
| out[id] = span | |
| } | |
| return out | |
| } | |
| // FileSpanGroups returns all the span groups for the given file | |
| func (t *Tree) FileSpanGroups(path string) map[SpanGroupID]SpanGroup { | |
| return t.files[path].spangroups | |
| } | |
| // FileCoverage returns the TestCoverageMap for the given file | |
| func (t *Tree) FileCoverage(path string) TestCoverageMap { | |
| return t.files[path].tcm | |
| } | |
| // Tests returns the root test | |
| func (t *Tree) Tests() *Test { return &t.testRoot } | |
| // Strings returns the string table | |
| func (t *Tree) Strings() Strings { return t.strings } | |
| func (t *Tree) index(path Path) []indexedTest { | |
| out := make([]indexedTest, len(path)) | |
| test := &t.testRoot | |
| for i, p := range path { | |
| name := t.strings.index(p) | |
| test, out[i] = test.index(name) | |
| } | |
| return out | |
| } | |
| func (t *Tree) addSpans(spans SpanList) SpanSet { | |
| out := make(SpanSet, len(spans)) | |
| for _, s := range spans { | |
| id, ok := t.spans[s] | |
| if !ok { | |
| id = SpanID(len(t.spans)) | |
| t.spans[s] = id | |
| } | |
| out[id] = struct{}{} | |
| } | |
| return out | |
| } | |
| // Add adds the coverage information cov to the tree node addressed by path. | |
| func (t *Tree) Add(path Path, cov *Coverage) { | |
| t.init() | |
| tests := t.index(path) | |
| nextFile: | |
| // For each file with coverage... | |
| for _, file := range cov.Files { | |
| // Lookup or create the file's test coverage map | |
| tf, ok := t.files[file.Path] | |
| if !ok { | |
| tf = newTreeFile() | |
| t.files[file.Path] = tf | |
| } | |
| for _, span := range file.Covered { | |
| tf.allSpans.Add(span) | |
| } | |
| for _, span := range file.Uncovered { | |
| tf.allSpans.Add(span) | |
| } | |
| // Add all the spans to the map, get the span ids | |
| spans := t.addSpans(file.Covered) | |
| // Starting from the test root, walk down the test tree. | |
| tcm, test := tf.tcm, t.testRoot | |
| parent := (*TestCoverage)(nil) | |
| for _, indexedTest := range tests { | |
| if indexedTest.created { | |
| if parent != nil && len(test.children) == 1 { | |
| parent.Spans = parent.Spans.addAll(spans) | |
| delete(parent.Children, indexedTest.index) | |
| } else { | |
| tc := tcm.index(indexedTest.index) | |
| tc.Spans = spans | |
| } | |
| continue nextFile | |
| } | |
| test = test.children[indexedTest.index] | |
| tc := tcm.index(indexedTest.index) | |
| // If the tree node contains spans that are not in this new test, | |
| // we need to push those spans down to all the other children. | |
| if lower := tc.Spans.removeAll(spans); len(lower) > 0 { | |
| // push into each child node | |
| for i := range test.children { | |
| child := tc.Children.index(TestIndex(i)) | |
| child.Spans = child.Spans.addAll(lower) | |
| } | |
| // remove from node | |
| tc.Spans = tc.Spans.removeAll(lower) | |
| } | |
| // The spans that are in the new test, but are not part of the tree | |
| // node carry propagating down. | |
| spans = spans.removeAll(tc.Spans) | |
| if len(spans) == 0 { | |
| continue nextFile | |
| } | |
| tcm = tc.Children | |
| parent = tc | |
| } | |
| } | |
| } | |
| // allSpans returns all the spans in use by the TestCoverageMap and its children. | |
| func (t *Tree) allSpans(tf *treeFile, tcm TestCoverageMap) SpanSet { | |
| spans := SpanSet{} | |
| for _, tc := range tcm { | |
| for id := tc.Group; id != nil; id = tf.spangroups[*id].Extend { | |
| group := tf.spangroups[*id] | |
| spans = spans.addAll(group.Spans) | |
| } | |
| spans = spans.addAll(tc.Spans) | |
| spans = spans.addAll(spans.invertAll(t.allSpans(tf, tc.Children))) | |
| } | |
| return spans | |
| } | |
| // StringID is an identifier of a string | |
| type StringID int | |
| // Strings holds a map of string to identifier | |
| type Strings struct { | |
| m map[string]StringID | |
| s []string | |
| } | |
| func (s *Strings) index(str string) StringID { | |
| i, ok := s.m[str] | |
| if !ok { | |
| i = StringID(len(s.s)) | |
| s.s = append(s.s, str) | |
| s.m[str] = i | |
| } | |
| return i | |
| } | |
| // TestIndex is an child test index | |
| type TestIndex int | |
| // Test is an collection of named sub-tests | |
| type Test struct { | |
| indices map[StringID]TestIndex | |
| children []Test | |
| } | |
| func newTest() Test { | |
| return Test{ | |
| indices: map[StringID]TestIndex{}, | |
| } | |
| } | |
| type indexedTest struct { | |
| index TestIndex | |
| created bool | |
| } | |
| func (t *Test) index(name StringID) (*Test, indexedTest) { | |
| idx, ok := t.indices[name] | |
| if !ok { | |
| idx = TestIndex(len(t.children)) | |
| t.children = append(t.children, newTest()) | |
| t.indices[name] = idx | |
| } | |
| return &t.children[idx], indexedTest{idx, !ok} | |
| } | |
| type namedIndex struct { | |
| name string | |
| idx TestIndex | |
| } | |
| func (t Test) byName(s Strings) []namedIndex { | |
| out := make([]namedIndex, len(t.children)) | |
| for id, idx := range t.indices { | |
| out[idx] = namedIndex{s.s[id], idx} | |
| } | |
| sort.Slice(out, func(i, j int) bool { return out[i].name < out[j].name }) | |
| return out | |
| } | |
| func (t Test) String(s Strings) string { | |
| sb := strings.Builder{} | |
| for i, n := range t.byName(s) { | |
| child := t.children[n.idx] | |
| if i > 0 { | |
| sb.WriteString(" ") | |
| } | |
| sb.WriteString(n.name) | |
| if len(child.children) > 0 { | |
| sb.WriteString(fmt.Sprintf(":%v", child.String(s))) | |
| } | |
| } | |
| return "{" + sb.String() + "}" | |
| } | |
| // TestCoverage holds the coverage information for a deqp test group / leaf. | |
| // For example: | |
| // The deqp test group may hold spans that are common for all children, and may | |
| // also optionally hold child nodes that describe coverage that differs per | |
| // child test. | |
| type TestCoverage struct { | |
| Spans SpanSet | |
| Group *SpanGroupID | |
| Children TestCoverageMap | |
| } | |
| func (tc TestCoverage) String(t *Test, s Strings) string { | |
| sb := strings.Builder{} | |
| sb.WriteString("{") | |
| if len(tc.Spans) > 0 { | |
| sb.WriteString(tc.Spans.String()) | |
| } | |
| if tc.Group != nil { | |
| sb.WriteString(" <") | |
| sb.WriteString(fmt.Sprintf("%v", *tc.Group)) | |
| sb.WriteString(">") | |
| } | |
| if len(tc.Children) > 0 { | |
| sb.WriteString(" ") | |
| sb.WriteString(tc.Children.String(t, s)) | |
| } | |
| sb.WriteString("}") | |
| return sb.String() | |
| } | |
| // deletable returns true if the TestCoverage provides no data. | |
| func (tc TestCoverage) deletable() bool { | |
| return len(tc.Spans) == 0 && tc.Group == nil && len(tc.Children) == 0 | |
| } | |
| // TestCoverageMap is a map of TestIndex to *TestCoverage. | |
| type TestCoverageMap map[TestIndex]*TestCoverage | |
| // traverse performs a depth first traversal of the TestCoverage tree. | |
| func (tcm TestCoverageMap) traverse(cb func(*TestCoverage)) { | |
| for _, tc := range tcm { | |
| cb(tc) | |
| tc.Children.traverse(cb) | |
| } | |
| } | |
| func (tcm TestCoverageMap) String(t *Test, s Strings) string { | |
| sb := strings.Builder{} | |
| for _, n := range t.byName(s) { | |
| if child, ok := tcm[n.idx]; ok { | |
| sb.WriteString(fmt.Sprintf("\n%v: %v", n.name, child.String(&t.children[n.idx], s))) | |
| } | |
| } | |
| if sb.Len() > 0 { | |
| sb.WriteString("\n") | |
| } | |
| return indent(sb.String()) | |
| } | |
| func newTestCoverage() *TestCoverage { | |
| return &TestCoverage{ | |
| Children: TestCoverageMap{}, | |
| Spans: SpanSet{}, | |
| } | |
| } | |
| func (tcm TestCoverageMap) index(idx TestIndex) *TestCoverage { | |
| tc, ok := tcm[idx] | |
| if !ok { | |
| tc = newTestCoverage() | |
| tcm[idx] = tc | |
| } | |
| return tc | |
| } | |
| // SpanID is an identifier of a span in a Tree. | |
| type SpanID int | |
| // SpanSet is a set of SpanIDs. | |
| type SpanSet map[SpanID]struct{} | |
| // SpanIDList is a list of SpanIDs | |
| type SpanIDList []SpanID | |
| // Compare returns -1 if l comes before o, 1 if l comes after o, otherwise 0. | |
| func (l SpanIDList) Compare(o SpanIDList) int { | |
| switch { | |
| case len(l) < len(o): | |
| return -1 | |
| case len(l) > len(o): | |
| return 1 | |
| } | |
| for i, a := range l { | |
| b := o[i] | |
| switch { | |
| case a < b: | |
| return -1 | |
| case a > b: | |
| return 1 | |
| } | |
| } | |
| return 0 | |
| } | |
| // List returns the full list of sorted span ids. | |
| func (s SpanSet) List() SpanIDList { | |
| out := make(SpanIDList, 0, len(s)) | |
| for span := range s { | |
| out = append(out, span) | |
| } | |
| sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) | |
| return out | |
| } | |
| func (s SpanSet) String() string { | |
| sb := strings.Builder{} | |
| sb.WriteString(`[`) | |
| l := s.List() | |
| for i, span := range l { | |
| if i > 0 { | |
| sb.WriteString(`, `) | |
| } | |
| sb.WriteString(fmt.Sprintf("%v", span)) | |
| } | |
| sb.WriteString(`]`) | |
| return sb.String() | |
| } | |
| func (s SpanSet) contains(rhs SpanID) bool { | |
| _, found := s[rhs] | |
| return found | |
| } | |
| func (s SpanSet) containsAll(rhs SpanSet) bool { | |
| for span := range rhs { | |
| if !s.contains(span) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func (s SpanSet) remove(rhs SpanID) SpanSet { | |
| out := make(SpanSet, len(s)) | |
| for span := range s { | |
| if span != rhs { | |
| out[span] = struct{}{} | |
| } | |
| } | |
| return out | |
| } | |
| func (s SpanSet) removeAll(rhs SpanSet) SpanSet { | |
| out := make(SpanSet, len(s)) | |
| for span := range s { | |
| if _, found := rhs[span]; !found { | |
| out[span] = struct{}{} | |
| } | |
| } | |
| return out | |
| } | |
| func (s SpanSet) add(rhs SpanID) SpanSet { | |
| out := make(SpanSet, len(s)+1) | |
| for span := range s { | |
| out[span] = struct{}{} | |
| } | |
| out[rhs] = struct{}{} | |
| return out | |
| } | |
| func (s SpanSet) addAll(rhs SpanSet) SpanSet { | |
| out := make(SpanSet, len(s)+len(rhs)) | |
| for span := range s { | |
| out[span] = struct{}{} | |
| } | |
| for span := range rhs { | |
| out[span] = struct{}{} | |
| } | |
| return out | |
| } | |
| func (s SpanSet) invert(rhs SpanID) SpanSet { | |
| if s.contains(rhs) { | |
| return s.remove(rhs) | |
| } | |
| return s.add(rhs) | |
| } | |
| func (s SpanSet) invertAll(rhs SpanSet) SpanSet { | |
| out := make(SpanSet, len(s)+len(rhs)) | |
| for span := range s { | |
| if !rhs.contains(span) { | |
| out[span] = struct{}{} | |
| } | |
| } | |
| for span := range rhs { | |
| if !s.contains(span) { | |
| out[span] = struct{}{} | |
| } | |
| } | |
| return out | |
| } | |
| // SpanGroupID is an identifier of a SpanGroup. | |
| type SpanGroupID int | |
| // SpanGroup holds a number of spans, potentially extending from another SpanGroup. | |
| type SpanGroup struct { | |
| Spans SpanSet | |
| Extend *SpanGroupID | |
| } | |
| func newSpanGroup() SpanGroup { | |
| return SpanGroup{Spans: SpanSet{}} | |
| } | |
| func indent(s string) string { | |
| return strings.TrimSuffix(strings.ReplaceAll(s, "\n", "\n "), " ") | |
| } |