Skip to content

Commit 3256d17

Browse files
committed
Report coverage as package reporter
1 parent af0c473 commit 3256d17

File tree

3 files changed

+148
-2
lines changed

3 files changed

+148
-2
lines changed

pkg/leeway/build.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"os/exec"
1717
"path/filepath"
1818
"regexp"
19+
"strconv"
1920
"strings"
2021
"sync"
2122
"time"
@@ -765,6 +766,18 @@ func (p *Package) build(buildctx *buildContext) (err error) {
765766
}
766767
}
767768

769+
if bld.TestCoverage != nil {
770+
coverage, funcsWithoutTest, funcsWithTest, err := bld.TestCoverage()
771+
if err != nil {
772+
return err
773+
}
774+
775+
pkgRep.TestCoverageAvailable = true
776+
pkgRep.TestCoveragePercentage = coverage
777+
pkgRep.FunctionsWithoutTest = funcsWithoutTest
778+
pkgRep.FunctionsWithTest = funcsWithTest
779+
}
780+
768781
err = executeCommandsForPackage(buildctx, p, builddir, bld.Commands[PackageBuildPhasePackage])
769782
if err != nil {
770783
return err
@@ -846,8 +859,17 @@ type packageBuild struct {
846859
// If Subjects is not nil it's used to compute the provenance subjects of the
847860
// package build. This field takes precedence over PostBuild
848861
Subjects func() ([]in_toto.Subject, error)
862+
863+
// If TestCoverage is not nil it's used to compute the test coverage of the package build.
864+
// This function is expected to return a value between 0 and 100.
865+
// If the package build does not have any tests, this function must return 0.
866+
// If the package build has tests but the test coverage cannot be computed, this function must return an error.
867+
// This function is guaranteed to be called after the test phase has finished.
868+
TestCoverage testCoverageFunc
849869
}
850870

871+
type testCoverageFunc func() (coverage, funcsWithoutTest, funcsWithTest int, err error)
872+
851873
const (
852874
getYarnLockScript = `#!/bin/bash
853875
set -Eeuo pipefail
@@ -1236,10 +1258,14 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
12361258
commands[PackageBuildPhaseLint] = append(commands[PackageBuildPhaseLint], cfg.LintCommand)
12371259
}
12381260
}
1261+
var reportCoverage testCoverageFunc
12391262
if !cfg.DontTest && !buildctx.DontTest {
12401263
testCommand := []string{goCommand, "test", "-v"}
12411264
if buildctx.buildOptions.CoverageOutputPath != "" {
12421265
testCommand = append(testCommand, fmt.Sprintf("-coverprofile=%v", codecovComponentName(p.FullName())))
1266+
} else {
1267+
testCommand = append(testCommand, "-coverprofile=testcoverage.out")
1268+
reportCoverage = collectGoTestCoverage(filepath.Join(wd, "testcoverage.out"), p.FullName())
12431269
}
12441270
testCommand = append(testCommand, "./...")
12451271

@@ -1269,10 +1295,61 @@ func (p *Package) buildGo(buildctx *buildContext, wd, result string) (res *packa
12691295
}
12701296

12711297
return &packageBuild{
1272-
Commands: commands,
1298+
Commands: commands,
1299+
TestCoverage: reportCoverage,
12731300
}, nil
12741301
}
12751302

1303+
func collectGoTestCoverage(covfile, fullName string) testCoverageFunc {
1304+
return func() (coverage, funcsWithoutTest, funcsWithTest int, err error) {
1305+
// We need to collect the coverage for all packages in the module.
1306+
// To that end we load the coverage file.
1307+
// The coverage file contains the coverage for all packages in the module.
1308+
1309+
cmd := exec.Command("go", "tool", "cover", "-func", covfile)
1310+
out, err := cmd.CombinedOutput()
1311+
if err != nil {
1312+
err = xerrors.Errorf("cannot collect test coverage: %w: %s", err, string(out))
1313+
return
1314+
}
1315+
1316+
coverage, funcsWithoutTest, funcsWithTest, err = parseGoCoverOutput(string(out))
1317+
return
1318+
}
1319+
}
1320+
1321+
func parseGoCoverOutput(input string) (coverage, funcsWithoutTest, funcsWithTest int, err error) {
1322+
// The output of the coverage tool looks like this:
1323+
// github.com/gitpod-io/gitpod/content_ws/pkg/contentws/contentws.go:33: New 100.0%
1324+
lines := strings.Split(input, "\n")
1325+
1326+
for _, line := range lines {
1327+
fields := strings.Fields(line)
1328+
if len(fields) < 3 {
1329+
continue
1330+
}
1331+
perc := strings.Trim(strings.TrimSpace(fields[2]), "%")
1332+
percF, err := strconv.ParseFloat(perc, 32)
1333+
if err != nil {
1334+
log.Warnf("cannot parse coverage percentage for line %s: %v", line, err)
1335+
continue
1336+
}
1337+
intCov := int(percF)
1338+
coverage += intCov
1339+
if intCov == 0 {
1340+
funcsWithoutTest++
1341+
} else {
1342+
funcsWithTest++
1343+
}
1344+
}
1345+
1346+
total := (funcsWithoutTest + funcsWithTest)
1347+
if total != 0 {
1348+
coverage = coverage / total
1349+
}
1350+
return
1351+
}
1352+
12761353
// buildDocker implements the build process for Docker packages.
12771354
// If you change anything in this process that's not backwards compatible, make sure you increment buildProcessVersions accordingly.
12781355
func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *packageBuild, err error) {

pkg/leeway/build_internal_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package leeway
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp"
7+
)
8+
9+
func TestParseGoCoverOutput(t *testing.T) {
10+
type Expectation struct {
11+
Error string
12+
Coverage int
13+
FuncsWithoutTest int
14+
FuncsWithTest int
15+
}
16+
tests := []struct {
17+
Name string
18+
Input string
19+
Expectation Expectation
20+
}{
21+
{
22+
Name: "empty",
23+
},
24+
{
25+
Name: "valid",
26+
Input: `github.com/gitpod-io/leeway/store.go:165: Get 100.0%
27+
github.com/gitpod-io/leeway/store.go:173: Set 100.0%
28+
github.com/gitpod-io/leeway/store.go:178: Delete 100.0%
29+
github.com/gitpod-io/leeway/store.go:183: Scan 80.0%
30+
github.com/gitpod-io/leeway/store.go:194: Close 0.0%
31+
github.com/gitpod-io/leeway/store.go:206: Upsert 0.0%`,
32+
Expectation: Expectation{
33+
Coverage: 63,
34+
FuncsWithoutTest: 2,
35+
FuncsWithTest: 4,
36+
},
37+
},
38+
}
39+
40+
for _, test := range tests {
41+
t.Run(test.Name, func(t *testing.T) {
42+
var act Expectation
43+
44+
var err error
45+
act.Coverage, act.FuncsWithoutTest, act.FuncsWithTest, err = parseGoCoverOutput(test.Input)
46+
if err != nil {
47+
act.Error = err.Error()
48+
}
49+
50+
if diff := cmp.Diff(test.Expectation, act); diff != "" {
51+
t.Errorf("parseGoCoverOutput() mismatch (-want +got):\n%s", diff)
52+
}
53+
})
54+
}
55+
}

pkg/leeway/reporter.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ type PackageBuildReport struct {
5454

5555
Phases []PackageBuildPhase
5656
Error error
57+
58+
TestCoverageAvailable bool
59+
TestCoveragePercentage int
60+
FunctionsWithoutTest int
61+
FunctionsWithTest int
5762
}
5863

5964
// PhaseDuration returns the time it took to execute the phases commands
@@ -210,7 +215,11 @@ func (r *ConsoleReporter) PackageBuildFinished(pkg *Package, rep *PackageBuildRe
210215
delete(r.times, nme)
211216
r.mu.Unlock()
212217

213-
msg := color.Sprintf("<green>package build succeded</> <gray>(%.2fs)</>\n", dur.Seconds())
218+
var coverage string
219+
if rep.TestCoverageAvailable {
220+
coverage = color.Sprintf("<fg=yellow>test coverage: %d%%</> <gray>(%d of %d functions have tests)</>\n", rep.TestCoveragePercentage, rep.FunctionsWithTest, rep.FunctionsWithTest+rep.FunctionsWithoutTest)
221+
}
222+
msg := color.Sprintf("%s<green>package build succeded</> <gray>(%.2fs)</>\n", coverage, dur.Seconds())
214223
if rep.Error != nil {
215224
msg = color.Sprintf("<red>package build failed while %sing</>\n<white>Reason:</> %s\n", rep.LastPhase(), rep.Error)
216225
}
@@ -571,6 +580,11 @@ func (sr *SegmentReporter) PackageBuildFinished(pkg *Package, rep *PackageBuildR
571580
"lastPhase": rep.LastPhase(),
572581
"durationMS": rep.TotalTime().Milliseconds(),
573582
}
583+
if rep.TestCoverageAvailable {
584+
props["testCoverage"] = rep.TestCoveragePercentage
585+
props["functionsWithoutTest"] = rep.FunctionsWithoutTest
586+
props["functionsWithTest"] = rep.FunctionsWithTest
587+
}
574588
addPackageToSegmentEventProps(props, pkg)
575589
sr.track("package_build_finished", props)
576590
}

0 commit comments

Comments
 (0)