xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/bzltestutil/lcov.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1// Copyright 2022 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package bzltestutil
16
17import (
18	"bufio"
19	"flag"
20	"fmt"
21	"io"
22	"log"
23	"os"
24	"regexp"
25	"sort"
26	"strconv"
27	"strings"
28	"testing/internal/testdeps"
29)
30
31// ConvertCoverToLcov converts the go coverprofile file coverage.dat.cover to
32// the expectedLcov format and stores it in coverage.dat, where it is picked up by
33// Bazel.
34// The conversion emits line and branch coverage, but not function coverage.
35func ConvertCoverToLcov() error {
36	inPath := flag.Lookup("test.coverprofile").Value.String()
37	in, err := os.Open(inPath)
38	if err != nil {
39		// This can happen if there are no tests and should not be an error.
40		log.Printf("Not collecting coverage: %s has not been created: %s", inPath, err)
41		return nil
42	}
43	defer in.Close()
44
45	// All *.dat files in $COVERAGE_DIR will be merged by Bazel's lcov_merger tool.
46	out, err := os.CreateTemp(os.Getenv("COVERAGE_DIR"), "go_coverage.*.dat")
47	if err != nil {
48		return err
49	}
50	defer out.Close()
51
52	return convertCoverToLcov(in, out)
53}
54
55var _coverLinePattern = regexp.MustCompile(`^(?P<path>.+):(?P<startLine>\d+)\.(?P<startColumn>\d+),(?P<endLine>\d+)\.(?P<endColumn>\d+) (?P<numStmt>\d+) (?P<count>\d+)$`)
56
57const (
58	_pathIdx      = 1
59	_startLineIdx = 2
60	_endLineIdx   = 4
61	_countIdx     = 7
62)
63
64func convertCoverToLcov(coverReader io.Reader, lcovWriter io.Writer) error {
65	cover := bufio.NewScanner(coverReader)
66	lcov := bufio.NewWriter(lcovWriter)
67	defer lcov.Flush()
68	currentPath := ""
69	var lineCounts map[uint32]uint32
70	for cover.Scan() {
71		l := cover.Text()
72		m := _coverLinePattern.FindStringSubmatch(l)
73		if m == nil {
74			if strings.HasPrefix(l, "mode: ") {
75				continue
76			}
77			return fmt.Errorf("invalid go cover line: %s", l)
78		}
79
80		if m[_pathIdx] != currentPath {
81			if currentPath != "" {
82				if err := emitLcovLines(lcov, currentPath, lineCounts); err != nil {
83					return err
84				}
85			}
86			currentPath = m[_pathIdx]
87			lineCounts = make(map[uint32]uint32)
88		}
89
90		startLine, err := strconv.ParseUint(m[_startLineIdx], 10, 32)
91		if err != nil {
92			return err
93		}
94		endLine, err := strconv.ParseUint(m[_endLineIdx], 10, 32)
95		if err != nil {
96			return err
97		}
98		count, err := strconv.ParseUint(m[_countIdx], 10, 32)
99		if err != nil {
100			return err
101		}
102		for line := uint32(startLine); line <= uint32(endLine); line++ {
103			prevCount, ok := lineCounts[line]
104			if !ok || uint32(count) > prevCount {
105				lineCounts[line] = uint32(count)
106			}
107		}
108	}
109	if currentPath != "" {
110		if err := emitLcovLines(lcov, currentPath, lineCounts); err != nil {
111			return err
112		}
113	}
114	return nil
115}
116
117func emitLcovLines(lcov io.StringWriter, path string, lineCounts map[uint32]uint32) error {
118	_, err := lcov.WriteString(fmt.Sprintf("SF:%s\n", path))
119	if err != nil {
120		return err
121	}
122
123	// Emit the coverage counters for the individual source lines.
124	sortedLines := make([]uint32, 0, len(lineCounts))
125	for line := range lineCounts {
126		sortedLines = append(sortedLines, line)
127	}
128	sort.Slice(sortedLines, func(i, j int) bool { return sortedLines[i] < sortedLines[j] })
129	numCovered := 0
130	for _, line := range sortedLines {
131		count := lineCounts[line]
132		if count > 0 {
133			numCovered++
134		}
135		_, err := lcov.WriteString(fmt.Sprintf("DA:%d,%d\n", line, count))
136		if err != nil {
137			return err
138		}
139	}
140	// Emit a summary containing the number of all/covered lines and end the info for the current source file.
141	_, err = lcov.WriteString(fmt.Sprintf("LH:%d\nLF:%d\nend_of_record\n", numCovered, len(sortedLines)))
142	if err != nil {
143		return err
144	}
145	return nil
146}
147
148// LcovTestDeps is a patched version of testdeps.TestDeps that allows to
149// hook into the SetPanicOnExit0 call happening right before testing.M.Run
150// returns.
151// This trick relies on the testDeps interface defined in this package being
152// identical to the actual testing.testDeps interface, which differs between
153// major versions of Go.
154type LcovTestDeps struct {
155	testdeps.TestDeps
156	OriginalPanicOnExit bool
157}
158
159// SetPanicOnExit0 is called with true by m.Run() before running all tests,
160// and with false right before returning -- after writing all coverage
161// profiles.
162// https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/testing/testing.go;l=1921-1931;drc=refs%2Ftags%2Fgo1.18.1
163//
164// This gives us a good place to intercept the os.Exit(m.Run()) with coverage
165// data already available.
166func (ltd LcovTestDeps) SetPanicOnExit0(panicOnExit bool) {
167	if !panicOnExit {
168		lcovAtExitHook()
169	}
170	ltd.TestDeps.SetPanicOnExit0(ltd.OriginalPanicOnExit)
171}
172
173func lcovAtExitHook() {
174	if err := ConvertCoverToLcov(); err != nil {
175		log.Printf("Failed to collect coverage: %s", err)
176		os.Exit(TestWrapperAbnormalExit)
177	}
178}
179