Coverage: Improve uncovered visualizations

List all files, including those that have no coverage.
Emit spans that were compiled, but not covered.
Emit a % coverage per file.

Bug: b/152192800
Fixes: b/153182184
Change-Id: I31c831273a8d3ee89c7ce0737b6e05398ff4f51b
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/43491
Kokoro-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Nicolas Capens <nicolascapens@google.com>
Tested-by: Ben Clayton <bclayton@google.com>
diff --git a/tests/regres/cov/coverage_test.go b/tests/regres/cov/coverage_test.go
index 95aca12..c6fb2cd 100644
--- a/tests/regres/cov/coverage_test.go
+++ b/tests/regres/cov/coverage_test.go
@@ -334,8 +334,8 @@
 	return &cov.Coverage{
 		[]cov.File{
 			cov.File{
-				Path:  file,
-				Spans: spans,
+				Path:    file,
+				Covered: spans,
 			},
 		},
 	}
diff --git a/tests/regres/cov/import.go b/tests/regres/cov/import.go
index 100c3cb..1328b50 100644
--- a/tests/regres/cov/import.go
+++ b/tests/regres/cov/import.go
@@ -18,7 +18,6 @@
 	"bytes"
 	"encoding/binary"
 	"encoding/json"
-	"fmt"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -28,60 +27,20 @@
 	"../llvm"
 )
 
-// Location describes a single line-column position in a source file.
-type Location struct {
-	Line, Column int
+var ignorePaths = map[string]bool{
+	"src/Common":   true,
+	"src/Main":     true,
+	"src/OpenGL":   true,
+	"src/Renderer": true,
+	"src/Shader":   true,
+	"src/System":   true,
 }
 
-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
-	}
-	return 0
-}
-
-// Before returns true if l comes before o.
-func (l Location) Before(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 }
-
 // File describes the coverage spans in a single source file.
 type File struct {
-	Path  string
-	Spans []Span
+	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
@@ -148,9 +107,45 @@
 	if err != nil {
 		return nil, cause.Wrap(err, "Couldn't process turbo-cov output")
 	}
+
+	// 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.
+	allFiles := map[string]struct{}{}
+	for _, file := range cov.Files {
+		allFiles[file.Path] = struct{}{}
+	}
+	filepath.Walk(filepath.Join(e.RootDir, "src"), func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		rel, err := filepath.Rel(e.RootDir, 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, 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) {
@@ -189,16 +184,13 @@
 		for sIdx := 0; sIdx+1 < len(f.Segments); sIdx++ {
 			start := Location{(int)(f.Segments[sIdx][0].(float64)), (int)(f.Segments[sIdx][1].(float64))}
 			end := Location{(int)(f.Segments[sIdx+1][0].(float64)), (int)(f.Segments[sIdx+1][1].(float64))}
-			covered := f.Segments[sIdx][2].(float64) != 0
-			if covered {
-				if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
-					file.Spans[c-1].End = end
-				} else {
-					file.Spans = append(file.Spans, Span{start, end})
-				}
+			if covered := f.Segments[sIdx][2].(float64) != 0; covered {
+				file.Covered.Add(Span{start, end})
+			} else {
+				file.Uncovered.Add(Span{start, end})
 			}
 		}
-		if len(file.Spans) > 0 {
+		if len(file.Covered) > 0 {
 			c.Files = append(c.Files, file)
 		}
 	}
@@ -257,16 +249,16 @@
 		for sIdx := 0; sIdx+1 < len(segments); sIdx++ {
 			start := segments[sIdx].location
 			end := segments[sIdx+1].location
-			if segments[sIdx].count > 0 {
-				if c := len(file.Spans); c > 0 && file.Spans[c-1].End == start {
-					file.Spans[c-1].End = end
+			if segments[sIdx].covered {
+				if segments[sIdx].count > 0 {
+					file.Covered.Add(Span{start, end})
 				} else {
-					file.Spans = append(file.Spans, Span{start, end})
+					file.Uncovered.Add(Span{start, end})
 				}
 			}
 		}
 
-		if len(file.Spans) > 0 {
+		if len(file.Covered) > 0 {
 			c.Files = append(c.Files, file)
 		}
 	}
diff --git a/tests/regres/cov/parser.go b/tests/regres/cov/serialization.go
similarity index 84%
rename from tests/regres/cov/parser.go
rename to tests/regres/cov/serialization.go
index 925e93c..76bd2e1 100644
--- a/tests/regres/cov/parser.go
+++ b/tests/regres/cov/serialization.go
@@ -50,6 +50,11 @@
 	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 + `"`)
 
@@ -75,7 +80,7 @@
 
 	// write the files
 	sb.WriteString(`,"f":`)
-	t.writeFilesJSON(sb)
+	t.writeFilesJSON(spansByID, sb)
 
 	sb.WriteString(`}`)
 	return sb.String()
@@ -125,7 +130,13 @@
 	sb.WriteString(`]`)
 }
 
-func (t *Tree) writeFilesJSON(sb *strings.Builder) {
+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)
@@ -135,6 +146,20 @@
 	sb.WriteString(`{`)
 	for i, path := range paths {
 		file := t.files[path]
+
+		uncovered := append(SpanList{}, file.allSpans...)
+		file.tcm.traverse(func(tc *TestCoverage) {
+			for id := range tc.Spans {
+				uncovered.Remove(spansByID[id])
+			}
+		})
+
+		percentage := 0.0
+		if totalLines := file.allSpans.NumLines(); totalLines > 0 {
+			uncoveredLines := uncovered.NumLines()
+			percentage = 1.0 - (float64(uncoveredLines) / float64(totalLines))
+		}
+
 		if i > 0 {
 			sb.WriteString(`,`)
 		}
@@ -142,8 +167,12 @@
 		sb.WriteString(path)
 		sb.WriteString(`":`)
 		sb.WriteString(`{`)
-		sb.WriteString(`"g":`)
+		sb.WriteString(`"p":`)
+		sb.WriteString(fmt.Sprintf("%v", percentage))
+		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(`}`)
@@ -190,6 +219,17 @@
 	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 {
@@ -326,10 +366,14 @@
 	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)
 			}
@@ -371,6 +415,12 @@
 	})
 }
 
+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 {
@@ -505,6 +555,26 @@
 	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...)
diff --git a/tests/regres/cov/span.go b/tests/regres/cov/span.go
new file mode 100644
index 0000000..9545936
--- /dev/null
+++ b/tests/regres/cov/span.go
@@ -0,0 +1,168 @@
+// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cov
+
+import (
+	"fmt"
+	"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 }
+
+// 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 })
+	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)
+}
diff --git a/tests/regres/cov/span_test.go b/tests/regres/cov/span_test.go
new file mode 100644
index 0000000..f11b280
--- /dev/null
+++ b/tests/regres/cov/span_test.go
@@ -0,0 +1,169 @@
+// 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_test
+
+import (
+	"reflect"
+	"testing"
+
+	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))
+	}
+}
diff --git a/tests/regres/cov/tree.go b/tests/regres/cov/tree.go
index 2f54a82..b94de68 100644
--- a/tests/regres/cov/tree.go
+++ b/tests/regres/cov/tree.go
@@ -23,6 +23,7 @@
 type treeFile struct {
 	tcm        TestCoverageMap
 	spangroups map[SpanGroupID]SpanGroup
+	allSpans   SpanList
 }
 
 func newTreeFile() *treeFile {
@@ -52,28 +53,6 @@
 	}
 }
 
-// SpanList is a list of Spans
-type SpanList []Span
-
-// 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
-}
-
 // Spans returns all the spans used by the tree
 func (t *Tree) Spans() SpanList {
 	out := make(SpanList, len(t.spans))
@@ -109,7 +88,7 @@
 	return out
 }
 
-func (t *Tree) addSpans(spans []Span) SpanSet {
+func (t *Tree) addSpans(spans SpanList) SpanSet {
 	out := make(SpanSet, len(spans))
 	for _, s := range spans {
 		id, ok := t.spans[s]
@@ -138,8 +117,15 @@
 			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.Spans)
+		spans := t.addSpans(file.Covered)
 
 		// Starting from the test root, walk down the test tree.
 		tcm, test := tf.tcm, t.testRoot