Skip to content

Commit

Permalink
Optionally convert cover report to lcov
Browse files Browse the repository at this point in the history
With the new --//go/config:lcov flag, go cover coverage reports are
converted to lcov. When combined with Bazel's --combined_report=lcov,
this allows for Go coverage to show up in cross-language, cross-target
coverage reports.

The combined report can be converted to HTML via e.g.

genhtml --rc genhtml_branch_coverage=1 \
        --rc genhtml_function_coverage=0 \
        bazel-out/_coverage/_coverage_report.dat
  • Loading branch information
fmeum committed Apr 14, 2022
1 parent 451f267 commit 31c45df
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 13 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ go_config(
"//conditions:default": "//go/config:debug",
}),
gotags = "//go/config:tags",
lcov = "//go/config:lcov",
linkmode = "//go/config:linkmode",
msan = "//go/config:msan",
pure = "//go/config:pure",
Expand Down
6 changes: 6 additions & 0 deletions go/config/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ string_list_flag(
visibility = ["//visibility:public"],
)

bool_flag(
name = "lcov",
build_setting_default = False,
visibility = ["//visibility:public"],
)

filegroup(
name = "all_files",
testonly = True,
Expand Down
2 changes: 2 additions & 0 deletions go/private/actions/compilepkg.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def emit_compilepkg(
args.add("-cover_mode", "atomic")
else:
args.add("-cover_mode", "set")
if go.lcov:
args.add("-cover_lcov")
args.add_all(cover, before_each = "-cover")
args.add_all(archives, before_each = "-arc", map_each = _archive)
if importpath:
Expand Down
6 changes: 6 additions & 0 deletions go/private/context.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ def go_context(ctx, attr = None):
tags = tags,
stamp = mode.stamp,
label = ctx.label,
lcov = go_config_info.lcov if go_config_info else False,

# Action generators
archive = toolchain.actions.archive,
Expand Down Expand Up @@ -781,6 +782,7 @@ def _go_config_impl(ctx):
linkmode = ctx.attr.linkmode[BuildSettingInfo].value,
tags = ctx.attr.gotags[BuildSettingInfo].value,
stamp = ctx.attr.stamp,
lcov = ctx.attr.lcov[BuildSettingInfo].value,
)]

go_config = rule(
Expand Down Expand Up @@ -819,6 +821,10 @@ go_config = rule(
providers = [BuildSettingInfo],
),
"stamp": attr.bool(mandatory = True),
"lcov": attr.label(
mandatory = True,
providers = [BuildSettingInfo],
),
},
provides = [GoConfigInfo],
doc = """Collects information about build settings in the current
Expand Down
2 changes: 2 additions & 0 deletions go/private/rules/test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def _go_test_impl(ctx):
arguments.add("-cover_mode", "atomic")
else:
arguments.add("-cover_mode", "set")
if go.lcov:
arguments.add("-cover_type", "lcov")
arguments.add(
# the l is the alias for the package under test, the l_test must be the
# same with the test suffix
Expand Down
30 changes: 19 additions & 11 deletions go/tools/builders/compilepkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func compilePkg(args []string) error {
var outPath, outFactsPath, cgoExportHPath string
var testFilter string
var gcFlags, asmFlags, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags quoteMultiFlag
var coverLcov bool
fs.Var(&unfilteredSrcs, "src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and compiled")
fs.Var(&coverSrcs, "cover", ".go file that should be instrumented for coverage (must also be a -src)")
fs.Var(&embedSrcs, "embedsrc", "file that may be compiled into the package with a //go:embed directive")
Expand All @@ -67,6 +68,7 @@ func compilePkg(args []string) error {
fs.StringVar(&outFactsPath, "x", "", "The output archive file to write export data and nogo facts")
fs.StringVar(&cgoExportHPath, "cgoexport", "", "The _cgo_exports.h file to write")
fs.StringVar(&testFilter, "testfilter", "off", "Controls test package filtering")
fs.BoolVar(&coverLcov, "cover_lcov", false, "Emit source file paths in coverage instrumentation for compatibility with the lcov format")
if err := fs.Parse(args); err != nil {
return err
}
Expand All @@ -85,9 +87,6 @@ func compilePkg(args []string) error {
for i := range embedSrcs {
embedSrcs[i] = abs(embedSrcs[i])
}
for i := range coverSrcs {
coverSrcs[i] = abs(coverSrcs[i])
}

// Filter sources.
srcs, err := filterAndSplitFiles(unfilteredSrcs)
Expand Down Expand Up @@ -143,7 +142,8 @@ func compilePkg(args []string) error {
packageListPath,
outPath,
outFactsPath,
cgoExportHPath)
cgoExportHPath,
coverLcov)
}

func compileArchive(
Expand All @@ -169,7 +169,8 @@ func compileArchive(
packageListPath string,
outPath string,
outXPath string,
cgoExportHPath string) error {
cgoExportHPath string,
coverLcov bool) error {

workDir, cleanup, err := goenv.workDir()
if err != nil {
Expand Down Expand Up @@ -232,23 +233,30 @@ func compileArchive(

// Instrument source files for coverage.
if coverMode != "" {
shouldCover := make(map[string]bool)
relCoverPath := make(map[string]string)
for _, s := range coverSrcs {
shouldCover[s] = true
relCoverPath[abs(s)] = s
}

combined := append([]string{}, goSrcs...)
if cgoEnabled {
combined = append(combined, cgoSrcs...)
}
for i, origSrc := range combined {
if !shouldCover[origSrc] {
if _, ok := relCoverPath[origSrc]; !ok {
continue
}

srcName := origSrc
if importPath != "" {
srcName = path.Join(importPath, filepath.Base(origSrc))
var srcName string
if coverLcov {
// Bazel merges lcov reports across languages and thus assumes
// that the source file paths are relative to the exec root.
srcName = relCoverPath[origSrc]
} else {
srcName = origSrc
if importPath != "" {
srcName = path.Join(importPath, filepath.Base(origSrc))
}
}

stem := filepath.Base(origSrc)
Expand Down
18 changes: 16 additions & 2 deletions go/tools/builders/generate_test_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type Cases struct {
Examples []Example
TestMain string
CoverMode string
CoverType string
Pkgname string
}

Expand Down Expand Up @@ -194,18 +195,29 @@ func main() {
})
if coverageDat, ok := os.LookupEnv("COVERAGE_OUTPUT_FILE"); ok {
{{if eq .CoverType "lcov"}}
flag.Lookup("test.coverprofile").Value.Set(coverageDat+".cover")
{{else}}
flag.Lookup("test.coverprofile").Value.Set(coverageDat)
{{end}}
}
}
{{end}}
{{if not .TestMain}}
os.Exit(m.Run())
res := m.Run()
{{else}}
{{.TestMain}}(m)
{{/* See golang.org/issue/34129 and golang.org/cl/219639 */}}
os.Exit(int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int()))
res := int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int())
{{end}}
{{if and (ne .CoverMode "") (eq .CoverType "lcov")}}
if err := bzltestutil.ConvertCoverToLcov(); err != nil {
log.Print(err)
os.Exit(bzltestutil.TestWrapperAbnormalExit)
}
{{end}}
os.Exit(res)
}
`

Expand All @@ -221,6 +233,7 @@ func genTestMain(args []string) error {
goenv := envFlags(flags)
out := flags.String("output", "", "output file to write. Defaults to stdout.")
coverMode := flags.String("cover_mode", "", "the coverage mode to use")
coverType := flags.String("cover_type", "cover", "the coverage report type to generate (cover or lcov)")
pkgname := flags.String("pkgname", "", "package name of test")
flags.Var(&imports, "import", "Packages to import")
flags.Var(&sources, "src", "Sources to process for tests")
Expand Down Expand Up @@ -271,6 +284,7 @@ func genTestMain(args []string) error {

cases := Cases{
CoverMode: *coverMode,
CoverType: *coverType,
Pkgname: *pkgname,
}

Expand Down
1 change: 1 addition & 0 deletions go/tools/bzltestutil/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go_tool_library(
name = "bzltestutil",
srcs = [
"init.go",
"lcov.go",
"test2json.go",
"wrap.go",
"xml.go",
Expand Down
171 changes: 171 additions & 0 deletions go/tools/bzltestutil/lcov.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// Copyright 2022 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bzltestutil

import (
"bufio"
"flag"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
)

// ConvertCoverToLcov converts the go coverprofile file coverage.dat.cover to
// the lcov format and stores it in coverage.dat, where it is picked up by
// Bazel.
// The conversion emits line and branch coverage, but not function coverage.
func ConvertCoverToLcov() error {
inPath := flag.Lookup("test.coverprofile").Value.String()
outPath := strings.TrimSuffix(inPath, ".cover")

in, err := os.Open(inPath)
if err != nil {
return err
}
defer in.Close()

out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()

return convertCoverToLcov(bufio.NewScanner(in), out)
}

var coverLinePattern = regexp.MustCompile(`^(?P<path>.*):(?P<startLine>\d+)\.(?P<startColumn>\d+),(?P<endLine>\d+)\.(?P<endColumn>\d+) (?P<numStmt>\d+) (?P<count>\d+)$`)
const pathIdx = 1
const startLineIdx = 2
const endLineIdx = 4
const countIdx = 7

type blockInfo struct {
midLine uint32
count uint32
}

func convertCoverToLcov(cover *bufio.Scanner, lcov io.StringWriter) error {
currentPath := ""
var lineCounts map[uint32]uint32
var blockInfos []blockInfo
for cover.Scan() {
l := cover.Text()
m := coverLinePattern.FindStringSubmatch(l)
if m == nil {
if strings.HasPrefix(l, "mode: ") {
continue
}
return fmt.Errorf("invalid go cover line: %s", l)
}

if m[pathIdx] != currentPath {
if currentPath != "" {
if err := emitLcovLines(lcov, currentPath, lineCounts, blockInfos); err != nil {
return err
}
}
currentPath = m[pathIdx]
lineCounts = make(map[uint32]uint32)
blockInfos = nil
}

startLine, err := strconv.ParseUint(m[startLineIdx], 10, 32)
if err != nil {
return err
}
endLine, err := strconv.ParseUint(m[endLineIdx], 10, 32)
if err != nil {
return err
}
count, err := strconv.ParseUint(m[countIdx], 10, 32)
if err != nil {
return err
}
blockInfos = append(blockInfos, blockInfo{
midLine: uint32((startLine + endLine) / 2),
count: uint32(count),
})
for line := uint32(startLine); line <= uint32(endLine); line++ {
prevCount, ok := lineCounts[line]
if !ok || uint32(count) > prevCount {
lineCounts[line] = uint32(count)
}
}
}
if currentPath != "" {
if err := emitLcovLines(lcov, currentPath, lineCounts, blockInfos); err != nil {
return err
}
}
return nil
}

func emitLcovLines(lcov io.StringWriter, path string, lineCounts map[uint32]uint32, blockInfos []blockInfo) error {
_, err := lcov.WriteString(fmt.Sprintf("SF:%s\n", path))
if err != nil {
return err
}

// Every individual block instrumented by go cover is reported as a branch
// with its corresponding line chosen as the mid line of the block.
sort.Slice(blockInfos, func(i, j int) bool { return blockInfos[i].midLine < blockInfos[j].midLine })
blocksCovered := 0
for i, block := range blockInfos {
taken := "-"
if block.count > 0 {
taken = fmt.Sprint(block.count)
blocksCovered++
}
_, err = lcov.WriteString(fmt.Sprintf("BRDA:%d,%d,%d,%s\n", block.midLine, i, i, taken))
if err != nil {
return err
}
}
// Emit a summary containing the number of all/covered blocks.
_, err = lcov.WriteString(fmt.Sprintf("BRF:%d\nBRH:%d\n", len(blockInfos), blocksCovered))
if err != nil {
return err
}

// Emit the coverage counters for the individual source lines.
sortedLines := make([]uint32, len(lineCounts))
i := 0
for line := range lineCounts {
sortedLines[i] = line
i++
}
sort.Slice(sortedLines, func(i, j int) bool { return sortedLines[i] < sortedLines[j] })
numCovered := 0
for _, line := range sortedLines {
count := lineCounts[line]
if count > 0 {
numCovered++
}
_, err := lcov.WriteString(fmt.Sprintf("DA:%d,%d\n", line, count))
if err != nil {
return err
}
}
// Emit a summary containing the number of all/covered lines and end the info for the current source file.
_, err = lcov.WriteString(fmt.Sprintf("LH:%d\nLF:%d\nend_of_record\n", numCovered, len(sortedLines)))
if err != nil {
return err
}
return nil
}
9 changes: 9 additions & 0 deletions tests/core/coverage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ go_bazel_test(
name = "binary_coverage_test",
srcs = ["binary_coverage_test.go"],
)

go_bazel_test(
name = "lcov_coverage_test",
srcs = ["lcov_coverage_test.go"],
target_compatible_with = select({
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
)
Loading

0 comments on commit 31c45df

Please sign in to comment.