54ab7b69d1
Currently, packages in coverme without tests created by student are not checked for coverage, because no there are no test targets for them at all! The issue can be fixed by simply creating empty !change files, but we are engineers, aren't we? :) Bonus point: it prevents new packages from giving false coverage credit. For example, mock file generated by gomock will have lots of covered code almost instantly. However, the task can be hacked by adding new code to existing packages.
127 lines
2.6 KiB
Go
127 lines
2.6 KiB
Go
package commands
|
|
|
|
import (
|
|
"fmt"
|
|
"go/parser"
|
|
"go/token"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/cover"
|
|
)
|
|
|
|
// coverageCommentPrefix is a prefix of coverage comment.
|
|
//
|
|
// Coverage comment has the following form:
|
|
//
|
|
// // min coverage: 80.5%
|
|
const coverageCommentPrefix = "min coverage: "
|
|
|
|
type CoverageRequirements struct {
|
|
Enabled bool
|
|
Percent float64
|
|
Packages []string
|
|
}
|
|
|
|
// getCoverageRequirements searches for comment in test files
|
|
// that specifies test coverage requirements.
|
|
//
|
|
// Stops on first matching comment.
|
|
func getCoverageRequirements(rootPackage string) *CoverageRequirements {
|
|
files := listTestFiles(rootPackage)
|
|
|
|
for _, f := range files {
|
|
if r, _ := searchCoverageComment(f); r.Enabled {
|
|
return r
|
|
}
|
|
}
|
|
|
|
return &CoverageRequirements{}
|
|
}
|
|
|
|
// searchCoverageComment searches for the first occurrence of the comment of the form
|
|
//
|
|
// // min coverage: 80.5%
|
|
//
|
|
// Stops on the first matching comment.
|
|
func searchCoverageComment(fname string) (*CoverageRequirements, error) {
|
|
fset := token.NewFileSet()
|
|
|
|
f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, c := range f.Comments {
|
|
t := c.Text()
|
|
if !strings.HasPrefix(t, coverageCommentPrefix) || !strings.HasSuffix(t, "%\n") {
|
|
continue
|
|
}
|
|
t = strings.TrimPrefix(t, coverageCommentPrefix)
|
|
t = strings.TrimSuffix(t, "%\n")
|
|
|
|
parts := strings.Split(t, " ")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
percent, err := strconv.ParseFloat(parts[1], 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if percent < 0 || percent > 100.0 {
|
|
continue
|
|
}
|
|
|
|
return &CoverageRequirements{
|
|
Enabled: true,
|
|
Percent: percent,
|
|
Packages: strings.Split(parts[0], ","),
|
|
}, nil
|
|
}
|
|
|
|
return &CoverageRequirements{}, nil
|
|
}
|
|
|
|
// calCoverage calculates coverage percent for given coverage profile.
|
|
func calCoverage(fileNames []string) (float64, error) {
|
|
type block struct {
|
|
fileName string
|
|
startLine, startCol int
|
|
endLine, endCol int
|
|
numStmt int
|
|
}
|
|
counts := map[block]int{}
|
|
|
|
for _, f := range fileNames {
|
|
profiles, err := cover.ParseProfiles(f)
|
|
if err != nil {
|
|
return 0.0, fmt.Errorf("cannot parse coverage profile file %s: %w", f, err)
|
|
}
|
|
|
|
for _, p := range profiles {
|
|
for _, b := range p.Blocks {
|
|
counts[block{
|
|
p.FileName,
|
|
b.StartLine, b.StartCol,
|
|
b.EndLine, b.EndCol,
|
|
b.NumStmt,
|
|
}] += b.Count
|
|
}
|
|
}
|
|
}
|
|
|
|
var total, covered int
|
|
for b, count := range counts {
|
|
total += b.numStmt
|
|
if count > 0 {
|
|
covered += b.numStmt
|
|
}
|
|
}
|
|
|
|
if total == 0 {
|
|
return 0.0, nil
|
|
}
|
|
|
|
return float64(covered) / float64(total) * 100, nil
|
|
}
|