// 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