-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from rogpeppe/003-better-coverage
testscript: better coverage support
- Loading branch information
Showing
2 changed files
with
290 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
package testscript | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"sync/atomic" | ||
"testing" | ||
|
||
"gopkg.in/errgo.v2/fmt/errors" | ||
) | ||
|
||
// mergeCoverProfile merges the coverage information in f into | ||
// cover. It assumes that the coverage information in f is | ||
// always produced from the same binary for every call. | ||
func mergeCoverProfile(cover *testing.Cover, r io.Reader) error { | ||
scanner, err := newProfileScanner(r) | ||
if err != nil { | ||
return errors.Wrap(err) | ||
} | ||
if scanner.Mode() != testing.CoverMode() { | ||
return errors.Newf("unexpected coverage mode in subcommand") | ||
} | ||
if cover.Mode == "" { | ||
cover.Mode = scanner.Mode() | ||
} | ||
isCount := cover.Mode == "count" | ||
if cover.Counters == nil { | ||
cover.Counters = make(map[string][]uint32) | ||
cover.Blocks = make(map[string][]testing.CoverBlock) | ||
} | ||
|
||
// Note that we rely on the fact that the coverage is written | ||
// out file-by-file, with all blocks for a file in sequence. | ||
var ( | ||
filename string | ||
blockId uint32 | ||
counters []uint32 | ||
blocks []testing.CoverBlock | ||
) | ||
flush := func() { | ||
if len(counters) > 0 { | ||
cover.Counters[filename] = counters | ||
cover.Blocks[filename] = blocks | ||
} | ||
} | ||
for scanner.Scan() { | ||
block := scanner.Block() | ||
if scanner.Filename() != filename { | ||
flush() | ||
filename = scanner.Filename() | ||
counters = cover.Counters[filename] | ||
blocks = cover.Blocks[filename] | ||
blockId = 0 | ||
} else { | ||
blockId++ | ||
} | ||
if int(blockId) >= len(counters) { | ||
counters = append(counters, block.Count) | ||
blocks = append(blocks, block.CoverBlock) | ||
continue | ||
} | ||
// TODO check that block.CoverBlock == blocks[blockId] ? | ||
if isCount { | ||
counters[blockId] += block.Count | ||
} else { | ||
counters[blockId] |= block.Count | ||
} | ||
} | ||
flush() | ||
if scanner.Err() != nil { | ||
return errors.Notef(err, nil, "error scanning profile") | ||
} | ||
return nil | ||
} | ||
|
||
var ( | ||
coverChan chan *os.File | ||
coverDone chan testing.Cover | ||
) | ||
|
||
func goCoverProfileMerge() { | ||
if coverChan != nil { | ||
panic("RunMain called twice!") | ||
} | ||
coverChan = make(chan *os.File) | ||
coverDone = make(chan testing.Cover) | ||
go mergeCoverProfiles() | ||
} | ||
|
||
func mergeCoverProfiles() { | ||
var cover testing.Cover | ||
for f := range coverChan { | ||
if err := mergeCoverProfile(&cover, f); err != nil { | ||
log.Printf("cannot merge coverage profile from %v: %v", f.Name(), err) | ||
} | ||
f.Close() | ||
os.Remove(f.Name()) | ||
} | ||
coverDone <- cover | ||
} | ||
|
||
func finalizeCoverProfile() error { | ||
cprof := coverProfile() | ||
if cprof == "" { | ||
return nil | ||
} | ||
f, err := os.Open(cprof) | ||
if err != nil { | ||
return errors.Notef(err, nil, "cannot open existing cover profile") | ||
} | ||
coverChan <- f | ||
close(coverChan) | ||
cover := <-coverDone | ||
f, err = os.Create(cprof) | ||
if err != nil { | ||
return errors.Notef(err, nil, "cannot create cover profile") | ||
} | ||
defer f.Close() | ||
w := bufio.NewWriter(f) | ||
if err := writeCoverProfile1(w, cover); err != nil { | ||
return errors.Wrap(err) | ||
} | ||
if err := w.Flush(); err != nil { | ||
return errors.Wrap(err) | ||
} | ||
if err := f.Close(); err != nil { | ||
return errors.Wrap(err) | ||
} | ||
return nil | ||
} | ||
|
||
func writeCoverProfile1(w io.Writer, cover testing.Cover) error { | ||
fmt.Fprintf(w, "mode: %s\n", cover.Mode) | ||
var active, total int64 | ||
var count uint32 | ||
for name, counts := range cover.Counters { | ||
blocks := cover.Blocks[name] | ||
for i := range counts { | ||
stmts := int64(blocks[i].Stmts) | ||
total += stmts | ||
count = atomic.LoadUint32(&counts[i]) // For -mode=atomic. | ||
if count > 0 { | ||
active += stmts | ||
} | ||
_, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name, | ||
blocks[i].Line0, blocks[i].Col0, | ||
blocks[i].Line1, blocks[i].Col1, | ||
stmts, | ||
count, | ||
) | ||
if err != nil { | ||
return errors.Wrap(err) | ||
} | ||
} | ||
} | ||
if total == 0 { | ||
total = 1 | ||
} | ||
fmt.Printf("total coverage: %.1f%% of statements%s\n", 100*float64(active)/float64(total), cover.CoveredPackages) | ||
return nil | ||
} | ||
|
||
type profileScanner struct { | ||
mode string | ||
err error | ||
scanner *bufio.Scanner | ||
filename string | ||
block coverBlock | ||
} | ||
|
||
type coverBlock struct { | ||
testing.CoverBlock | ||
Count uint32 | ||
} | ||
|
||
var profileLineRe = 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 | ||
} | ||
|
||
func newProfileScanner(r io.Reader) (*profileScanner, error) { | ||
s := &profileScanner{ | ||
scanner: bufio.NewScanner(r), | ||
} | ||
// 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 | ||
if !s.scanner.Scan() { | ||
return nil, errors.Newf("no lines found in profile: %v", s.Err()) | ||
} | ||
line := s.scanner.Text() | ||
mode := strings.TrimPrefix(line, "mode: ") | ||
if len(mode) == len(line) { | ||
return nil, fmt.Errorf("bad mode line %q", line) | ||
} | ||
s.mode = mode | ||
return s, nil | ||
} | ||
|
||
// Mode returns the profile's coverage mode (one of "atomic", "count: | ||
// or "set"). | ||
func (s *profileScanner) Mode() string { | ||
return s.mode | ||
} | ||
|
||
// Err returns any error encountered when scanning a profile. | ||
func (s *profileScanner) Err() error { | ||
if s.err == io.EOF { | ||
return nil | ||
} | ||
return s.err | ||
} | ||
|
||
// Block returns the most recently scanned profile block, or the zero | ||
// block if Scan has not been called or has returned false. | ||
func (s *profileScanner) Block() coverBlock { | ||
if s.err == nil { | ||
return s.block | ||
} | ||
return coverBlock{} | ||
} | ||
|
||
// Filename returns the filename of the most recently scanned profile | ||
// block, or the empty string if Scan has not been called or has | ||
// returned false. | ||
func (s *profileScanner) Filename() string { | ||
if s.err == nil { | ||
return s.filename | ||
} | ||
return "" | ||
} | ||
|
||
// Scan scans the next line in a coverage profile and reports whether | ||
// a line was found. | ||
func (s *profileScanner) Scan() bool { | ||
if s.err != nil { | ||
return false | ||
} | ||
if !s.scanner.Scan() { | ||
s.err = io.EOF | ||
return false | ||
} | ||
m := profileLineRe.FindStringSubmatch(s.scanner.Text()) | ||
if m == nil { | ||
s.err = errors.Newf("line %q doesn't match expected format %v", m, profileLineRe) | ||
return false | ||
} | ||
s.filename = m[1] | ||
s.block = coverBlock{ | ||
CoverBlock: testing.CoverBlock{ | ||
Line0: uint32(toInt(m[2])), | ||
Col0: uint16(toInt(m[3])), | ||
Line1: uint32(toInt(m[4])), | ||
Col1: uint16(toInt(m[5])), | ||
Stmts: uint16(toInt(m[6])), | ||
}, | ||
Count: uint32(toInt(m[7])), | ||
} | ||
return true | ||
} |
Oops, something went wrong.