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