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 ( import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/csv"
"fmt" "fmt"
"html" "html"
"html/template" "html/template"
@ -41,6 +42,13 @@ type symbol struct {
end uint64 end uint64
} }
var CSVHeader = []string{
"Filename",
"Function",
"Covered PCs",
"Total PCs",
}
func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir string) (*ReportGenerator, error) { func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir string) (*ReportGenerator, error) {
rg := &ReportGenerator{ rg := &ReportGenerator{
target: target, target: target,
@ -70,12 +78,18 @@ func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir
type file struct { type file struct {
lines map[int]line lines map[int]line
functions map[string]*function
totalPCs map[uint64]bool totalPCs map[uint64]bool
coverPCs map[uint64]bool coverPCs map[uint64]bool
totalInline map[int]bool totalInline map[int]bool
coverInline map[int]bool coverInline map[int]bool
} }
type function struct {
totalPCs map[uint64]bool
coverPCs map[uint64]bool
}
type line struct { type line struct {
count map[int]bool count map[int]bool
prog int prog int
@ -83,7 +97,23 @@ type line struct {
symbolCovered bool 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) coveredPCs := make(map[uint64]bool)
allPCs := make(map[uint64]bool) allPCs := make(map[uint64]bool)
symbols := 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 { 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 { 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 { for pc, frames := range rg.pcs {
covered := coveredPCs[pc] covered := coveredPCs[pc]
@ -133,6 +163,8 @@ func (rg *ReportGenerator) Do(w io.Writer, progs []Prog) error {
f.coverPCs[pc] = true f.coverPCs[pc] = true
} }
} }
function := getFunction(f.functions, frame.Func)
function.totalPCs[pc] = true
if !covered { if !covered {
ln := f.lines[frame.Line] ln := f.lines[frame.Line]
if !frame.Inline || len(ln.count) == 0 { 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)] ln.symbolCovered = symbols[rg.findSymbol(pc)]
f.lines[frame.Line] = ln 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 { 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 { if f == nil {
f = &file{ f = &file{
lines: make(map[int]line), lines: make(map[int]line),
functions: make(map[string]*function),
totalPCs: make(map[uint64]bool), totalPCs: make(map[uint64]bool),
coverPCs: make(map[uint64]bool), coverPCs: make(map[uint64]bool),
totalInline: make(map[int]bool), totalInline: make(map[int]bool),
@ -161,7 +196,43 @@ func getFile(files map[string]*file, name string) *file {
return f 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{ d := &templateData{
Root: new(templateDir), Root: new(templateDir),
} }

View File

@ -9,9 +9,11 @@ package cover
import ( import (
"bytes" "bytes"
"encoding/csv"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"regexp" "regexp"
"runtime" "runtime"
"strconv" "strconv"
@ -88,7 +90,7 @@ func TestReportGenerator(t *testing.T) {
} }
func testReportGenerator(t *testing.T, target *targets.Target, test Test) { 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 err != nil {
if test.Result == "" { if test.Result == "" {
t.Fatalf("expected no error, but got:\n%v", err) 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 != "" { if test.Result != "" {
t.Fatalf("got no error, but expected %q", test.Result) t.Fatalf("got no error, but expected %q", test.Result)
} }
checkCSVReport(t, csv)
_ = rep _ = rep
} }
@ -136,7 +139,7 @@ void __sanitizer_cov_trace_pc() { printf("%llu", (long long)__builtin_return_add
return bin 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") dir, err := ioutil.TempDir("", "syz-cover-test")
if err != nil { if err != nil {
t.Fatal(err) 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) bin := buildTestBinary(t, target, test, dir)
rg, err := MakeReportGenerator(target, bin, dir, dir) rg, err := MakeReportGenerator(target, bin, dir, dir)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
if test.Result == "" { if test.Result == "" {
var pcs []uint64 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}) test.Progs = append(test.Progs, Prog{Data: "main", PCs: pcs})
} }
out := new(bytes.Buffer) html := new(bytes.Buffer)
if err := rg.Do(out, test.Progs); err != nil { if err := rg.DoHTML(html, test.Progs); err != nil {
return nil, err 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) http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
return return
} }

View File

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