You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
7.1 KiB

// Copyright 2020 The SwiftShader Authors. All Rights Reserved.
//
// 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"
"os"
"os/exec"
"path/filepath"
"strings"
"../cause"
"../llvm"
)
// 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 enviroment settings for performing coverage processing.
type Env struct {
LLVM llvm.Toolchain
RootDir string // path to SwiftShader git root directory
ExePath string // path to the executable binary
TurboCov string // path to turbo-cov (optional)
}
// AppendRuntimeEnv returns the environment variables env with the
// LLVM_PROFILE_FILE environment variable appended.
func AppendRuntimeEnv(env []string, coverageFile string) []string {
return append(env, "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{
"src/Common": true,
"src/Main": true,
"src/OpenGL": true,
"src/Renderer": true,
"src/Shader": true,
"src/System": true,
}
// 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(e.RootDir, "src"), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(e.RootDir, 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) {
profdata := profrawPath + ".profdata"
if err := exec.Command(e.LLVM.Profdata(), "merge", "-sparse", profrawPath, "-output", profdata).Run(); err != nil {
return nil, cause.Wrap(err, "llvm-profdata errored")
}
defer os.Remove(profdata)
if e.TurboCov == "" {
args := []string{
"export",
e.ExePath,
"-instr-profile=" + profdata,
"-format=text",
}
if e.LLVM.Version.GreaterEqual(llvm.Version{Major: 9}) {
// LLVM 9 has new flags that omit stuff we don't care about.
args = append(args,
"-skip-expansions",
"-skip-functions",
)
}
data, err := exec.Command(e.LLVM.Cov(), args...).Output()
if err != nil {
return nil, cause.Wrap(err, "llvm-cov errored: %v", string(err.(*exec.ExitError).Stderr))
}
cov, err := e.parseCov(data)
if err != nil {
return nil, cause.Wrap(err, "Couldn't parse coverage json data")
}
return cov, nil
}
data, err := exec.Command(e.TurboCov, e.ExePath, profdata).Output()
if err != nil {
return nil, cause.Wrap(err, "turbo-cov errored: %v", string(err.(*exec.ExitError).Stderr))
}
cov, err := e.parseTurboCov(data)
if err != nil {
return nil, cause.Wrap(err, "Couldn't process turbo-cov output")
}
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
}
c := &Coverage{Files: make([]File, 0, len(root.Data[0].Files))}
for _, f := range root.Data[0].Files {
relpath, err := filepath.Rel(e.RootDir, 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
}
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)
}
numFiles := u32()
c := &Coverage{Files: make([]File, 0, numFiles)}
for i := 0; i < int(numFiles); i++ {
path := str()
relpath, err := filepath.Rel(e.RootDir, 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 is a tree node path formed from a list of strings
type Path []string