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