You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
599 lines
12 KiB
599 lines
12 KiB
// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package cov
|
|
|
|
import (
|
|
"bufio"
|
|
"compress/zlib"
|
|
"fmt"
|
|
"io"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ReadJSON parses the JSON Tree from r.
|
|
func ReadJSON(r io.Reader) (*Tree, string, error) {
|
|
p := parser{r: bufio.NewReader(r)}
|
|
return p.parse()
|
|
}
|
|
|
|
// Encode zlib encodes the JSON coverage tree to w.
|
|
func (t *Tree) Encode(revision string, w io.Writer) error {
|
|
t.Optimize()
|
|
|
|
zw := zlib.NewWriter(w)
|
|
|
|
_, err := zw.Write([]byte(t.JSON(revision)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return zw.Close()
|
|
}
|
|
|
|
// JSON returns the full test tree serialized to JSON.
|
|
func (t *Tree) JSON(revision string) string {
|
|
sb := &strings.Builder{}
|
|
sb.WriteString(`{`)
|
|
|
|
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(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)
|
|
}
|
|
}
|