blob: 104d604dd7bd600e4960c2f1efca04acdd72989e [file] [log] [blame] [edit]
// Copyright 2022 The Dawn Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cov
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"dawn.googlesource.com/dawn/tools/src/fileutils"
)
// File describes the coverage spans in a single source file.
type File struct {
Path string
Covered SpanList // Spans with coverage
Uncovered SpanList // Compiled spans without coverage
}
// Coverage describes the coverage spans for all the source files for a single
// process invocation.
type Coverage struct {
Files []File
}
// Env holds the environment settings for performing coverage processing.
type Env struct {
LLVMBin string // path to the LLVM bin directory
Binary string // path to the executable binary
TurboCov string // path to turbo-cov (optional)
}
// RuntimeEnv returns the environment variable key=value pair for setting
// LLVM_PROFILE_FILE to coverageFile
func RuntimeEnv(env []string, coverageFile string) string {
return "LLVM_PROFILE_FILE=" + coverageFile
}
// AllSourceFiles returns a *Coverage containing all the source files without
// coverage data. This populates the coverage view with files even if they
// didn't get compiled.
func (e Env) AllSourceFiles() *Coverage {
var ignorePaths = map[string]bool{
//
}
projectRoot := fileutils.DawnRoot()
// Gather all the source files to include them even if there is no coverage
// information produced for these files. This highlights files that aren't
// even compiled.
cov := Coverage{}
allFiles := map[string]struct{}{}
filepath.Walk(filepath.Join(projectRoot, "src"), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(projectRoot, path)
if err != nil || ignorePaths[rel] {
return filepath.SkipDir
}
if !info.IsDir() {
switch filepath.Ext(path) {
case ".h", ".c", ".cc", ".cpp", ".hpp":
if _, seen := allFiles[rel]; !seen {
cov.Files = append(cov.Files, File{Path: rel})
}
}
}
return nil
})
return &cov
}
// Import uses the llvm-profdata and llvm-cov tools to import the coverage
// information from a .profraw file.
func (e Env) Import(profrawPath string) (*Coverage, error) {
llvmProfdataExe := filepath.Join(e.LLVMBin, "llvm-profdata"+fileutils.ExeExt)
llvmCovExe := filepath.Join(e.LLVMBin, "llvm-cov"+fileutils.ExeExt)
profdata := profrawPath + ".profdata"
if err := exec.Command(
llvmProfdataExe,
"merge",
"-sparse",
profrawPath,
"-output",
profdata).Run(); err != nil {
return nil, fmt.Errorf("llvm-profdata errored: %w", err)
}
defer os.Remove(profdata)
if e.TurboCov == "" {
data, err := exec.Command(
llvmCovExe,
"export",
e.Binary,
"-instr-profile="+profdata,
"-format=text",
"-skip-expansions",
"-skip-functions").Output()
if err != nil {
return nil, fmt.Errorf("llvm-cov errored: %v\n%v", string(err.(*exec.ExitError).Stderr), err)
}
cov, err := e.parseCov(data)
if err != nil {
return nil, fmt.Errorf("failed to parse coverage json data: %w", err)
}
return cov, nil
}
data, err := exec.Command(e.TurboCov, e.Binary, profdata).Output()
if err != nil {
return nil, fmt.Errorf("turbo-cov errored: %v\n%v", string(err.(*exec.ExitError).Stderr), err)
}
cov, err := e.parseTurboCov(data)
if err != nil {
return nil, fmt.Errorf("failed to process turbo-cov output: %w", err)
}
return cov, nil
}
func appendSpan(spans []Span, span Span) []Span {
if c := len(spans); c > 0 && spans[c-1].End == span.Start {
spans[c-1].End = span.End
} else {
spans = append(spans, span)
}
return spans
}
// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
// https://stackoverflow.com/a/56792192
func (e Env) parseCov(raw []byte) (*Coverage, error) {
// line int, col int, count int64, hasCount bool, isRegionEntry bool
type segment []interface{}
type file struct {
// expansions ignored
Name string `json:"filename"`
Segments []segment `json:"segments"`
// summary ignored
}
type data struct {
Files []file `json:"files"`
}
root := struct {
Data []data `json:"data"`
}{}
err := json.NewDecoder(bytes.NewReader(raw)).Decode(&root)
if err != nil {
return nil, err
}
projectRoot := fileutils.DawnRoot()
c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
for _, f := range root.Data[0].Files {
relpath, err := filepath.Rel(projectRoot, f.Name)
if err != nil {
return nil, err
}
if strings.HasPrefix(relpath, "..") {
continue
}
file := File{Path: relpath}
for sIdx := 0; sIdx+1 < len(f.Segments); sIdx++ {
start := Location{(int)(f.Segments[sIdx][0].(float64)), (int)(f.Segments[sIdx][1].(float64))}
end := Location{(int)(f.Segments[sIdx+1][0].(float64)), (int)(f.Segments[sIdx+1][1].(float64))}
if covered := f.Segments[sIdx][2].(float64) != 0; covered {
file.Covered = appendSpan(file.Covered, Span{start, end})
} else {
file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
}
}
if len(file.Covered) > 0 {
c.Files = append(c.Files, file)
}
}
return c, nil
}
// parseTurboCov parses coverage information from a `turbo-cov` file.
// See tools/src/cmd/turbo-cov/README.md for more information
func (e Env) parseTurboCov(data []byte) (*Coverage, error) {
u32 := func() uint32 {
out := binary.LittleEndian.Uint32(data)
data = data[4:]
return out
}
u8 := func() uint8 {
out := data[0]
data = data[1:]
return out
}
str := func() string {
len := u32()
out := data[:len]
data = data[len:]
return string(out)
}
projectRoot := fileutils.DawnRoot()
numFiles := u32()
c := &Coverage{Files: make([]File, 0, numFiles)}
for i := 0; i < int(numFiles); i++ {
path := str()
relpath, err := filepath.Rel(projectRoot, path)
if err != nil {
return nil, err
}
if strings.HasPrefix(relpath, "..") {
continue
}
file := File{Path: relpath}
type segment struct {
location Location
count int
covered bool
}
numSegements := u32()
segments := make([]segment, numSegements)
for j := range segments {
segment := &segments[j]
segment.location.Line = int(u32())
segment.location.Column = int(u32())
segment.count = int(u32())
segment.covered = u8() != 0
}
for sIdx := 0; sIdx+1 < len(segments); sIdx++ {
start := segments[sIdx].location
end := segments[sIdx+1].location
if segments[sIdx].covered {
if segments[sIdx].count > 0 {
file.Covered = appendSpan(file.Covered, Span{start, end})
} else {
file.Uncovered = appendSpan(file.Uncovered, Span{start, end})
}
}
}
if len(file.Covered) > 0 {
c.Files = append(c.Files, file)
}
}
return c, nil
}
// Path uniquely identifies a test that was run to produce coverage.
// Paths are split into a hierarchical sequence of strings, where the 0'th
// string represents the root of the hierarchy and the last string is typically
// the leaf name of the test.
type Path []string