| // Copyright 2022 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| package cov | |
| import ( | |
| "bufio" | |
| "compress/zlib" | |
| "fmt" | |
| "io" | |
| "path/filepath" | |
| "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 compresses the JSON coverage tree with zlib and writes the result 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(filepath.ToSlash(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) | |
| } | |
| } |