Regres: Split coverage.go into multiple files
This was getting unruly.
Bug: b/152192800
Change-Id: I41a393e0548367ea4d69c3982b89a6ffe198e9f7
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/43308
Kokoro-Presubmit: kokoro <noreply+kokoro@google.com>
Reviewed-by: Nicolas Capens <nicolascapens@google.com>
Tested-by: Ben Clayton <bclayton@google.com>
diff --git a/tests/regres/cov/cov.go b/tests/regres/cov/cov.go
new file mode 100644
index 0000000..67ba4a2
--- /dev/null
+++ b/tests/regres/cov/cov.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// 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 cov provides functions for consuming and combining llvm coverage
+// information from multiple processes.
+package cov
diff --git a/tests/regres/cov/coverage.go b/tests/regres/cov/coverage.go
deleted file mode 100644
index f34ac67..0000000
--- a/tests/regres/cov/coverage.go
+++ /dev/null
@@ -1,1199 +0,0 @@
-// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
-//
-// 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 cov provides functions for consuming and combining llvm coverage
-// information from multiple processes.
-package cov
-
-import (
- "bufio"
- "bytes"
- "compress/zlib"
- "encoding/binary"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "runtime/debug"
- "sort"
- "strconv"
- "strings"
- "sync"
-
- "../cause"
- "../llvm"
-)
-
-// Location describes a single line-column position in a source file.
-type Location struct {
- Line, Column int
-}
-
-func (l Location) String() string {
- return fmt.Sprintf("%v:%v", l.Line, l.Column)
-}
-
-// Span describes a start and end interval in a source file.
-type Span struct {
- Start, End Location
-}
-
-func (l Span) String() string {
- return fmt.Sprintf("%v-%v", l.Start, l.End)
-}
-
-// File describes the coverage spans in a single source file.
-type File struct {
- Path string
- Spans []Span
-}
-
-// Coverage describes the coverage spans for all the source files for a single
-// process invocation.
-type Coverage struct {
- Files []File
-}
-
-// Env holds the enviroment settings for performing coverage processing.
-type Env struct {
- LLVM llvm.Toolchain
- RootDir string // path to SwiftShader git root directory
- ExePath string // path to the executable binary
- TurboCov string // path to turbo-cov (optional)
-}
-
-// AppendRuntimeEnv returns the environment variables env with the
-// LLVM_PROFILE_FILE environment variable appended.
-func AppendRuntimeEnv(env []string, coverageFile string) []string {
- return append(env, "LLVM_PROFILE_FILE="+coverageFile)
-}
-
-// Import uses the llvm-profdata and llvm-cov tools to import the coverage
-// information from a .profraw file.
-func (e Env) Import(profrawPath string) (*Coverage, error) {
- profdata := profrawPath + ".profdata"
-
- if err := exec.Command(e.LLVM.Profdata(), "merge", "-sparse", profrawPath, "-output", profdata).Run(); err != nil {
- return nil, cause.Wrap(err, "llvm-profdata errored")
- }
- defer os.Remove(profdata)
-
- if e.TurboCov == "" {
- args := []string{
- "export",
- e.ExePath,
- "-instr-profile=" + profdata,
- "-format=text",
- }
- if e.LLVM.Version.GreaterEqual(llvm.Version{Major: 9}) {
- // LLVM 9 has new flags that omit stuff we don't care about.
- args = append(args,
- "-skip-expansions",
- "-skip-functions",
- )
- }
-
- data, err := exec.Command(e.LLVM.Cov(), args...).Output()
- if err != nil {
- return nil, cause.Wrap(err, "llvm-cov errored: %v", string(err.(*exec.ExitError).Stderr))
- }
- cov, err := e.parseCov(data)
- if err != nil {
- return nil, cause.Wrap(err, "Couldn't parse coverage json data")
- }
- return cov, nil
- }
-
- data, err := exec.Command(e.TurboCov, e.ExePath, profdata).Output()
- if err != nil {
- return nil, cause.Wrap(err, "turbo-cov errored: %v", string(err.(*exec.ExitError).Stderr))
- }
- cov, err := e.parseTurboCov(data)
- if err != nil {
- return nil, cause.Wrap(err, "Couldn't process turbo-cov output")
- }
- return cov, nil
-}
-
-// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
-// https://stackoverflow.com/a/56792192
-func (e Env) parseCov(raw []byte) (*Coverage, error) {
- // line int, col int, count int64, hasCount bool, isRegionEntry bool
- type segment []interface{}
-
- type file struct {
- // expansions ignored
- Name string `json:"filename"`
- Segments []segment `json:"segments"`
- // summary ignored
- }
-
- type data struct {
- Files []file `json:"files"`
- }
-
- root := struct {
- Data []data `json:"data"`
- }{}
- err := json.NewDecoder(bytes.NewReader(raw)).Decode(&root)
- if err != nil {
- return nil, err
- }
-
- c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
- for _, f := range root.Data[0].Files {
- relpath, err := filepath.Rel(e.RootDir, f.Name)
- if err != nil {
- return nil, err
- }
- if strings.HasPrefix(relpath, "..") {
- continue
- }
- file := File{Path: relpath}
- for sIdx := 0; sIdx+1 < len(f.Segments); sIdx++ {
- start := Location{(int)(f.Segments[sIdx][0].(float64)), (int)(f.Segments[sIdx][1].(float64))}
- end := Location{(int)(f.Segments[sIdx+1][0].(float64)), (int)(f.Segments[sIdx+1][1].(float64))}
- covered := f.Segments[sIdx][2].(float64) != 0
- if covered {
- if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
- file.Spans[c-1].End = end
- } else {
- file.Spans = append(file.Spans, Span{start, end})
- }
- }
- }
- if len(file.Spans) > 0 {
- c.Files = append(c.Files, file)
- }
- }
-
- return c, nil
-}
-
-func (e Env) parseTurboCov(data []byte) (*Coverage, error) {
- u32 := func() uint32 {
- out := binary.LittleEndian.Uint32(data)
- data = data[4:]
- return out
- }
- u8 := func() uint8 {
- out := data[0]
- data = data[1:]
- return out
- }
- str := func() string {
- len := u32()
- out := data[:len]
- data = data[len:]
- return string(out)
- }
-
- numFiles := u32()
- c := &Coverage{Files: make([]File, 0, numFiles)}
- for i := 0; i < int(numFiles); i++ {
- path := str()
- relpath, err := filepath.Rel(e.RootDir, path)
- if err != nil {
- return nil, err
- }
- if strings.HasPrefix(relpath, "..") {
- continue
- }
-
- file := File{Path: relpath}
-
- type segment struct {
- location Location
- count int
- covered bool
- }
-
- numSegements := u32()
- segments := make([]segment, numSegements)
- for j := range segments {
- segment := &segments[j]
- segment.location.Line = int(u32())
- segment.location.Column = int(u32())
- segment.count = int(u32())
- segment.covered = u8() != 0
- }
-
- for sIdx := 0; sIdx+1 < len(segments); sIdx++ {
- start := segments[sIdx].location
- end := segments[sIdx+1].location
- if segments[sIdx].count > 0 {
- if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
- file.Spans[c-1].End = end
- } else {
- file.Spans = append(file.Spans, Span{start, end})
- }
- }
- }
-
- if len(file.Spans) > 0 {
- c.Files = append(c.Files, file)
- }
- }
-
- return c, nil
-}
-
-// Path is a tree node path formed from a list of strings
-type Path []string
-
-type treeFile struct {
- tcm TestCoverageMap
- spangroups map[SpanGroupID]SpanGroup
-}
-
-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() []Span {
- out := make([]Span, 0, len(t.spans))
- for span := range t.spans {
- out = append(out, span)
- }
- sort.Slice(out, func(i, j int) bool {
- if out[i].Start.Line < out[j].Start.Line {
- return true
- }
- if out[i].Start.Line > out[j].Start.Line {
- return false
- }
- return out[i].Start.Column < out[j].Start.Column
- })
- return out
-}
-
-// 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 []Span) 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
- }
-
- // Add all the spans to the map, get the span ids
- spans := t.addSpans(file.Spans)
-
- // 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.add(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.sub(spans); len(lower) > 0 {
- // push into each child node
- for i := range test.children {
- child := tc.Children.index(TestIndex(i))
- child.Spans = child.Spans.add(lower)
- }
- // remove from node
- tc.Spans = tc.Spans.sub(lower)
- }
-
- // The spans that are in the new test, but are not part of the tree
- // node carry propagating down.
- spans = spans.sub(tc.Spans)
- if len(spans) == 0 {
- continue nextFile
- }
-
- tcm = tc.Children
- parent = tc
- }
- }
-}
-
-// 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(fmt.Sprintf("{%v", tc.Spans))
- if len(tc.Children) > 0 {
- sb.WriteString(" ")
- sb.WriteString(tc.Children.String(t, s))
- }
- sb.WriteString("}")
- return sb.String()
-}
-
-// TestCoverageMap is a map of TestIndex to *TestCoverage.
-type TestCoverageMap map[TestIndex]*TestCoverage
-
-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{}
-
-// List returns the full list of sorted span ids.
-func (s SpanSet) List() []SpanID {
- out := make([]SpanID, 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) containsAll(rhs SpanSet) bool {
- for span := range rhs {
- if _, found := s[span]; !found {
- return false
- }
- }
- return true
-}
-
-func (s SpanSet) sub(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 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
-}
-
-// 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 "), " ")
-}
-
-// Optimize optimizes the Tree by de-duplicating common spans into a tree of
-// SpanGroups.
-func (t *Tree) Optimize() {
- log.Printf("Optimizing coverage tree...")
-
- // Start by gathering all of the unique spansets
- wg := sync.WaitGroup{}
- wg.Add(len(t.files))
- for _, file := range t.files {
- file := file
- go func() {
- defer wg.Done()
- file.optimize()
- }()
- }
- wg.Wait()
-}
-
-func (f *treeFile) optimize() {
- const minSpansInGroup = 2
-
- type spansetKey string
- spansetMap := map[spansetKey]SpanSet{}
-
- f.tcm.traverse(func(tc *TestCoverage) {
- if len(tc.Spans) >= minSpansInGroup {
- key := spansetKey(tc.Spans.String())
- if _, ok := spansetMap[key]; !ok {
- spansetMap[key] = tc.Spans
- }
- }
- })
-
- if len(spansetMap) == 0 {
- return
- }
-
- // Sort by number of spans in each sets.
- type spansetInfo struct {
- key spansetKey
- set SpanSet // fully expanded set
- grp SpanGroup
- id SpanGroupID
- }
- spansets := make([]*spansetInfo, 0, len(spansetMap))
- for key, set := range spansetMap {
- spansets = append(spansets, &spansetInfo{
- key: key,
- set: set,
- grp: SpanGroup{spans: set},
- id: SpanGroupID(len(spansets)),
- })
- }
- sort.Slice(spansets, func(i, j int) bool { return len(spansets[i].set) > len(spansets[j].set) })
-
- // Loop over the spanGroups starting from the largest, and try to fold them
- // into the larger sets.
- // This is O(n^2) complexity.
-nextSpan:
- for i, a := range spansets[:len(spansets)-1] {
- for _, b := range spansets[i+1:] {
- if len(a.set) > len(b.set) && a.set.containsAll(b.set) {
- extend := b.id // Do not take address of iterator!
- a.grp.spans = a.set.sub(b.set)
- a.grp.extend = &extend
- continue nextSpan
- }
- }
- }
-
- // Rebuild a map of spansetKey to SpanGroup
- spangroupMap := make(map[spansetKey]*spansetInfo, len(spansets))
- for _, s := range spansets {
- spangroupMap[s.key] = s
- }
-
- // Store the groups in the tree
- f.spangroups = make(map[SpanGroupID]SpanGroup, len(spansets))
- for _, s := range spansets {
- f.spangroups[s.id] = s.grp
- }
-
- // Update all the uses.
- f.tcm.traverse(func(tc *TestCoverage) {
- key := spansetKey(tc.Spans.String())
- if g, ok := spangroupMap[key]; ok {
- tc.Spans = nil
- tc.Group = &g.id
- }
- })
-}
-
-// Encode zlib encodes the JSON coverage tree to w.
-func (t *Tree) Encode(revision string, w io.Writer) error {
- t.Optimize()
-
- zw := zlib.NewWriter(w)
-
- _, err := zw.Write([]byte(t.JSON(revision)))
- if err != nil {
- return err
- }
-
- return zw.Close()
-}
-
-// JSON returns the full test tree serialized to JSON.
-func (t *Tree) JSON(revision string) string {
- sb := &strings.Builder{}
- sb.WriteString(`{`)
-
- // write the revision
- sb.WriteString(`"r":"` + revision + `"`)
-
- // write the strings
- sb.WriteString(`,"n":[`)
- for i, s := range t.strings.s {
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(`"`)
- sb.WriteString(s)
- sb.WriteString(`"`)
- }
- sb.WriteString(`]`)
-
- // write the tests
- sb.WriteString(`,"t":`)
- t.writeTestJSON(&t.testRoot, sb)
-
- // write the spans
- sb.WriteString(`,"s":`)
- t.writeSpansJSON(sb)
-
- // write the files
- sb.WriteString(`,"f":`)
- t.writeFilesJSON(sb)
-
- sb.WriteString(`}`)
- return sb.String()
-}
-
-func (t *Tree) writeTestJSON(test *Test, sb *strings.Builder) {
- names := map[int]StringID{}
- for name, idx := range test.indices {
- names[int(idx)] = name
- }
-
- sb.WriteString(`[`)
- for i, child := range test.children {
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(`[`)
- sb.WriteString(fmt.Sprintf("%v,", names[i]))
- t.writeTestJSON(&child, sb)
- sb.WriteString(`]`)
- }
-
- sb.WriteString(`]`)
-}
-
-func (t *Tree) writeSpansJSON(sb *strings.Builder) {
- type spanAndID struct {
- span Span
- id SpanID
- }
- spans := make([]spanAndID, 0, len(t.spans))
- for span, id := range t.spans {
- spans = append(spans, spanAndID{span, id})
- }
- sort.Slice(spans, func(i, j int) bool { return spans[i].id < spans[j].id })
-
- sb.WriteString(`[`)
- for i, s := range spans {
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(fmt.Sprintf("[%v,%v,%v,%v]",
- s.span.Start.Line, s.span.Start.Column,
- s.span.End.Line, s.span.End.Column))
- }
-
- sb.WriteString(`]`)
-}
-
-func (t *Tree) writeFilesJSON(sb *strings.Builder) {
- paths := make([]string, 0, len(t.files))
- for path := range t.files {
- paths = append(paths, path)
- }
- sort.Strings(paths)
-
- sb.WriteString(`{`)
- for i, path := range paths {
- file := t.files[path]
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(`"`)
- sb.WriteString(path)
- sb.WriteString(`":`)
- sb.WriteString(`{`)
- sb.WriteString(`"g":`)
- t.writeSpanGroupsJSON(file.spangroups, sb)
- sb.WriteString(`,"c":`)
- t.writeCoverageMapJSON(file.tcm, sb)
- sb.WriteString(`}`)
- }
-
- sb.WriteString(`}`)
-}
-
-func (t *Tree) writeSpanGroupsJSON(spangroups map[SpanGroupID]SpanGroup, sb *strings.Builder) {
- type groupAndID struct {
- group SpanGroup
- id SpanGroupID
- }
- groups := make([]groupAndID, 0, len(spangroups))
- for id, group := range spangroups {
- groups = append(groups, groupAndID{group, id})
- }
- sort.Slice(groups, func(i, j int) bool { return groups[i].id < groups[j].id })
-
- sb.WriteString(`[`)
- for i, g := range groups {
- if i > 0 {
- sb.WriteString(`,`)
- }
- t.writeSpanGroupJSON(g.group, sb)
- }
- sb.WriteString(`]`)
-}
-
-func (t *Tree) writeSpanGroupJSON(group SpanGroup, sb *strings.Builder) {
- sb.WriteString(`{`)
- sb.WriteString(`"s":[`)
- for i, spanID := range group.spans.List() {
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(fmt.Sprintf("%v", spanID))
- }
- sb.WriteString(`]`)
- if group.extend != nil {
- sb.WriteString(`,"e":`)
- sb.WriteString(fmt.Sprintf("%v", *group.extend))
- }
- sb.WriteString(`}`)
-}
-
-func (t *Tree) writeCoverageMapJSON(c TestCoverageMap, sb *strings.Builder) {
- ids := make([]TestIndex, 0, len(c))
- for id := range c {
- ids = append(ids, id)
- }
- sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
-
- sb.WriteString(`[`)
- for i, id := range ids {
- if i > 0 {
- sb.WriteString(`,`)
- }
-
- sb.WriteString(`[`)
- sb.WriteString(fmt.Sprintf("%v", id))
- sb.WriteString(`,`)
- t.writeCoverageJSON(c[id], sb)
- sb.WriteString(`]`)
- }
- sb.WriteString(`]`)
-}
-
-func (t *Tree) writeCoverageJSON(c *TestCoverage, sb *strings.Builder) {
- sb.WriteString(`{`)
- comma := false
- if len(c.Spans) > 0 {
- sb.WriteString(`"s":[`)
- for i, spanID := range c.Spans.List() {
- if i > 0 {
- sb.WriteString(`,`)
- }
- sb.WriteString(fmt.Sprintf("%v", spanID))
- }
- sb.WriteString(`]`)
- comma = true
- }
- if c.Group != nil {
- sb.WriteString(`"g":`)
- sb.WriteString(fmt.Sprintf("%v", *c.Group))
- comma = true
- }
- if len(c.Children) > 0 {
- if comma {
- sb.WriteString(`,`)
- }
- sb.WriteString(`"c":`)
- t.writeCoverageMapJSON(c.Children, sb)
- }
- sb.WriteString(`}`)
-}
-
-// ReadJSON parses the JSON Tree from r.
-func ReadJSON(r io.Reader) (*Tree, string, error) {
- p := parser{r: bufio.NewReader(r)}
- return p.parse()
-}
-
-type parser struct {
- r *bufio.Reader
- err error
-
- revision string
- tree Tree
-}
-
-func (p *parser) parse() (*Tree, string, error) {
- p.tree.init()
- p.dict(func(key string) {
- switch key {
- case "r":
- p.revision = p.str()
- case "n":
- p.parseStrings()
- case "t":
- p.parseTests(&p.tree.testRoot)
- case "s":
- p.parseSpans()
- case "g":
- p.parseSpanGroups()
- case "f":
- p.parseFiles()
- default:
- p.fail("Unknown root key '%v'", key)
- }
- })
- if p.err != nil {
- return nil, "", p.err
- }
- return &p.tree, p.revision, nil
-}
-
-func (p *parser) parseStrings() {
- p.array(func(idx int) {
- id := StringID(idx)
- s := p.str()
- p.tree.strings.m[s] = id
- p.tree.strings.s = append(p.tree.strings.s, s)
- })
-}
-
-func (p *parser) parseTests(t *Test) {
- p.array(func(idx int) {
- p.expect("[")
- name := StringID(p.integer())
- child, _ := t.index(name)
- p.expect(",")
- p.parseTests(child)
- p.expect("]")
- })
-}
-
-func (p *parser) parseSpans() {
- p.array(func(idx int) {
- p.tree.spans[p.parseSpan()] = SpanID(idx)
- })
-}
-
-func (p *parser) parseSpan() Span {
- p.expect("[")
- s := Span{}
- s.Start.Line = p.integer()
- p.expect(",")
- s.Start.Column = p.integer()
- p.expect(",")
- s.End.Line = p.integer()
- p.expect(",")
- s.End.Column = p.integer()
- p.expect("]")
- return s
-}
-
-func (p *parser) parseFiles() {
- p.dict(func(path string) {
- p.tree.files[path] = p.parseFile()
- })
-}
-
-func (p *parser) parseFile() *treeFile {
- file := newTreeFile()
- if p.peek() == '{' {
- p.dict(func(key string) {
- switch key {
- case "g":
- file.spangroups = p.parseSpanGroups()
- case "c":
- p.parseCoverageMap(file.tcm)
- default:
- p.fail("Unknown file key: '%s'", key)
- }
- })
- } else { // backwards compatibility
- p.parseCoverageMap(file.tcm)
- }
- return file
-}
-
-func (p *parser) parseSpanGroups() map[SpanGroupID]SpanGroup {
- spangroups := map[SpanGroupID]SpanGroup{}
- p.array(func(groupIdx int) {
- g := newSpanGroup()
- p.dict(func(key string) {
- switch key {
- case "s":
- p.array(func(spanIdx int) {
- id := SpanID(p.integer())
- g.spans[id] = struct{}{}
- })
- case "e":
- extend := SpanGroupID(p.integer())
- g.extend = &extend
- }
- })
- spangroups[SpanGroupID(groupIdx)] = g
- })
- return spangroups
-}
-
-func (p *parser) parseCoverageMap(tcm TestCoverageMap) {
- p.array(func(int) {
- p.expect("[")
- idx := TestIndex(p.integer())
- p.expect(",")
- p.parseCoverage(tcm.index(idx))
- p.expect("]")
- })
-}
-
-func (p *parser) parseCoverage(tc *TestCoverage) {
- p.dict(func(key string) {
- switch key {
- case "s":
- p.array(func(int) {
- id := SpanID(p.integer())
- tc.Spans[id] = struct{}{}
- })
- case "g":
- groupID := SpanGroupID(p.integer())
- tc.Group = &groupID
- case "c":
- p.parseCoverageMap(tc.Children)
- default:
- p.fail("Unknown test key: '%s'", key)
- }
- })
-}
-
-func (p *parser) array(f func(idx int)) {
- p.expect("[")
- if p.match("]") {
- return
- }
- idx := 0
- for p.err == nil {
- f(idx)
- if !p.match(",") {
- p.expect("]")
- return
- }
- idx++
- }
- p.expect("]")
-}
-
-func (p *parser) dict(f func(key string)) {
- p.expect("{")
- if p.match("}") {
- return
- }
- for p.err == nil {
- key := p.str()
- p.expect(`:`)
- f(key)
- if !p.match(",") {
- p.expect("}")
- return
- }
- }
- p.expect("}")
-}
-
-func (p *parser) next() byte {
- d := make([]byte, 1)
- n, err := p.r.Read(d)
- if err != nil || n != 1 {
- p.err = err
- return 0
- }
- return d[0]
-}
-
-func (p *parser) peek() byte {
- d, err := p.r.Peek(1)
- if err != nil {
- p.err = err
- return 0
- }
- return d[0]
-}
-
-func (p *parser) expect(s string) {
- if p.err != nil {
- return
- }
- d := make([]byte, len(s))
- n, err := p.r.Read(d)
- if err != nil {
- p.err = err
- return
- }
- got := string(d[:n])
- if got != s {
- p.fail("Expected '%v', got '%v'", s, got)
- return
- }
-}
-
-func (p *parser) match(s string) bool {
- got, err := p.r.Peek(len(s))
- if err != nil {
- return false
- }
- if string(got) != s {
- return false
- }
- p.r.Discard(len(s))
- return true
-}
-
-func (p *parser) str() string {
- p.expect(`"`)
- sb := strings.Builder{}
- for p.err == nil {
- c := p.next()
- if c == '"' {
- return sb.String()
- }
- sb.WriteByte(c)
- }
- return ""
-}
-
-func (p *parser) integer() int {
- sb := strings.Builder{}
- for {
- if c := p.peek(); c < '0' || c > '9' {
- break
- }
- sb.WriteByte(p.next())
- }
- if sb.Len() == 0 {
- p.fail("Expected integer, got '%c'", p.peek())
- return 0
- }
- i, err := strconv.Atoi(sb.String())
- if err != nil {
- p.fail("Failed to parse integer: %v", err)
- return 0
- }
- return i
-}
-
-func (p *parser) fail(msg string, args ...interface{}) {
- if p.err == nil {
- msg = fmt.Sprintf(msg, args...)
- stack := string(debug.Stack())
- p.err = fmt.Errorf("%v\nCallstack:\n%v", msg, stack)
- }
-}
diff --git a/tests/regres/cov/import.go b/tests/regres/cov/import.go
new file mode 100644
index 0000000..e3c3053
--- /dev/null
+++ b/tests/regres/cov/import.go
@@ -0,0 +1,246 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// 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 cov
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "../cause"
+ "../llvm"
+)
+
+// Location describes a single line-column position in a source file.
+type Location struct {
+ Line, Column int
+}
+
+func (l Location) String() string {
+ return fmt.Sprintf("%v:%v", l.Line, l.Column)
+}
+
+// Span describes a start and end interval in a source file.
+type Span struct {
+ Start, End Location
+}
+
+func (l Span) String() string {
+ return fmt.Sprintf("%v-%v", l.Start, l.End)
+}
+
+// File describes the coverage spans in a single source file.
+type File struct {
+ Path string
+ Spans []Span
+}
+
+// Coverage describes the coverage spans for all the source files for a single
+// process invocation.
+type Coverage struct {
+ Files []File
+}
+
+// Env holds the enviroment settings for performing coverage processing.
+type Env struct {
+ LLVM llvm.Toolchain
+ RootDir string // path to SwiftShader git root directory
+ ExePath string // path to the executable binary
+ TurboCov string // path to turbo-cov (optional)
+}
+
+// AppendRuntimeEnv returns the environment variables env with the
+// LLVM_PROFILE_FILE environment variable appended.
+func AppendRuntimeEnv(env []string, coverageFile string) []string {
+ return append(env, "LLVM_PROFILE_FILE="+coverageFile)
+}
+
+// Import uses the llvm-profdata and llvm-cov tools to import the coverage
+// information from a .profraw file.
+func (e Env) Import(profrawPath string) (*Coverage, error) {
+ profdata := profrawPath + ".profdata"
+
+ if err := exec.Command(e.LLVM.Profdata(), "merge", "-sparse", profrawPath, "-output", profdata).Run(); err != nil {
+ return nil, cause.Wrap(err, "llvm-profdata errored")
+ }
+ defer os.Remove(profdata)
+
+ if e.TurboCov == "" {
+ args := []string{
+ "export",
+ e.ExePath,
+ "-instr-profile=" + profdata,
+ "-format=text",
+ }
+ if e.LLVM.Version.GreaterEqual(llvm.Version{Major: 9}) {
+ // LLVM 9 has new flags that omit stuff we don't care about.
+ args = append(args,
+ "-skip-expansions",
+ "-skip-functions",
+ )
+ }
+
+ data, err := exec.Command(e.LLVM.Cov(), args...).Output()
+ if err != nil {
+ return nil, cause.Wrap(err, "llvm-cov errored: %v", string(err.(*exec.ExitError).Stderr))
+ }
+ cov, err := e.parseCov(data)
+ if err != nil {
+ return nil, cause.Wrap(err, "Couldn't parse coverage json data")
+ }
+ return cov, nil
+ }
+
+ data, err := exec.Command(e.TurboCov, e.ExePath, profdata).Output()
+ if err != nil {
+ return nil, cause.Wrap(err, "turbo-cov errored: %v", string(err.(*exec.ExitError).Stderr))
+ }
+ cov, err := e.parseTurboCov(data)
+ if err != nil {
+ return nil, cause.Wrap(err, "Couldn't process turbo-cov output")
+ }
+ return cov, nil
+}
+
+// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
+// https://stackoverflow.com/a/56792192
+func (e Env) parseCov(raw []byte) (*Coverage, error) {
+ // line int, col int, count int64, hasCount bool, isRegionEntry bool
+ type segment []interface{}
+
+ type file struct {
+ // expansions ignored
+ Name string `json:"filename"`
+ Segments []segment `json:"segments"`
+ // summary ignored
+ }
+
+ type data struct {
+ Files []file `json:"files"`
+ }
+
+ root := struct {
+ Data []data `json:"data"`
+ }{}
+ err := json.NewDecoder(bytes.NewReader(raw)).Decode(&root)
+ if err != nil {
+ return nil, err
+ }
+
+ c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
+ for _, f := range root.Data[0].Files {
+ relpath, err := filepath.Rel(e.RootDir, f.Name)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(relpath, "..") {
+ continue
+ }
+ file := File{Path: relpath}
+ for sIdx := 0; sIdx+1 < len(f.Segments); sIdx++ {
+ start := Location{(int)(f.Segments[sIdx][0].(float64)), (int)(f.Segments[sIdx][1].(float64))}
+ end := Location{(int)(f.Segments[sIdx+1][0].(float64)), (int)(f.Segments[sIdx+1][1].(float64))}
+ covered := f.Segments[sIdx][2].(float64) != 0
+ if covered {
+ if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
+ file.Spans[c-1].End = end
+ } else {
+ file.Spans = append(file.Spans, Span{start, end})
+ }
+ }
+ }
+ if len(file.Spans) > 0 {
+ c.Files = append(c.Files, file)
+ }
+ }
+
+ return c, nil
+}
+
+func (e Env) parseTurboCov(data []byte) (*Coverage, error) {
+ u32 := func() uint32 {
+ out := binary.LittleEndian.Uint32(data)
+ data = data[4:]
+ return out
+ }
+ u8 := func() uint8 {
+ out := data[0]
+ data = data[1:]
+ return out
+ }
+ str := func() string {
+ len := u32()
+ out := data[:len]
+ data = data[len:]
+ return string(out)
+ }
+
+ numFiles := u32()
+ c := &Coverage{Files: make([]File, 0, numFiles)}
+ for i := 0; i < int(numFiles); i++ {
+ path := str()
+ relpath, err := filepath.Rel(e.RootDir, path)
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(relpath, "..") {
+ continue
+ }
+
+ file := File{Path: relpath}
+
+ type segment struct {
+ location Location
+ count int
+ covered bool
+ }
+
+ numSegements := u32()
+ segments := make([]segment, numSegements)
+ for j := range segments {
+ segment := &segments[j]
+ segment.location.Line = int(u32())
+ segment.location.Column = int(u32())
+ segment.count = int(u32())
+ segment.covered = u8() != 0
+ }
+
+ for sIdx := 0; sIdx+1 < len(segments); sIdx++ {
+ start := segments[sIdx].location
+ end := segments[sIdx+1].location
+ if segments[sIdx].count > 0 {
+ if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
+ file.Spans[c-1].End = end
+ } else {
+ file.Spans = append(file.Spans, Span{start, end})
+ }
+ }
+ }
+
+ if len(file.Spans) > 0 {
+ c.Files = append(c.Files, file)
+ }
+ }
+
+ return c, nil
+}
+
+// Path is a tree node path formed from a list of strings
+type Path []string
diff --git a/tests/regres/cov/parser.go b/tests/regres/cov/parser.go
new file mode 100644
index 0000000..413d808
--- /dev/null
+++ b/tests/regres/cov/parser.go
@@ -0,0 +1,514 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// 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 cov
+
+import (
+ "bufio"
+ "compress/zlib"
+ "fmt"
+ "io"
+ "runtime/debug"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// ReadJSON parses the JSON Tree from r.
+func ReadJSON(r io.Reader) (*Tree, string, error) {
+ p := parser{r: bufio.NewReader(r)}
+ return p.parse()
+}
+
+// Encode zlib encodes the JSON coverage tree to w.
+func (t *Tree) Encode(revision string, w io.Writer) error {
+ t.Optimize()
+
+ zw := zlib.NewWriter(w)
+
+ _, err := zw.Write([]byte(t.JSON(revision)))
+ if err != nil {
+ return err
+ }
+
+ return zw.Close()
+}
+
+// JSON returns the full test tree serialized to JSON.
+func (t *Tree) JSON(revision string) string {
+ sb := &strings.Builder{}
+ sb.WriteString(`{`)
+
+ // write the revision
+ sb.WriteString(`"r":"` + revision + `"`)
+
+ // write the strings
+ sb.WriteString(`,"n":[`)
+ for i, s := range t.strings.s {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(`"`)
+ sb.WriteString(s)
+ sb.WriteString(`"`)
+ }
+ sb.WriteString(`]`)
+
+ // write the tests
+ sb.WriteString(`,"t":`)
+ t.writeTestJSON(&t.testRoot, sb)
+
+ // write the spans
+ sb.WriteString(`,"s":`)
+ t.writeSpansJSON(sb)
+
+ // write the files
+ sb.WriteString(`,"f":`)
+ t.writeFilesJSON(sb)
+
+ sb.WriteString(`}`)
+ return sb.String()
+}
+
+func (t *Tree) writeTestJSON(test *Test, sb *strings.Builder) {
+ names := map[int]StringID{}
+ for name, idx := range test.indices {
+ names[int(idx)] = name
+ }
+
+ sb.WriteString(`[`)
+ for i, child := range test.children {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(`[`)
+ sb.WriteString(fmt.Sprintf("%v,", names[i]))
+ t.writeTestJSON(&child, sb)
+ sb.WriteString(`]`)
+ }
+
+ sb.WriteString(`]`)
+}
+
+func (t *Tree) writeSpansJSON(sb *strings.Builder) {
+ type spanAndID struct {
+ span Span
+ id SpanID
+ }
+ spans := make([]spanAndID, 0, len(t.spans))
+ for span, id := range t.spans {
+ spans = append(spans, spanAndID{span, id})
+ }
+ sort.Slice(spans, func(i, j int) bool { return spans[i].id < spans[j].id })
+
+ sb.WriteString(`[`)
+ for i, s := range spans {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(fmt.Sprintf("[%v,%v,%v,%v]",
+ s.span.Start.Line, s.span.Start.Column,
+ s.span.End.Line, s.span.End.Column))
+ }
+
+ sb.WriteString(`]`)
+}
+
+func (t *Tree) writeFilesJSON(sb *strings.Builder) {
+ paths := make([]string, 0, len(t.files))
+ for path := range t.files {
+ paths = append(paths, path)
+ }
+ sort.Strings(paths)
+
+ sb.WriteString(`{`)
+ for i, path := range paths {
+ file := t.files[path]
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(`"`)
+ sb.WriteString(path)
+ sb.WriteString(`":`)
+ sb.WriteString(`{`)
+ sb.WriteString(`"g":`)
+ t.writeSpanGroupsJSON(file.spangroups, sb)
+ sb.WriteString(`,"c":`)
+ t.writeCoverageMapJSON(file.tcm, sb)
+ sb.WriteString(`}`)
+ }
+
+ sb.WriteString(`}`)
+}
+
+func (t *Tree) writeSpanGroupsJSON(spangroups map[SpanGroupID]SpanGroup, sb *strings.Builder) {
+ type groupAndID struct {
+ group SpanGroup
+ id SpanGroupID
+ }
+ groups := make([]groupAndID, 0, len(spangroups))
+ for id, group := range spangroups {
+ groups = append(groups, groupAndID{group, id})
+ }
+ sort.Slice(groups, func(i, j int) bool { return groups[i].id < groups[j].id })
+
+ sb.WriteString(`[`)
+ for i, g := range groups {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ t.writeSpanGroupJSON(g.group, sb)
+ }
+ sb.WriteString(`]`)
+}
+
+func (t *Tree) writeSpanGroupJSON(group SpanGroup, sb *strings.Builder) {
+ sb.WriteString(`{`)
+ sb.WriteString(`"s":[`)
+ for i, spanID := range group.spans.List() {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(fmt.Sprintf("%v", spanID))
+ }
+ sb.WriteString(`]`)
+ if group.extend != nil {
+ sb.WriteString(`,"e":`)
+ sb.WriteString(fmt.Sprintf("%v", *group.extend))
+ }
+ sb.WriteString(`}`)
+}
+
+func (t *Tree) writeCoverageMapJSON(c TestCoverageMap, sb *strings.Builder) {
+ ids := make([]TestIndex, 0, len(c))
+ for id := range c {
+ ids = append(ids, id)
+ }
+ sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
+
+ sb.WriteString(`[`)
+ for i, id := range ids {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+
+ sb.WriteString(`[`)
+ sb.WriteString(fmt.Sprintf("%v", id))
+ sb.WriteString(`,`)
+ t.writeCoverageJSON(c[id], sb)
+ sb.WriteString(`]`)
+ }
+ sb.WriteString(`]`)
+}
+
+func (t *Tree) writeCoverageJSON(c *TestCoverage, sb *strings.Builder) {
+ sb.WriteString(`{`)
+ comma := false
+ if len(c.Spans) > 0 {
+ sb.WriteString(`"s":[`)
+ for i, spanID := range c.Spans.List() {
+ if i > 0 {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(fmt.Sprintf("%v", spanID))
+ }
+ sb.WriteString(`]`)
+ comma = true
+ }
+ if c.Group != nil {
+ sb.WriteString(`"g":`)
+ sb.WriteString(fmt.Sprintf("%v", *c.Group))
+ comma = true
+ }
+ if len(c.Children) > 0 {
+ if comma {
+ sb.WriteString(`,`)
+ }
+ sb.WriteString(`"c":`)
+ t.writeCoverageMapJSON(c.Children, sb)
+ }
+ sb.WriteString(`}`)
+}
+
+type parser struct {
+ r *bufio.Reader
+ err error
+
+ revision string
+ tree Tree
+}
+
+func (p *parser) parse() (*Tree, string, error) {
+ p.tree.init()
+ p.dict(func(key string) {
+ switch key {
+ case "r":
+ p.revision = p.str()
+ case "n":
+ p.parseStrings()
+ case "t":
+ p.parseTests(&p.tree.testRoot)
+ case "s":
+ p.parseSpans()
+ case "g":
+ p.parseSpanGroups()
+ case "f":
+ p.parseFiles()
+ default:
+ p.fail("Unknown root key '%v'", key)
+ }
+ })
+ if p.err != nil {
+ return nil, "", p.err
+ }
+ return &p.tree, p.revision, nil
+}
+
+func (p *parser) parseStrings() {
+ p.array(func(idx int) {
+ id := StringID(idx)
+ s := p.str()
+ p.tree.strings.m[s] = id
+ p.tree.strings.s = append(p.tree.strings.s, s)
+ })
+}
+
+func (p *parser) parseTests(t *Test) {
+ p.array(func(idx int) {
+ p.expect("[")
+ name := StringID(p.integer())
+ child, _ := t.index(name)
+ p.expect(",")
+ p.parseTests(child)
+ p.expect("]")
+ })
+}
+
+func (p *parser) parseSpans() {
+ p.array(func(idx int) {
+ p.tree.spans[p.parseSpan()] = SpanID(idx)
+ })
+}
+
+func (p *parser) parseSpan() Span {
+ p.expect("[")
+ s := Span{}
+ s.Start.Line = p.integer()
+ p.expect(",")
+ s.Start.Column = p.integer()
+ p.expect(",")
+ s.End.Line = p.integer()
+ p.expect(",")
+ s.End.Column = p.integer()
+ p.expect("]")
+ return s
+}
+
+func (p *parser) parseFiles() {
+ p.dict(func(path string) {
+ p.tree.files[path] = p.parseFile()
+ })
+}
+
+func (p *parser) parseFile() *treeFile {
+ file := newTreeFile()
+ if p.peek() == '{' {
+ p.dict(func(key string) {
+ switch key {
+ case "g":
+ file.spangroups = p.parseSpanGroups()
+ case "c":
+ p.parseCoverageMap(file.tcm)
+ default:
+ p.fail("Unknown file key: '%s'", key)
+ }
+ })
+ } else { // backwards compatibility
+ p.parseCoverageMap(file.tcm)
+ }
+ return file
+}
+
+func (p *parser) parseSpanGroups() map[SpanGroupID]SpanGroup {
+ spangroups := map[SpanGroupID]SpanGroup{}
+ p.array(func(groupIdx int) {
+ g := newSpanGroup()
+ p.dict(func(key string) {
+ switch key {
+ case "s":
+ p.array(func(spanIdx int) {
+ id := SpanID(p.integer())
+ g.spans[id] = struct{}{}
+ })
+ case "e":
+ extend := SpanGroupID(p.integer())
+ g.extend = &extend
+ }
+ })
+ spangroups[SpanGroupID(groupIdx)] = g
+ })
+ return spangroups
+}
+
+func (p *parser) parseCoverageMap(tcm TestCoverageMap) {
+ p.array(func(int) {
+ p.expect("[")
+ idx := TestIndex(p.integer())
+ p.expect(",")
+ p.parseCoverage(tcm.index(idx))
+ p.expect("]")
+ })
+}
+
+func (p *parser) parseCoverage(tc *TestCoverage) {
+ p.dict(func(key string) {
+ switch key {
+ case "s":
+ p.array(func(int) {
+ id := SpanID(p.integer())
+ tc.Spans[id] = struct{}{}
+ })
+ case "g":
+ groupID := SpanGroupID(p.integer())
+ tc.Group = &groupID
+ case "c":
+ p.parseCoverageMap(tc.Children)
+ default:
+ p.fail("Unknown test key: '%s'", key)
+ }
+ })
+}
+
+func (p *parser) array(f func(idx int)) {
+ p.expect("[")
+ if p.match("]") {
+ return
+ }
+ idx := 0
+ for p.err == nil {
+ f(idx)
+ if !p.match(",") {
+ p.expect("]")
+ return
+ }
+ idx++
+ }
+ p.expect("]")
+}
+
+func (p *parser) dict(f func(key string)) {
+ p.expect("{")
+ if p.match("}") {
+ return
+ }
+ for p.err == nil {
+ key := p.str()
+ p.expect(`:`)
+ f(key)
+ if !p.match(",") {
+ p.expect("}")
+ return
+ }
+ }
+ p.expect("}")
+}
+
+func (p *parser) next() byte {
+ d := make([]byte, 1)
+ n, err := p.r.Read(d)
+ if err != nil || n != 1 {
+ p.err = err
+ return 0
+ }
+ return d[0]
+}
+
+func (p *parser) peek() byte {
+ d, err := p.r.Peek(1)
+ if err != nil {
+ p.err = err
+ return 0
+ }
+ return d[0]
+}
+
+func (p *parser) expect(s string) {
+ if p.err != nil {
+ return
+ }
+ d := make([]byte, len(s))
+ n, err := p.r.Read(d)
+ if err != nil {
+ p.err = err
+ return
+ }
+ got := string(d[:n])
+ if got != s {
+ p.fail("Expected '%v', got '%v'", s, got)
+ return
+ }
+}
+
+func (p *parser) match(s string) bool {
+ got, err := p.r.Peek(len(s))
+ if err != nil {
+ return false
+ }
+ if string(got) != s {
+ return false
+ }
+ p.r.Discard(len(s))
+ return true
+}
+
+func (p *parser) str() string {
+ p.expect(`"`)
+ sb := strings.Builder{}
+ for p.err == nil {
+ c := p.next()
+ if c == '"' {
+ return sb.String()
+ }
+ sb.WriteByte(c)
+ }
+ return ""
+}
+
+func (p *parser) integer() int {
+ sb := strings.Builder{}
+ for {
+ if c := p.peek(); c < '0' || c > '9' {
+ break
+ }
+ sb.WriteByte(p.next())
+ }
+ if sb.Len() == 0 {
+ p.fail("Expected integer, got '%c'", p.peek())
+ return 0
+ }
+ i, err := strconv.Atoi(sb.String())
+ if err != nil {
+ p.fail("Failed to parse integer: %v", err)
+ return 0
+ }
+ return i
+}
+
+func (p *parser) fail(msg string, args ...interface{}) {
+ if p.err == nil {
+ msg = fmt.Sprintf(msg, args...)
+ stack := string(debug.Stack())
+ p.err = fmt.Errorf("%v\nCallstack:\n%v", msg, stack)
+ }
+}
diff --git a/tests/regres/cov/tree.go b/tests/regres/cov/tree.go
new file mode 100644
index 0000000..d4c356b
--- /dev/null
+++ b/tests/regres/cov/tree.go
@@ -0,0 +1,478 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// 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 cov
+
+import (
+ "fmt"
+ "log"
+ "sort"
+ "strings"
+ "sync"
+)
+
+type treeFile struct {
+ tcm TestCoverageMap
+ spangroups map[SpanGroupID]SpanGroup
+}
+
+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() []Span {
+ out := make([]Span, 0, len(t.spans))
+ for span := range t.spans {
+ out = append(out, span)
+ }
+ sort.Slice(out, func(i, j int) bool {
+ if out[i].Start.Line < out[j].Start.Line {
+ return true
+ }
+ if out[i].Start.Line > out[j].Start.Line {
+ return false
+ }
+ return out[i].Start.Column < out[j].Start.Column
+ })
+ return out
+}
+
+// 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 []Span) 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
+ }
+
+ // Add all the spans to the map, get the span ids
+ spans := t.addSpans(file.Spans)
+
+ // 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.add(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.sub(spans); len(lower) > 0 {
+ // push into each child node
+ for i := range test.children {
+ child := tc.Children.index(TestIndex(i))
+ child.Spans = child.Spans.add(lower)
+ }
+ // remove from node
+ tc.Spans = tc.Spans.sub(lower)
+ }
+
+ // The spans that are in the new test, but are not part of the tree
+ // node carry propagating down.
+ spans = spans.sub(tc.Spans)
+ if len(spans) == 0 {
+ continue nextFile
+ }
+
+ tcm = tc.Children
+ parent = tc
+ }
+ }
+}
+
+// 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(fmt.Sprintf("{%v", tc.Spans))
+ if len(tc.Children) > 0 {
+ sb.WriteString(" ")
+ sb.WriteString(tc.Children.String(t, s))
+ }
+ sb.WriteString("}")
+ return sb.String()
+}
+
+// TestCoverageMap is a map of TestIndex to *TestCoverage.
+type TestCoverageMap map[TestIndex]*TestCoverage
+
+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{}
+
+// List returns the full list of sorted span ids.
+func (s SpanSet) List() []SpanID {
+ out := make([]SpanID, 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) containsAll(rhs SpanSet) bool {
+ for span := range rhs {
+ if _, found := s[span]; !found {
+ return false
+ }
+ }
+ return true
+}
+
+func (s SpanSet) sub(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 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
+}
+
+// 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 "), " ")
+}
+
+// Optimize optimizes the Tree by de-duplicating common spans into a tree of
+// SpanGroups.
+func (t *Tree) Optimize() {
+ log.Printf("Optimizing coverage tree...")
+
+ // Start by gathering all of the unique spansets
+ wg := sync.WaitGroup{}
+ wg.Add(len(t.files))
+ for _, file := range t.files {
+ file := file
+ go func() {
+ defer wg.Done()
+ file.optimize()
+ }()
+ }
+ wg.Wait()
+}
+
+func (f *treeFile) optimize() {
+ const minSpansInGroup = 2
+
+ type spansetKey string
+ spansetMap := map[spansetKey]SpanSet{}
+
+ f.tcm.traverse(func(tc *TestCoverage) {
+ if len(tc.Spans) >= minSpansInGroup {
+ key := spansetKey(tc.Spans.String())
+ if _, ok := spansetMap[key]; !ok {
+ spansetMap[key] = tc.Spans
+ }
+ }
+ })
+
+ if len(spansetMap) == 0 {
+ return
+ }
+
+ // Sort by number of spans in each sets.
+ type spansetInfo struct {
+ key spansetKey
+ set SpanSet // fully expanded set
+ grp SpanGroup
+ id SpanGroupID
+ }
+ spansets := make([]*spansetInfo, 0, len(spansetMap))
+ for key, set := range spansetMap {
+ spansets = append(spansets, &spansetInfo{
+ key: key,
+ set: set,
+ grp: SpanGroup{spans: set},
+ id: SpanGroupID(len(spansets)),
+ })
+ }
+ sort.Slice(spansets, func(i, j int) bool { return len(spansets[i].set) > len(spansets[j].set) })
+
+ // Loop over the spanGroups starting from the largest, and try to fold them
+ // into the larger sets.
+ // This is O(n^2) complexity.
+nextSpan:
+ for i, a := range spansets[:len(spansets)-1] {
+ for _, b := range spansets[i+1:] {
+ if len(a.set) > len(b.set) && a.set.containsAll(b.set) {
+ extend := b.id // Do not take address of iterator!
+ a.grp.spans = a.set.sub(b.set)
+ a.grp.extend = &extend
+ continue nextSpan
+ }
+ }
+ }
+
+ // Rebuild a map of spansetKey to SpanGroup
+ spangroupMap := make(map[spansetKey]*spansetInfo, len(spansets))
+ for _, s := range spansets {
+ spangroupMap[s.key] = s
+ }
+
+ // Store the groups in the tree
+ f.spangroups = make(map[SpanGroupID]SpanGroup, len(spansets))
+ for _, s := range spansets {
+ f.spangroups[s.id] = s.grp
+ }
+
+ // Update all the uses.
+ f.tcm.traverse(func(tc *TestCoverage) {
+ key := spansetKey(tc.Spans.String())
+ if g, ok := spangroupMap[key]; ok {
+ tc.Spans = nil
+ tc.Group = &g.id
+ }
+ })
+}