github.com/tiagovtristao/plz@v13.4.0+incompatible/src/test/coverage.go (about) 1 // Code for parsing coverage output in various formats. 2 3 package test 4 5 import ( 6 "bytes" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14 15 "github.com/thought-machine/please/src/cli" 16 "github.com/thought-machine/please/src/core" 17 ) 18 19 // Parses test coverage for a single target from its output file. 20 func parseTestCoverage(target *core.BuildTarget, outputFile string) (core.TestCoverage, error) { 21 coverage := core.NewTestCoverage() 22 data, err := ioutil.ReadFile(outputFile) 23 if err != nil && os.IsNotExist(err) { 24 return coverage, nil // Tests aren't required to produce coverage files. 25 } else if err != nil { 26 return coverage, err 27 } else if len(data) == 0 { 28 return coverage, fmt.Errorf("Empty coverage output") 29 } else if looksLikeGoCoverageResults(data) { 30 // TODO(pebers): this is a little wasteful, we've already read the file once and we must do it again. 31 return coverage, parseGoCoverageResults(target, &coverage, outputFile) 32 } else if looksLikeGcovCoverageResults(data) { 33 return coverage, parseGcovCoverageResults(target, &coverage, data) 34 } else if looksLikeIstanbulCoverageResults(data) { 35 return coverage, parseIstanbulCoverageResults(target, &coverage, data) 36 } else { 37 return coverage, parseXMLCoverageResults(target, &coverage, data) 38 } 39 } 40 41 // AddOriginalTargetsToCoverage adds empty coverage entries for any files covered by the original 42 // query that we haven't discovered through tests to the overall report. 43 // The coverage reports only contain information about files that were covered during 44 // tests, so it's important that we identify anything with zero coverage here. 45 // This is made trickier by attempting to reconcile coverage targets from languages like 46 // Java that don't preserve the original file structure, which requires a slightly fuzzy match. 47 func AddOriginalTargetsToCoverage(state *core.BuildState, includeAllFiles bool) { 48 // First we collect all the source files from all relevant targets 49 allFiles := map[string]bool{} 50 doneTargets := map[*core.BuildTarget]bool{} 51 // Track the set of packages the user ran tests from; we only show coverage metrics from them. 52 coveragePackages := map[string]bool{} 53 for _, label := range state.OriginalTargets { 54 coveragePackages[label.PackageName] = true 55 } 56 for _, label := range state.ExpandOriginalTargets() { 57 collectAllFiles(state, state.Graph.TargetOrDie(label), coveragePackages, allFiles, doneTargets, includeAllFiles) 58 } 59 60 // Now merge the recorded coverage so far into them 61 recordedCoverage := state.Coverage 62 state.Coverage = core.TestCoverage{Tests: recordedCoverage.Tests, Files: map[string][]core.LineCoverage{}} 63 mergeCoverage(state, recordedCoverage, coveragePackages, allFiles, includeAllFiles) 64 } 65 66 // Collects all the source files from a single target 67 func collectAllFiles(state *core.BuildState, target *core.BuildTarget, coveragePackages, allFiles map[string]bool, doneTargets map[*core.BuildTarget]bool, includeAllFiles bool) { 68 doneTargets[target] = true 69 if !includeAllFiles && !coveragePackages[target.Label.PackageName] { 70 return 71 } 72 // Small hack here; explore these targets when we don't have any sources yet. Helps languages 73 // like Java where we generate a wrapper target with a complete one immediately underneath. 74 // TODO(pebers): do we still need this now we have Java sourcemaps? 75 if !target.OutputIsComplete || len(allFiles) == 0 { 76 for _, dep := range target.Dependencies() { 77 if !doneTargets[dep] { 78 collectAllFiles(state, dep, coveragePackages, allFiles, doneTargets, includeAllFiles) 79 } 80 } 81 } 82 if target.IsTest { 83 return // Test sources don't count for coverage. 84 } 85 for _, path := range target.AllSourcePaths(state.Graph) { 86 extension := filepath.Ext(path) 87 for _, ext := range state.Config.Cover.FileExtension { 88 if ext == extension { 89 allFiles[path] = target.IsTest || target.TestOnly // Skip test source files from actual coverage display 90 break 91 } 92 } 93 } 94 } 95 96 // mergeCoverage merges recorded coverage with the list of all existing files. 97 func mergeCoverage(state *core.BuildState, recordedCoverage core.TestCoverage, coveragePackages, allFiles map[string]bool, includeAllFiles bool) { 98 for file, coverage := range recordedCoverage.Files { 99 if includeAllFiles || isOwnedBy(file, coveragePackages) { 100 state.Coverage.Files[file] = coverage 101 allFiles[file] = true 102 } 103 } 104 // For any files left over now, enter them in as 100% uncovered. 105 // This is pessimistic but there's not much we can do at this point. 106 for file, done := range allFiles { 107 if !done { 108 s := make([]core.LineCoverage, countLines(file)) 109 if len(s) > 0 { 110 for i := 0; i < len(s); i++ { 111 s[i] = core.Uncovered 112 } 113 state.Coverage.Files[file] = s 114 } 115 } 116 } 117 } 118 119 // isOwnedBy returns true if the given file is owned by any of the given packages. 120 func isOwnedBy(file string, coveragePackages map[string]bool) bool { 121 for file != "." && file != "/" { 122 file = path.Dir(file) 123 if coveragePackages[file] { 124 return true 125 } 126 } 127 return false 128 } 129 130 // countLines returns the number of lines in a file. 131 func countLines(path string) int { 132 data, _ := ioutil.ReadFile(path) 133 return bytes.Count(data, []byte{'\n'}) 134 } 135 136 // WriteCoverageToFileOrDie writes the collected coverage data to a file in JSON format. Dies on failure. 137 func WriteCoverageToFileOrDie(coverage core.TestCoverage, filename string) { 138 out := jsonCoverage{Tests: map[string]map[string]string{}} 139 allowedFiles := coverage.OrderedFiles() 140 141 for label, coverage := range coverage.Tests { 142 out.Tests[label.String()] = convertCoverage(coverage, allowedFiles) 143 } 144 145 out.Files = convertCoverage(coverage.Files, allowedFiles) 146 out.Stats = getStats(coverage) 147 if b, err := json.MarshalIndent(out, "", " "); err != nil { 148 log.Fatalf("Failed to encode json: %s", err) 149 } else if err := ioutil.WriteFile(filename, b, 0644); err != nil { 150 log.Fatalf("Failed to write coverage results to %s: %s", filename, err) 151 } 152 } 153 154 // WriteXMLCoverageToFileOrDie writes the collected coverage data to a file in XML format. Dies on failure. 155 func WriteXMLCoverageToFileOrDie(sources []core.BuildLabel, coverage core.TestCoverage, filename string) { 156 data := coverageResultToXML(sources, coverage) 157 158 if err := ioutil.WriteFile(filename, data, 0644); err != nil { 159 log.Fatalf("Failed to write coverage results to %s: %s", filename, err) 160 } 161 } 162 163 // CountCoverage counts the number of lines covered and the total number coverable in a single file. 164 func CountCoverage(lines []core.LineCoverage) (int, int) { 165 covered := 0 166 total := 0 167 for _, line := range lines { 168 if line == core.Covered { 169 total++ 170 covered++ 171 } else if line != core.NotExecutable { 172 total++ 173 } 174 } 175 return covered, total 176 } 177 178 func getStats(coverage core.TestCoverage) stats { 179 stats := stats{CoverageByFile: map[string]float32{}} 180 totalLinesCovered := 0 181 totalCoverableLines := 0 182 for _, file := range coverage.OrderedFiles() { 183 covered, total := CountCoverage(coverage.Files[file]) 184 totalLinesCovered += covered 185 totalCoverableLines += total 186 if total > 0 { 187 stats.CoverageByFile[file] = 100.0 * float32(covered) / float32(total) 188 } 189 } 190 if totalCoverableLines > 0 { 191 stats.TotalCoverage = 100.0 * float32(totalLinesCovered) / float32(totalCoverableLines) 192 } 193 return stats 194 } 195 196 func convertCoverage(in map[string][]core.LineCoverage, allowedFiles []string) map[string]string { 197 ret := map[string]string{} 198 for k, v := range in { 199 if cli.ContainsString(k, allowedFiles) { 200 ret[k] = core.TestCoverageString(v) 201 } 202 } 203 return ret 204 } 205 206 // Used to prepare core.TestCoverage objects for JSON marshalling. 207 type jsonCoverage struct { 208 Tests map[string]map[string]string `json:"tests"` 209 Files map[string]string `json:"files"` 210 Stats stats `json:"stats"` 211 } 212 213 // stats is a struct describing summarised coverage stats. 214 type stats struct { 215 TotalCoverage float32 `json:"total_coverage"` 216 CoverageByFile map[string]float32 `json:"coverage_by_file"` 217 } 218 219 // RemoveFilesFromCoverage removes any files with extensions matching the given set from coverage. 220 func RemoveFilesFromCoverage(coverage core.TestCoverage, extensions []string) { 221 for _, files := range coverage.Tests { 222 removeFilesFromCoverage(files, extensions) 223 } 224 removeFilesFromCoverage(coverage.Files, extensions) 225 } 226 227 func removeFilesFromCoverage(files map[string][]core.LineCoverage, extensions []string) { 228 for filename := range files { 229 for _, ext := range extensions { 230 if strings.HasSuffix(filename, ext) { 231 delete(files, filename) 232 } 233 } 234 } 235 }