@@ -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+
851873const (
852874 getYarnLockScript = `#!/bin/bash
853875set -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.
12781355func (p * Package ) buildDocker (buildctx * buildContext , wd , result string ) (res * packageBuild , err error ) {
0 commit comments