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