diff --git a/tools/testtool/commands/coverage.go b/tools/testtool/commands/coverage.go new file mode 100644 index 0000000..1f2f538 --- /dev/null +++ b/tools/testtool/commands/coverage.go @@ -0,0 +1,98 @@ +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 +} + +// 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) + + r := &CoverageRequirements{} + for _, f := range files { + r, _ := searchCoverageComment(f) + if r.Enabled { + return r + } + } + + return r +} + +// 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") + percent, err := strconv.ParseFloat(t, 64) + if err != nil { + continue + } + if percent < 0 || percent > 100.0 { + continue + } + return &CoverageRequirements{Enabled: true, Percent: percent}, nil + } + + return &CoverageRequirements{}, nil +} + +// calCoverage calculates coverage percent for given coverage profile. +func calCoverage(profile string) (float64, error) { + profiles, err := cover.ParseProfiles(profile) + if err != nil { + return 0.0, fmt.Errorf("cannot parse coverage profile file %s: %w", profile, err) + } + + var total, covered int + for _, p := range profiles { + for _, block := range p.Blocks { + total += block.NumStmt + if block.Count > 0 { + covered += block.NumStmt + } + } + } + + if total == 0 { + return 0.0, nil + } + + return float64(covered) / float64(total) * 100, nil +} diff --git a/tools/testtool/commands/coverage_test.go b/tools/testtool/commands/coverage_test.go new file mode 100644 index 0000000..766eae3 --- /dev/null +++ b/tools/testtool/commands/coverage_test.go @@ -0,0 +1,17 @@ +package commands + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_getCoverageRequirements(t *testing.T) { + r := getCoverageRequirements("../testdata/coverage/sum") + require.True(t, r.Enabled) + require.Equal(t, 90.0, r.Percent) + t.Logf("r: %+v", r) + //require.Equal(t, + // absPaths([]string{"sum/private_test.go", "sum/public_test.go"}), + // getCoverageRequirements("../testdata/coverage")) +}