2020-03-18 22:29:11 +00:00
|
|
|
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 {
|
2020-03-22 00:26:50 +00:00
|
|
|
Enabled bool
|
|
|
|
Percent float64
|
|
|
|
Packages []string
|
2020-03-18 22:29:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2020-03-18 22:33:16 +00:00
|
|
|
if r, _ := searchCoverageComment(f); r.Enabled {
|
2020-03-18 22:29:11 +00:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-18 22:33:16 +00:00
|
|
|
return &CoverageRequirements{}
|
2020-03-18 22:29:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
2020-03-22 00:26:50 +00:00
|
|
|
|
|
|
|
parts := strings.Split(t, " ")
|
|
|
|
if len(parts) != 2 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
percent, err := strconv.ParseFloat(parts[1], 64)
|
2020-03-18 22:29:11 +00:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if percent < 0 || percent > 100.0 {
|
|
|
|
continue
|
|
|
|
}
|
2020-03-22 00:26:50 +00:00
|
|
|
|
|
|
|
return &CoverageRequirements{
|
|
|
|
Enabled: true,
|
|
|
|
Percent: percent,
|
|
|
|
Packages: strings.Split(parts[0], ","),
|
|
|
|
}, nil
|
2020-03-18 22:29:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &CoverageRequirements{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// calCoverage calculates coverage percent for given coverage profile.
|
2020-03-21 23:51:13 +00:00
|
|
|
func calCoverage(fileNames []string) (float64, error) {
|
|
|
|
type block struct {
|
|
|
|
fileName string
|
|
|
|
startLine, startCol int
|
|
|
|
endLine, endCol int
|
|
|
|
numStmt int
|
2020-03-18 22:29:11 +00:00
|
|
|
}
|
2020-03-21 23:51:13 +00:00
|
|
|
counts := map[block]int{}
|
2020-03-18 22:29:11 +00:00
|
|
|
|
2020-03-21 23:51:13 +00:00
|
|
|
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
|
2020-03-18 22:29:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-21 23:51:13 +00:00
|
|
|
var total, covered int
|
|
|
|
for b, count := range counts {
|
|
|
|
total += b.numStmt
|
|
|
|
if count > 0 {
|
|
|
|
covered += b.numStmt
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-18 22:29:11 +00:00
|
|
|
if total == 0 {
|
|
|
|
return 0.0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return float64(covered) / float64(total) * 100, nil
|
|
|
|
}
|