tools: Port 'cov' package from SwiftShader

A library for dealing with coverage information, and compressing CTS test information down into something that can be easily downloaded and visualized in a web page

Change-Id: If025e1555665609f7c963bad14c9c91d8324a390
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/113642
Reviewed-by: Austin Eng <enga@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
This commit is contained in:
Ben Clayton 2022-12-12 21:49:02 +00:00 committed by Dawn LUCI CQ
parent f6d95d3244
commit f8e0aac2a6
10 changed files with 2274 additions and 0 deletions

17
tools/src/cov/cov.go Normal file
View File

@ -0,0 +1,17 @@
// 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 cov provides functions for consuming and combining llvm coverage
// information from multiple processes.
package cov

View File

@ -0,0 +1,377 @@
// 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 cov_test
import (
"reflect"
"strings"
"testing"
"dawn.googlesource.com/dawn/tools/src/cov"
)
var (
fileA = "coverage/file/a"
fileB = "coverage/file/b"
fileC = "coverage/file/c"
fileD = "coverage/file/c"
span0 = cov.Span{cov.Location{3, 2}, cov.Location{3, 9}}
span1 = cov.Span{cov.Location{4, 1}, cov.Location{5, 1}}
span2 = cov.Span{cov.Location{5, 5}, cov.Location{5, 7}}
span3 = cov.Span{cov.Location{7, 2}, cov.Location{7, 7}}
)
// a
// ╭───────┴───────╮
// b c
// ╭───┴───╮ ╭───┴───╮
// d e f g
// ╭─┴─╮ ╭─┴─╮ ╭─┴─╮ ╭─┴─╮
// h i j k l m n o
// ╭┴╮ ╭┴╮ ╭┴╮ ╭┴╮ ╭┴╮ ╭╯
// p q r s t u v w x y z
//
func TestTree(t *testing.T) {
tree := &cov.Tree{}
t.Log("Add 'b' with the coverage [0,1]")
tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
// [0,1]
// (a)
// ╭─────╯
// b
checkSpans(t, tree.Spans(), span0, span1)
checkTests(t, tree, `{a:{b}}`)
checkCoverage(t, tree, fileA, `a:{[0,1]}`)
t.Log("Add 'i' with the coverage [0,1]")
tree.Add(cov.Path{"a", "b", "d", "i"}, coverage(fileA, span0, span1))
// [0,1]
// (a)
// ╭─────╯
// b
// ╭──╯
// d
// ╰─╮
// i
checkSpans(t, tree.Spans(), span0, span1)
checkTests(t, tree, `{a:{b:{d:{i}}}}`)
checkCoverage(t, tree, fileA, `a:{[0,1]}`)
t.Log("Add 'e' with the coverage [0,1,2]")
tree.Add(cov.Path{"a", "b", "e"}, coverage(fileA, span0, span1, span2))
// [0,1]
// (a)
// ┏━━━━━┛
// (b)
// ╭──┺━━┓
// d (e)[2]
// ╰─╮
// i
checkSpans(t, tree.Spans(), span0, span1, span2)
checkTests(t, tree, `{a:{b:{d:{i} e}}}`)
checkCoverage(t, tree, fileA, `a:{[0,1] b:{e:{[2]}}}`)
t.Log("Add 'n' with the coverage [0,3]")
tree.Add(cov.Path{"a", "c", "g", "n"}, coverage(fileA, span0, span3))
// [0]
// (a)
// ┏━━━━━┻━━━━━┓
// [1](b) (c)[3]
// ╭──┺━━┓ ╰──╮
// d (e)[2] g
// ╰─╮ ╭─╯
// i n
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i}e}c:{g:{n}}}}`)
checkCoverage(t, tree, fileA, `a:{[0] b:{[1] e:{[2]}} c:{[3]}}`)
t.Log("Add 'o' with the coverage [0, 3]")
tree.Add(cov.Path{"a", "c", "g", "o"}, coverage(fileA, span0, span3))
// [0]
// (a)
// ┏━━━━━━━┻━━━━━━━┓
// [1](b) (c)[3]
// ╭──┺━━┓ ╰──╮
// d (e)[2] g
// ╰─╮ ╭─┴─╮
// i n o
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i}e}c:{g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{[0] b:{[1] e:{[2]}} c:{[3]}}`)
t.Log("Add 'f' with the coverage [1]")
tree.Add(cov.Path{"a", "c", "f"}, coverage(fileA, span1))
// (a)
// ┏━━━━━━━━┻━━━━━━━━┓
// [0,1](b) (c)
// ╭──┺━━┓ ┏━━┻━━┓
// d (e)[2] [1](f) (g)[0,3]
// ╰─╮ ╭─┴─╮
// i n o
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e} c:{f g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{[0,1] e:{[2]}} c:{f:{[1]} g:{[0,3]}}}`)
t.Log("Add 'j' with the coverage [3]")
tree.Add(cov.Path{"a", "b", "e", "j"}, coverage(fileA, span3))
// (a)
// ┏━━━━━━━━┻━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━┻━━┓
// [0,1](d) (e)[3] [1](f) (g)[0,3]
// ╰─╮ ╭─╯ ╭─┴─╮
// i j n o
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e:{j}} c:{f g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1]} g:{[0,3]}}}`)
t.Log("Add 'k' with the coverage [3]")
tree.Add(cov.Path{"a", "b", "e", "k"}, coverage(fileA, span3))
// (a)
// ┏━━━━━━━━┻━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━┻━━┓
// [0,1](d) (e)[3] [1](f) (g)[0,3]
// ╰─╮ ╭─┴─╮ ╭─┴─╮
// i j k n o
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1]} g:{[0,3]}}}`)
t.Log("Add 'v' with the coverage [1,2]")
tree.Add(cov.Path{"a", "c", "f", "l", "v"}, coverage(fileA, span1, span2))
// (a)
// ┏━━━━━━━━┻━━━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━┻━━┓
// [0,1](d) (e)[3] [1,2](f) (g)[0,3]
// ╰─╮ ╭─┴─╮ ╭─╯ ╭─┴─╮
// i j k l n o
// ╭╯
// v
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v}} g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{[0,3]}}}`)
t.Log("Add 'x' with the coverage [1,2]")
tree.Add(cov.Path{"a", "c", "f", "l", "x"}, coverage(fileA, span1, span2))
// (a)
// ┏━━━━━━━━┻━━━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━┻━━┓
// [0,1](d) (e)[3] [1,2](f) (g)[0,3]
// ╰─╮ ╭─┴─╮ ╭─╯ ╭─┴─╮
// i j k l n o
// ╭┴╮
// v x
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{[0,3]}}}`)
t.Log("Add 'z' with the coverage [2]")
tree.Add(cov.Path{"a", "c", "g", "n", "z"}, coverage(fileA, span2))
// (a)
// ┏━━━━━━━━┻━━━━━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━━━┻━━━━┓
// [0,1](d) (e)[3] [1,2](f) (g)
// ╰─╮ ╭─┴─╮ ╭─╯ ┏━┻━┓
// i j k l [2](n) (o)[0,3]
// ╭┴╮ ╭╯
// v x z
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n: {z} o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{[0,1]} e:{[3]}} c:{f:{[1,2]} g:{n:{[2]} o:{[0,3]}}}}`)
tree.Optimize()
// (a)
// ┏━━━━━━━━┻━━━━━━━━━━━━┓
// (b) (c)
// ┏━━━┻━━━┓ ┏━━━━┻━━━━┓
// <0>(d) (e)[3] <2>(f) (g)
// ╰─╮ ╭─┴─╮ ╭─╯ ┏━┻━┓
// i j k l [2](n) (o)<1>
// ╭┴╮ ╭╯
// v x z
checkSpans(t, tree.Spans(), span0, span1, span2, span3)
checkGroups(t, tree.FileSpanGroups(fileA), map[cov.SpanGroupID]cov.SpanGroup{
0: cov.SpanGroup{Spans: spans(0, 1)},
1: cov.SpanGroup{Spans: spans(0, 3)},
2: cov.SpanGroup{Spans: spans(1, 2)},
})
checkTests(t, tree, `{a:{b:{d:{i} e:{j k}} c:{f:{l:{v x}} g:{n: {z} o}}}}`)
checkCoverage(t, tree, fileA, `a:{b:{d:{<0>} e:{[3]}} c:{f:{<2>} g:{n:{[2]} o:{<1>}}}}`)
}
func TestTreeOptInvertForCommon(t *testing.T) {
tree := &cov.Tree{}
tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "c"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "d"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "e"}, coverage(fileA, span1))
tree.Add(cov.Path{"a", "f"}, coverage(fileA, span1))
tree.Add(cov.Path{"a", "g"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "h"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "i"}, coverage(fileA, span0))
// (a)
// ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
// (b) (c) (d) (e) (f) (g) (h) (i)
// [0] [0] [0] [1] [1] [0] [0] [0]
checkSpans(t, tree.Spans(), span0, span1)
checkTests(t, tree, `{a:{b c d e f g h i}}`)
checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[0]} h:{[0]} i:{[0]}}`)
tree.Optimize()
// [0]
// (a)
// ╭───┬───┬───┲━┻━┱───┬───┬───╮
// b c d ┏┛ ┗┓ g h i
// (e) (f)
// <0> <0>
checkSpans(t, tree.Spans(), span0, span1)
checkGroups(t, tree.FileSpanGroups(fileA), map[cov.SpanGroupID]cov.SpanGroup{
0: cov.SpanGroup{Spans: spans(0, 1)},
})
checkTests(t, tree, `{a:{b c d e f g h i}}`)
checkCoverage(t, tree, fileA, `a:{[0] e:{<0>} f:{<0>}}`)
}
func TestTreeOptDontInvertForCommon(t *testing.T) {
tree := &cov.Tree{}
tree.Add(cov.Path{"a", "b"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "c"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "d"}, coverage(fileA, span0))
tree.Add(cov.Path{"a", "e"}, coverage(fileA, span1))
tree.Add(cov.Path{"a", "f"}, coverage(fileA, span1))
tree.Add(cov.Path{"a", "g"}, coverage(fileA, span2))
tree.Add(cov.Path{"a", "h"}, coverage(fileA, span2))
tree.Add(cov.Path{"a", "i"}, coverage(fileA, span2))
// (a)
// ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
// (b) (c) (d) (e) (f) (g) (h) (i)
// [0] [0] [0] [1] [1] [2] [2] [2]
checkSpans(t, tree.Spans(), span0, span1, span2)
checkTests(t, tree, `{a:{b c d e f g h i}}`)
checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[2]} h:{[2]} i:{[2]}}`)
tree.Optimize()
// (a)
// ┏━━━┳━━━┳━━━┳━┻━┳━━━┳━━━┳━━━┓
// (b) (c) (d) (e) (f) (g) (h) (i)
// [0] [0] [0] [1] [1] [2] [2] [2]
checkSpans(t, tree.Spans(), span0, span1, span2)
checkTests(t, tree, `{a:{b c d e f g h i}}`)
checkCoverage(t, tree, fileA, `a:{b:{[0]} c:{[0]} d:{[0]} e:{[1]} f:{[1]} g:{[2]} h:{[2]} i:{[2]}}`)
}
func checkSpans(t *testing.T, got []cov.Span, expect ...cov.Span) {
if !reflect.DeepEqual(got, expect) {
t.Errorf("Spans not as expected.\nGot: %+v\nExpect: %+v", got, expect)
}
}
func checkGroups(t *testing.T, got, expect map[cov.SpanGroupID]cov.SpanGroup) {
if !reflect.DeepEqual(got, expect) {
t.Errorf("SpanGroupss not as expected.\nGot: %+v\nExpect: %+v", got, expect)
}
}
func checkTests(t *testing.T, tree *cov.Tree, expect string) {
g, e := tree.Tests().String(tree.Strings()), expect
if tg, te := trimWS(g), trimWS(e); tg != te {
t.Errorf("Tests not as expected.\nGot:\n%v\nExpect:\n%v\n------\nGot: %v\nExpect: %v", g, e, tg, te)
}
}
func checkCoverage(t *testing.T, tree *cov.Tree, file string, expect string) {
g, e := tree.FileCoverage(file).String(tree.Tests(), tree.Strings()), expect
if tg, te := trimWS(g), trimWS(e); tg != te {
t.Errorf("Coverage not as expected.\nGot:\n%v\nExpect:\n%v\n------\nGot: %v\nExpect: %v", g, e, tg, te)
}
}
func trimWS(s string) string {
s = strings.ReplaceAll(s, " ", "")
s = strings.ReplaceAll(s, "\n", "")
return s
}
func coverage(file string, spans ...cov.Span) *cov.Coverage {
return &cov.Coverage{
[]cov.File{
cov.File{
Path: file,
Covered: spans,
},
},
}
}
func spans(ids ...cov.SpanID) cov.SpanSet {
out := make(cov.SpanSet, len(ids))
for _, id := range ids {
out[id] = struct{}{}
}
return out
}
func TestTreeEncodeDecode(t *testing.T) {
orig := &cov.Tree{}
orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
orig.Add(cov.Path{"a", "b", "d", "i"}, coverage(fileA, span0, span1))
orig.Add(cov.Path{"a", "b", "e"}, coverage(fileA, span0, span1, span2))
orig.Add(cov.Path{"a", "c", "g", "n"}, coverage(fileB, span0, span3))
orig.Add(cov.Path{"a", "c", "g", "o"}, coverage(fileB, span0, span3))
orig.Add(cov.Path{"a", "c", "f"}, coverage(fileA, span1))
orig.Add(cov.Path{"a", "b", "e", "j"}, coverage(fileC, span3))
orig.Add(cov.Path{"a", "b", "e", "k"}, coverage(fileA, span3))
orig.Add(cov.Path{"a", "c", "f", "l", "v"}, coverage(fileA, span1, span2))
orig.Add(cov.Path{"a", "c", "f", "l", "x"}, coverage(fileA, span1, span2))
orig.Add(cov.Path{"a", "c", "g", "n", "z"}, coverage(fileC, span2))
orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
orig.Add(cov.Path{"a", "b"}, coverage(fileA, span0, span1))
origJSON := orig.JSON("revision goes here")
read, revision, err := cov.ReadJSON(strings.NewReader(origJSON))
if err != nil {
t.Fatalf("cov.ReadJSON() failed with: %v", err)
}
readJSON := read.JSON(revision)
if origJSON != readJSON {
t.Fatalf("Encode -> Decode -> Encode produced different results:\nOriginal:\n\n%v\n\nRead:\n\n%v", origJSON, readJSON)
}
}

276
tools/src/cov/import.go Normal file
View File

@ -0,0 +1,276 @@
// 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 cov
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"dawn.googlesource.com/dawn/tools/src/fileutils"
)
// File describes the coverage spans in a single source file.
type File struct {
Path string
Covered SpanList // Spans with coverage
Uncovered SpanList // Compiled spans without coverage
}
// Coverage describes the coverage spans for all the source files for a single
// process invocation.
type Coverage struct {
Files []File
}
// Env holds the environment settings for performing coverage processing.
type Env struct {
LLVMBin string // path to the LLVM bin directory
Binary string // path to the executable binary
TurboCov string // path to turbo-cov (optional)
}
// RuntimeEnv returns the environment variable key=value pair for setting
// LLVM_PROFILE_FILE to coverageFile
func RuntimeEnv(env []string, coverageFile string) string {
return "LLVM_PROFILE_FILE=" + coverageFile
}
// AllSourceFiles returns a *Coverage containing all the source files without
// coverage data. This populates the coverage view with files even if they
// didn't get compiled.
func (e Env) AllSourceFiles() *Coverage {
var ignorePaths = map[string]bool{
//
}
projectRoot := fileutils.DawnRoot()
// Gather all the source files to include them even if there is no coverage
// information produced for these files. This highlights files that aren't
// even compiled.
cov := Coverage{}
allFiles := map[string]struct{}{}
filepath.Walk(filepath.Join(projectRoot, "src"), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(projectRoot, path)
if err != nil || ignorePaths[rel] {
return filepath.SkipDir
}
if !info.IsDir() {
switch filepath.Ext(path) {
case ".h", ".c", ".cc", ".cpp", ".hpp":
if _, seen := allFiles[rel]; !seen {
cov.Files = append(cov.Files, File{Path: rel})
}
}
}
return nil
})
return &cov
}
// 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) {
llvmProfdataExe := filepath.Join(e.LLVMBin, "llvm-profdata"+fileutils.ExeExt)
llvmCovExe := filepath.Join(e.LLVMBin, "llvm-cov"+fileutils.ExeExt)
profdata := profrawPath + ".profdata"
if err := exec.Command(
llvmProfdataExe,
"merge",
"-sparse",
profrawPath,
"-output",
profdata).Run(); err != nil {
return nil, fmt.Errorf("llvm-profdata errored: %w", err)
}
defer os.Remove(profdata)
if e.TurboCov == "" {
data, err := exec.Command(
llvmCovExe,
"export",
e.Binary,
"-instr-profile="+profdata,
"-format=text",
"-skip-expansions",
"-skip-functions").Output()
if err != nil {
return nil, fmt.Errorf("llvm-cov errored: %v\n%v", string(err.(*exec.ExitError).Stderr), err)
}
cov, err := e.parseCov(data)
if err != nil {
return nil, fmt.Errorf("failed to parse coverage json data: %w", err)
}
return cov, nil
}
data, err := exec.Command(e.TurboCov, e.Binary, profdata).Output()
if err != nil {
return nil, fmt.Errorf("turbo-cov errored: %v\n%v", string(err.(*exec.ExitError).Stderr), err)
}
cov, err := e.parseTurboCov(data)
if err != nil {
return nil, fmt.Errorf("failed to process turbo-cov output: %w", err)
}
return cov, nil
}
func appendSpan(spans []Span, span Span) []Span {
if c := len(spans); c > 0 && spans[c-1].End == span.Start {
spans[c-1].End = span.End
} else {
spans = append(spans, span)
}
return spans
}
// 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
}
projectRoot := fileutils.DawnRoot()
c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
for _, f := range root.Data[0].Files {
relpath, err := filepath.Rel(projectRoot, 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))}
if covered := f.Segments[sIdx][2].(float64) != 0; covered {
file.Covered = appendSpan(file.Covered, Span{start, end})
} else {
file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
}
}
if len(file.Covered) > 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)
}
projectRoot := fileutils.DawnRoot()
numFiles := u32()
c := &Coverage{Files: make([]File, 0, numFiles)}
for i := 0; i < int(numFiles); i++ {
path := str()
relpath, err := filepath.Rel(projectRoot, 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].covered {
if segments[sIdx].count > 0 {
file.Covered = appendSpan(file.Covered, Span{start, end})
} else {
file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
}
}
}
if len(file.Covered) > 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

View File

@ -0,0 +1,171 @@
// 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 cov
import (
"log"
"sort"
"sync"
)
// 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()
o := optimizer{}
for idx, tc := range file.tcm {
o.invertForCommon(tc, &t.testRoot.children[idx])
}
o.createGroups(file)
}()
}
wg.Wait()
}
type optimizer struct{}
// createGroups looks for common SpanSets, and creates indexable span groups
// which are then used instead.
func (o *optimizer) createGroups(f *treeFile) {
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
}
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},
})
}
// Sort by number of spans in each sets starting with the largest.
sort.Slice(spansets, func(i, j int) bool {
a, b := spansets[i].set, spansets[j].set
switch {
case len(a) > len(b):
return true
case len(a) < len(b):
return false
}
return a.List().Compare(b.List()) == -1 // Just to keep output stable
})
// Assign IDs now that we have stable order.
for i := range spansets {
spansets[i].id = SpanGroupID(i)
}
// 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.removeAll(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
}
})
}
// invertCommon looks for tree nodes with the majority of the child nodes with
// the same spans. This span is promoted up to the parent, and the children
// have the span inverted.
func (o *optimizer) invertForCommon(tc *TestCoverage, t *Test) {
wg := sync.WaitGroup{}
wg.Add(len(tc.Children))
for id, child := range tc.Children {
id, child := id, child
go func() {
defer wg.Done()
o.invertForCommon(child, &t.children[id])
}()
}
wg.Wait()
counts := map[SpanID]int{}
for _, child := range tc.Children {
for span := range child.Spans {
counts[span] = counts[span] + 1
}
}
for span, count := range counts {
if count > len(t.children)/2 {
tc.Spans = tc.Spans.invert(span)
for _, idx := range t.indices {
child := tc.Children.index(idx)
child.Spans = child.Spans.invert(span)
if child.deletable() {
delete(tc.Children, idx)
}
}
}
}
}

View File

@ -0,0 +1,598 @@
// 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 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(`{`)
spansByID := map[SpanID]Span{}
for span, id := range t.spans {
spansByID[id] = span
}
// 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(strings.ReplaceAll(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(spansByID, 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) writeSpanJSON(span Span, sb *strings.Builder) {
sb.WriteString(fmt.Sprintf("[%v,%v,%v,%v]",
span.Start.Line, span.Start.Column,
span.End.Line, span.End.Column))
}
func (t *Tree) writeFilesJSON(spansByID map[SpanID]Span, 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]
uncovered := append(SpanList{}, file.allSpans...)
for id := range t.allSpans(file, file.tcm) {
uncovered.Remove(spansByID[id])
}
if i > 0 {
sb.WriteString(`,`)
}
sb.WriteString(`"`)
sb.WriteString(path)
sb.WriteString(`":`)
sb.WriteString(`{`)
if totalLines := file.allSpans.NumLines(); totalLines > 0 {
uncoveredLines := uncovered.NumLines()
percentage := 1.0 - (float64(uncoveredLines) / float64(totalLines))
sb.WriteString(`"p":`)
sb.WriteString(fmt.Sprintf("%v", percentage))
sb.WriteString(`,`)
}
sb.WriteString(`"g":`)
t.writeSpanGroupsJSON(file.spangroups, sb)
sb.WriteString(`,"u":`)
t.writeUncoveredJSON(file, uncovered, 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) writeUncoveredJSON(tf *treeFile, uncovered SpanList, sb *strings.Builder) {
sb.WriteString(`[`)
for i, span := range uncovered {
if i > 0 {
sb.WriteString(`,`)
}
t.writeSpanJSON(span, sb)
}
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
}
p.populateAllSpans(&p.tree)
return &p.tree, p.revision, nil
}
// populateAllSpans() adds all the coverage spans to each treeFile.allSpans.
func (p *parser) populateAllSpans(tree *Tree) {
spansByID := map[SpanID]Span{}
for span, id := range tree.spans {
spansByID[id] = span
}
for _, file := range tree.files {
for spanID := range tree.allSpans(file, file.tcm) {
span := spansByID[spanID]
file.allSpans.Add(span)
}
}
}
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 "p":
p.double()
case "g":
file.spangroups = p.parseSpanGroups()
case "c":
p.parseCoverageMap(file.tcm)
case "u":
p.parseUncovered(file)
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) parseUncovered(tf *treeFile) {
p.array(func(int) {
tf.allSpans.Add(p.parseSpan())
})
}
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) double() float64 {
sb := strings.Builder{}
for {
if c := p.peek(); c != '.' && (c < '0' || c > '9') {
break
}
sb.WriteByte(p.next())
}
if sb.Len() == 0 {
p.fail("Expected double, got '%c'", p.peek())
return 0
}
f, err := strconv.ParseFloat(sb.String(), 64)
if err != nil {
p.fail("Failed to parse double: %v", err)
return 0
}
return f
}
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)
}
}

176
tools/src/cov/span.go Normal file
View File

@ -0,0 +1,176 @@
// 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 cov
import (
"fmt"
"sort"
)
// 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)
}
// Compare returns -1 if l comes before o, 1 if l comes after o, otherwise 0.
func (l Location) Compare(o Location) int {
switch {
case l.Line < o.Line:
return -1
case l.Line > o.Line:
return 1
case l.Column < o.Column:
return -1
case l.Column > o.Column:
return 1
}
return 0
}
// Before returns true if l comes before o.
func (l Location) Before(o Location) bool { return l.Compare(o) == -1 }
// After returns true if l comes after o.
func (l Location) After(o Location) bool { return l.Compare(o) == 1 }
// Span describes a start and end interval in a source file.
type Span struct {
Start, End Location
}
func (s Span) String() string {
return fmt.Sprintf("%v-%v", s.Start, s.End)
}
// Compare returns -1 if l comes before o, 1 if l comes after o, otherwise 0.
func (s Span) Compare(o Span) int {
switch {
case s.Start.Before(o.Start):
return -1
case o.Start.Before(s.Start):
return 1
case s.End.Before(o.End):
return -1
case o.End.Before(s.End):
return 1
}
return 0
}
// Before returns true if span s comes before o.
func (s Span) Before(o Span) bool { return s.Compare(o) == -1 }
// Inside returns true if span s fits entirely inside o.
func (s Span) Inside(o Span) bool { return s.Start.Compare(o.Start) >= 0 && s.End.Compare(o.End) <= 0 }
// SpanList is a sorted list of spans. Use SpanList.Add() to insert new spans.
type SpanList []Span
// Add adds the Span to the SpanList, merging and expanding overlapping spans.
func (l *SpanList) Add(s Span) {
// [===]
// [0] [1] | idxStart: 2 | idxEnd: 2
// [0] [1] | idxStart: 0 | idxEnd: 0
// [ 0 ] [ 1 ] [ 2 ] [ 3 ] | idxStart: 1 | idxEnd: 2
// [0] [1] [2] [3] [4] | idxStart: 2 | idxEnd: 2
idxStart := sort.Search(len(*l), func(i int) bool { return (*l)[i].End.Compare(s.Start) >= 0 })
if idxStart < len(*l) && s.Inside((*l)[idxStart]) {
return // No change.
}
idxEnd := sort.Search(len(*l), func(i int) bool { return (*l)[i].Start.Compare(s.End) > 0 })
if idxStart < idxEnd {
if first := (*l)[idxStart]; first.Start.Before(s.Start) {
s.Start = first.Start
}
if last := (*l)[idxEnd-1]; last.End.After(s.End) {
s.End = last.End
}
}
merged := append(SpanList{}, (*l)[:idxStart]...)
merged = append(merged, s)
merged = append(merged, (*l)[idxEnd:]...)
*l = merged
}
// Remove cuts out the Span from the SpanList, removing and trimming overlapping
// spans.
func (l *SpanList) Remove(s Span) {
if s.Start == s.End {
return // zero length == no split.
}
// [===]
// [0] [1] | idxStart: 2 | idxEnd: 2
// [0] [1] | idxStart: 0 | idxEnd: 0
// [ 0 ] [ 1 ] [ 2 ] [ 3 ] | idxStart: 1 | idxEnd: 2
// [0] [1] [2] [3] [4] | idxStart: 2 | idxEnd: 2
idxStart := sort.Search(len(*l), func(i int) bool { return (*l)[i].End.Compare(s.Start) > 0 })
idxEnd := sort.Search(len(*l), func(i int) bool { return (*l)[i].Start.Compare(s.End) >= 0 })
merged := append(SpanList{}, (*l)[:idxStart]...)
if idxStart < idxEnd {
first, last := (*l)[idxStart], (*l)[idxEnd-1]
if first.Start.Compare(s.Start) < 0 {
merged = append(merged, Span{first.Start, s.Start})
}
if last.End.Compare(s.End) > 0 {
merged = append(merged, Span{s.End, last.End})
}
}
merged = append(merged, (*l)[idxEnd:]...)
*l = merged
}
// Compare returns -1 if l comes before o, 1 if l comes after o, otherwise 0.
func (l SpanList) Compare(o SpanList) int {
switch {
case len(l) < len(o):
return -1
case len(l) > len(o):
return 1
}
for i, a := range l {
switch a.Compare(o[i]) {
case -1:
return -1
case 1:
return 1
}
}
return 0
}
// NumLines returns the total number of lines covered by all spans in the list.
func (l SpanList) NumLines() int {
seen := map[int]struct{}{}
for _, span := range l {
for s := span.Start.Line; s <= span.End.Line; s++ {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
}
}
}
return len(seen)
}

169
tools/src/cov/span_test.go Normal file
View File

@ -0,0 +1,169 @@
// 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 cov_test
import (
"reflect"
"testing"
"dawn.googlesource.com/dawn/tools/src/cov"
)
func TestSpanListAddNoMerge(t *testing.T) {
l := cov.SpanList{}
l.Add(span(3, 1, 3, 5))
checkSpanList(t, l, span(3, 1, 3, 5))
l.Add(span(4, 1, 4, 5))
checkSpanList(t, l, span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Add(span(2, 1, 2, 5))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
}
func TestSpanListAddExpand(t *testing.T) {
l := cov.SpanList{span(1, 1, 1, 5), span(5, 4, 5, 7), span(9, 1, 9, 5)}
// Expand front (column)
l.Add(span(5, 1, 5, 5))
checkSpanList(t, l, span(1, 1, 1, 5), span(5, 1, 5, 7), span(9, 1, 9, 5))
// Expand back (column)
l.Add(span(5, 5, 5, 9))
checkSpanList(t, l, span(1, 1, 1, 5), span(5, 1, 5, 9), span(9, 1, 9, 5))
// Expand front (line)
l.Add(span(4, 3, 5, 2))
checkSpanList(t, l, span(1, 1, 1, 5), span(4, 3, 5, 9), span(9, 1, 9, 5))
// Expand back (line)
l.Add(span(5, 4, 6, 3))
checkSpanList(t, l, span(1, 1, 1, 5), span(4, 3, 6, 3), span(9, 1, 9, 5))
// Expand front (touching)
l.Add(span(4, 2, 4, 3))
checkSpanList(t, l, span(1, 1, 1, 5), span(4, 2, 6, 3), span(9, 1, 9, 5))
// Expand back (touching)
l.Add(span(6, 3, 6, 4))
checkSpanList(t, l, span(1, 1, 1, 5), span(4, 2, 6, 4), span(9, 1, 9, 5))
}
func TestSpanListAddMergeOverlap(t *testing.T) {
l := cov.SpanList{span(1, 1, 1, 5), span(5, 4, 5, 7), span(9, 1, 9, 5)}
l.Add(span(1, 3, 5, 6))
checkSpanList(t, l, span(1, 1, 5, 7), span(9, 1, 9, 5))
l.Add(span(5, 5, 9, 3))
checkSpanList(t, l, span(1, 1, 9, 5))
}
func TestSpanListAddMergeTouching(t *testing.T) {
l := cov.SpanList{span(1, 1, 1, 5), span(5, 4, 5, 7), span(9, 1, 9, 5)}
l.Add(span(1, 5, 9, 1))
checkSpanList(t, l, span(1, 1, 9, 5))
}
func TestSpanListRemoveNothing(t *testing.T) {
l := cov.SpanList{span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5)}
l.Remove(span(1, 1, 2, 1))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(2, 5, 3, 1))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(3, 5, 4, 1))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(4, 5, 10, 10))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
}
func TestSpanListRemoveWhole(t *testing.T) {
l := cov.SpanList{span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5)}
l.Remove(span(3, 1, 3, 5))
checkSpanList(t, l, span(2, 1, 2, 5), span(4, 1, 4, 5))
l.Remove(span(1, 1, 3, 3))
checkSpanList(t, l, span(4, 1, 4, 5))
l.Remove(span(3, 1, 4, 5))
checkSpanList(t, l)
}
func TestSpanListRemoveZeroLength(t *testing.T) {
l := cov.SpanList{span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5)}
l.Remove(span(3, 1, 3, 1))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(3, 5, 3, 5))
checkSpanList(t, l, span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
}
func TestSpanListRemoveTrim(t *testing.T) {
l := cov.SpanList{span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5)}
l.Remove(span(2, 1, 2, 2))
checkSpanList(t, l, span(2, 2, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(2, 4, 2, 5))
checkSpanList(t, l, span(2, 2, 2, 4), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(2, 5, 3, 2))
checkSpanList(t, l, span(2, 2, 2, 4), span(3, 2, 3, 5), span(4, 1, 4, 5))
l.Remove(span(3, 4, 3, 5))
checkSpanList(t, l, span(2, 2, 2, 4), span(3, 2, 3, 4), span(4, 1, 4, 5))
l.Remove(span(4, 1, 4, 2))
checkSpanList(t, l, span(2, 2, 2, 4), span(3, 2, 3, 4), span(4, 2, 4, 5))
l.Remove(span(4, 4, 4, 5))
checkSpanList(t, l, span(2, 2, 2, 4), span(3, 2, 3, 4), span(4, 2, 4, 4))
}
func TestSpanListRemoveSplit(t *testing.T) {
l := cov.SpanList{span(2, 1, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5)}
l.Remove(span(2, 2, 2, 3))
checkSpanList(t, l, span(2, 1, 2, 2), span(2, 3, 2, 5), span(3, 1, 3, 5), span(4, 1, 4, 5))
l.Remove(span(3, 2, 3, 4))
checkSpanList(t, l, span(2, 1, 2, 2), span(2, 3, 2, 5), span(3, 1, 3, 2), span(3, 4, 3, 5), span(4, 1, 4, 5))
l.Remove(span(4, 2, 4, 2)) // zero length == no split
checkSpanList(t, l, span(2, 1, 2, 2), span(2, 3, 2, 5), span(3, 1, 3, 2), span(3, 4, 3, 5), span(4, 1, 4, 5))
}
func span(startLine, startColumn, endLine, endColumn int) cov.Span {
return cov.Span{
Start: cov.Location{Line: startLine, Column: startColumn},
End: cov.Location{Line: endLine, Column: endColumn},
}
}
func checkSpanList(t *testing.T, got cov.SpanList, expect ...cov.Span) {
if expect == nil {
expect = cov.SpanList{}
}
if !reflect.DeepEqual(got, cov.SpanList(expect)) {
t.Errorf("SpanList not as expected.\nGot:\n%v\nExpect:\n%v", got, cov.SpanList(expect))
}
}

486
tools/src/cov/tree.go Normal file
View File

@ -0,0 +1,486 @@
// 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 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 "), " ")
}

View File

@ -22,6 +22,8 @@ import (
"os" "os"
) )
const ExeExt = ""
// IsExe returns true if the file at path is an executable // IsExe returns true if the file at path is an executable
func IsExe(path string) bool { func IsExe(path string) bool {
s, err := os.Stat(path) s, err := os.Stat(path)

View File

@ -15,6 +15,8 @@
// Package fileutils contains utility functions for files // Package fileutils contains utility functions for files
package fileutils package fileutils
const ExeExt = ".exe"
// IsExe returns true if the file at path is an executable // IsExe returns true if the file at path is an executable
func IsExe(path string) bool { func IsExe(path string) bool {
return true return true