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:
parent
f6d95d3244
commit
f8e0aac2a6
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 "), " ")
|
||||
}
|
|
@ -22,6 +22,8 @@ import (
|
|||
"os"
|
||||
)
|
||||
|
||||
const ExeExt = ""
|
||||
|
||||
// IsExe returns true if the file at path is an executable
|
||||
func IsExe(path string) bool {
|
||||
s, err := os.Stat(path)
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
// Package fileutils contains utility functions for files
|
||||
package fileutils
|
||||
|
||||
const ExeExt = ".exe"
|
||||
|
||||
// IsExe returns true if the file at path is an executable
|
||||
func IsExe(path string) bool {
|
||||
return true
|
||||
|
|
Loading…
Reference in New Issue