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  }