mirror of
https://github.com/golang/go
synced 2024-11-18 15:04:44 -07:00
57335a8a8f
When cover is used with cgo packages, the coverage profile is produced from the cgo-generated Go source files, so the profile will reference line and column numbers from that generated source, even though it names the original file. This is okay in general because the cgo-generated Go source files are very similar to the original, with one significant exception: the original source may refer to C identifiers such as C.foo(), but the cgo tool generates source that rewrites these mentions to something like _Cfunc_foo. This means that column numbers in coverage profiles might be higher than they should be, so be lenient when interpreting them. Update golang/go#9479 Change-Id: Ic3abef07471614101ce0c686d35b85e7e5e6a777 Reviewed-on: https://go-review.googlesource.com/2410 Reviewed-by: Rob Pike <r@golang.org>
191 lines
5.1 KiB
Go
191 lines
5.1 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package cover provides support for parsing coverage profiles
|
|
// generated by "go test -coverprofile=cover.out".
|
|
package cover // import "golang.org/x/tools/cover"
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Profile represents the profiling data for a specific file.
|
|
type Profile struct {
|
|
FileName string
|
|
Mode string
|
|
Blocks []ProfileBlock
|
|
}
|
|
|
|
// ProfileBlock represents a single block of profiling data.
|
|
type ProfileBlock struct {
|
|
StartLine, StartCol int
|
|
EndLine, EndCol int
|
|
NumStmt, Count int
|
|
}
|
|
|
|
type byFileName []*Profile
|
|
|
|
func (p byFileName) Len() int { return len(p) }
|
|
func (p byFileName) Less(i, j int) bool { return p[i].FileName < p[j].FileName }
|
|
func (p byFileName) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
|
|
|
// ParseProfiles parses profile data in the specified file and returns a
|
|
// Profile for each source file described therein.
|
|
func ParseProfiles(fileName string) ([]*Profile, error) {
|
|
pf, err := os.Open(fileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer pf.Close()
|
|
|
|
files := make(map[string]*Profile)
|
|
buf := bufio.NewReader(pf)
|
|
// First line is "mode: foo", where foo is "set", "count", or "atomic".
|
|
// Rest of file is in the format
|
|
// encoding/base64/base64.go:34.44,37.40 3 1
|
|
// where the fields are: name.go:line.column,line.column numberOfStatements count
|
|
s := bufio.NewScanner(buf)
|
|
mode := ""
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
if mode == "" {
|
|
const p = "mode: "
|
|
if !strings.HasPrefix(line, p) || line == p {
|
|
return nil, fmt.Errorf("bad mode line: %v", line)
|
|
}
|
|
mode = line[len(p):]
|
|
continue
|
|
}
|
|
m := lineRe.FindStringSubmatch(line)
|
|
if m == nil {
|
|
return nil, fmt.Errorf("line %q doesn't match expected format: %v", m, lineRe)
|
|
}
|
|
fn := m[1]
|
|
p := files[fn]
|
|
if p == nil {
|
|
p = &Profile{
|
|
FileName: fn,
|
|
Mode: mode,
|
|
}
|
|
files[fn] = p
|
|
}
|
|
p.Blocks = append(p.Blocks, ProfileBlock{
|
|
StartLine: toInt(m[2]),
|
|
StartCol: toInt(m[3]),
|
|
EndLine: toInt(m[4]),
|
|
EndCol: toInt(m[5]),
|
|
NumStmt: toInt(m[6]),
|
|
Count: toInt(m[7]),
|
|
})
|
|
}
|
|
if err := s.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, p := range files {
|
|
sort.Sort(blocksByStart(p.Blocks))
|
|
}
|
|
// Generate a sorted slice.
|
|
profiles := make([]*Profile, 0, len(files))
|
|
for _, profile := range files {
|
|
profiles = append(profiles, profile)
|
|
}
|
|
sort.Sort(byFileName(profiles))
|
|
return profiles, nil
|
|
}
|
|
|
|
type blocksByStart []ProfileBlock
|
|
|
|
func (b blocksByStart) Len() int { return len(b) }
|
|
func (b blocksByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
func (b blocksByStart) Less(i, j int) bool {
|
|
bi, bj := b[i], b[j]
|
|
return bi.StartLine < bj.StartLine || bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol
|
|
}
|
|
|
|
var lineRe = regexp.MustCompile(`^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`)
|
|
|
|
func toInt(s string) int {
|
|
i, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return i
|
|
}
|
|
|
|
// Boundary represents the position in a source file of the beginning or end of a
|
|
// block as reported by the coverage profile. In HTML mode, it will correspond to
|
|
// the opening or closing of a <span> tag and will be used to colorize the source
|
|
type Boundary struct {
|
|
Offset int // Location as a byte offset in the source file.
|
|
Start bool // Is this the start of a block?
|
|
Count int // Event count from the cover profile.
|
|
Norm float64 // Count normalized to [0..1].
|
|
}
|
|
|
|
// Boundaries returns a Profile as a set of Boundary objects within the provided src.
|
|
func (p *Profile) Boundaries(src []byte) (boundaries []Boundary) {
|
|
// Find maximum count.
|
|
max := 0
|
|
for _, b := range p.Blocks {
|
|
if b.Count > max {
|
|
max = b.Count
|
|
}
|
|
}
|
|
// Divisor for normalization.
|
|
divisor := math.Log(float64(max))
|
|
|
|
// boundary returns a Boundary, populating the Norm field with a normalized Count.
|
|
boundary := func(offset int, start bool, count int) Boundary {
|
|
b := Boundary{Offset: offset, Start: start, Count: count}
|
|
if !start || count == 0 {
|
|
return b
|
|
}
|
|
if max <= 1 {
|
|
b.Norm = 0.8 // Profile is in"set" mode; we want a heat map. Use cov8 in the CSS.
|
|
} else if count > 0 {
|
|
b.Norm = math.Log(float64(count)) / divisor
|
|
}
|
|
return b
|
|
}
|
|
|
|
line, col := 1, 2 // TODO: Why is this 2?
|
|
for si, bi := 0, 0; si < len(src) && bi < len(p.Blocks); {
|
|
b := p.Blocks[bi]
|
|
if b.StartLine == line && b.StartCol == col {
|
|
boundaries = append(boundaries, boundary(si, true, b.Count))
|
|
}
|
|
if b.EndLine == line && b.EndCol == col || line > b.EndLine {
|
|
boundaries = append(boundaries, boundary(si, false, 0))
|
|
bi++
|
|
continue // Don't advance through src; maybe the next block starts here.
|
|
}
|
|
if src[si] == '\n' {
|
|
line++
|
|
col = 0
|
|
}
|
|
col++
|
|
si++
|
|
}
|
|
sort.Sort(boundariesByPos(boundaries))
|
|
return
|
|
}
|
|
|
|
type boundariesByPos []Boundary
|
|
|
|
func (b boundariesByPos) Len() int { return len(b) }
|
|
func (b boundariesByPos) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
func (b boundariesByPos) Less(i, j int) bool {
|
|
if b[i].Offset == b[j].Offset {
|
|
return !b[i].Start && b[j].Start
|
|
}
|
|
return b[i].Offset < b[j].Offset
|
|
}
|