pkg/cover: implement function coverage calculation

This commit is contained in:
Jouni Hogander 2020-08-14 15:08:54 +03:00 committed by Dmitry Vyukov
parent 0b9318b447
commit 6f0ea384b1
4 changed files with 111 additions and 14 deletions

View File

@ -6,6 +6,7 @@ package cover
import (
"bufio"
"bytes"
"encoding/csv"
"fmt"
"html"
"html/template"
@ -41,6 +42,13 @@ type symbol struct {
end uint64
}
var CSVHeader = []string{
"Filename",
"Function",
"Covered PCs",
"Total PCs",
}
func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir string) (*ReportGenerator, error) {
rg := &ReportGenerator{
target: target,
@ -70,12 +78,18 @@ func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir
type file struct {
lines map[int]line
functions map[string]*function
totalPCs map[uint64]bool
coverPCs map[uint64]bool
totalInline map[int]bool
coverInline map[int]bool
}
type function struct {
totalPCs map[uint64]bool
coverPCs map[uint64]bool
}
type line struct {
count map[int]bool
prog int
@ -83,7 +97,23 @@ type line struct {
symbolCovered bool
}
func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
func (rg *ReportGenerator) DoHTML(buf io.Writer, progs []Prog) error {
files, err := rg.prepareFileMap(progs)
if err != nil {
return err
}
return rg.generateHTML(buf, progs, files)
}
func (rg *ReportGenerator) DoCSV(buf io.Writer, progs []Prog) error {
files, err := rg.prepareFileMap(progs)
if err != nil {
return err
}
return rg.generateCSV(buf, progs, files)
}
func (rg *ReportGenerator) prepareFileMap(progs []Prog) (map[string]*file, error) {
coveredPCs := make(map[uint64]bool)
allPCs := make(map[uint64]bool)
symbols := make(map[uint64]bool)
@ -113,10 +143,10 @@ func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
}
}
if len(allPCs) == 0 {
return fmt.Errorf("no coverage collected so far")
return nil, fmt.Errorf("no coverage collected so far")
}
if len(coveredPCs) == 0 {
return fmt.Errorf("coverage (%v) doesn't match coverage callbacks", len(allPCs))
return nil, fmt.Errorf("coverage (%v) doesn't match coverage callbacks", len(allPCs))
}
for pc, frames := range rg.pcs {
covered := coveredPCs[pc]
@ -133,6 +163,8 @@ func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
f.coverPCs[pc] = true
}
}
function := getFunction(f.functions, frame.Func)
function.totalPCs[pc] = true
if !covered {
ln := f.lines[frame.Line]
if !frame.Inline || len(ln.count) == 0 {
@ -140,10 +172,12 @@ func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
ln.symbolCovered = symbols[rg.findSymbol(pc)]
f.lines[frame.Line] = ln
}
} else {
function.coverPCs[pc] = true
}
}
}
return rg.generate(w, progs, files)
return files, nil
}
func getFile(files map[string]*file, name string) *file {
@ -151,6 +185,7 @@ func getFile(files map[string]*file, name string) *file {
if f == nil {
f = &file{
lines: make(map[int]line),
functions: make(map[string]*function),
totalPCs: make(map[uint64]bool),
coverPCs: make(map[uint64]bool),
totalInline: make(map[int]bool),
@ -161,7 +196,43 @@ func getFile(files map[string]*file, name string) *file {
return f
}
func (rg *ReportGenerator) generate(w io.Writer, progs []Prog, files map[string]*file) error {
func getFunction(functions map[string]*function, name string) *function {
f := functions[name]
if f == nil {
f = &function{
totalPCs: make(map[uint64]bool),
coverPCs: make(map[uint64]bool),
}
functions[name] = f
}
return f
}
func (rg *ReportGenerator) generateCSV(w io.Writer, progs []Prog, files map[string]*file) error {
data := [][]string{
CSVHeader,
}
for fname, file := range files {
for funcName, function := range file.functions {
line := []string{filepath.Clean(fname), funcName,
strconv.Itoa(len(function.coverPCs)),
strconv.Itoa(len(function.totalPCs))}
data = append(data, line)
}
}
writer := csv.NewWriter(w)
defer writer.Flush()
err := writer.WriteAll(data)
if err != nil {
return err
}
return nil
}
func (rg *ReportGenerator) generateHTML(w io.Writer, progs []Prog, files map[string]*file) error {
d := &templateData{
Root: new(templateDir),
}

View File

@ -9,9 +9,11 @@ package cover
import (
"bytes"
"encoding/csv"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strconv"
@ -88,7 +90,7 @@ func TestReportGenerator(t *testing.T) {
}
func testReportGenerator(t *testing.T, target *targets.Target, test Test) {
rep, err := generateReport(t, target, test)
rep, csv, err := generateReport(t, target, test)
if err != nil {
if test.Result == "" {
t.Fatalf("expected no error, but got:\n%v", err)
@ -101,6 +103,7 @@ func testReportGenerator(t *testing.T, target *targets.Target, test Test) {
if test.Result != "" {
t.Fatalf("got no error, but expected %q", test.Result)
}
checkCSVReport(t, csv)
_ = rep
}
@ -136,7 +139,7 @@ void __sanitizer_cov_trace_pc() { printf("%llu", (long long)__builtin_return_add
return bin
}
func generateReport(t *testing.T, target *targets.Target, test Test) ([]byte, error) {
func generateReport(t *testing.T, target *targets.Target, test Test) ([]byte, []byte, error) {
dir, err := ioutil.TempDir("", "syz-cover-test")
if err != nil {
t.Fatal(err)
@ -145,7 +148,7 @@ func generateReport(t *testing.T, target *targets.Target, test Test) ([]byte, er
bin := buildTestBinary(t, target, test, dir)
rg, err := MakeReportGenerator(target, bin, dir, dir)
if err != nil {
return nil, err
return nil, nil, err
}
if test.Result == "" {
var pcs []uint64
@ -175,9 +178,32 @@ func generateReport(t *testing.T, target *targets.Target, test Test) ([]byte, er
}
test.Progs = append(test.Progs, Prog{Data: "main", PCs: pcs})
}
out := new(bytes.Buffer)
if err := rg.Do(out, test.Progs); err != nil {
return nil, err
html := new(bytes.Buffer)
if err := rg.DoHTML(html, test.Progs); err != nil {
return nil, nil, err
}
csv := new(bytes.Buffer)
if err := rg.DoCSV(csv, test.Progs); err != nil {
return nil, nil, err
}
return html.Bytes(), csv.Bytes(), nil
}
func checkCSVReport(t *testing.T, CSVReport []byte) {
csvReader := csv.NewReader(bytes.NewBuffer(CSVReport))
lines, err := csvReader.ReadAll()
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(lines[0], CSVHeader) {
t.Fatalf("Heading line in CSV doesn't match %v", lines[0])
}
for _, line := range lines {
if line[1] == "main" && line[2] != "1" && line[3] != "1" {
t.Fatalf("Function coverage percentage doesn't match %v vs. %v", line[2], "100")
}
}
return out.Bytes(), nil
}

View File

@ -233,7 +233,7 @@ func (mgr *Manager) httpCoverCover(w http.ResponseWriter, r *http.Request) {
})
}
}
if err := reportGenerator.Do(w, progs); err != nil {
if err := reportGenerator.DoHTML(w, progs); err != nil {
http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
return
}

View File

@ -72,7 +72,7 @@ func main() {
}
progs := []cover.Prog{{PCs: pcs}}
buf := new(bytes.Buffer)
if err := rg.Do(buf, progs); err != nil {
if err := rg.DoHTML(buf, progs); err != nil {
failf("%v", err)
}
fn, err := osutil.TempFile("syz-cover")