blob: f1d936b5900e2c2b4901a3facc0d36262d340734 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"bytes"
"debug/elf"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"fuchsia.googlesource.com/tools/elflib"
"fuchsia.googlesource.com/tools/symbolize"
)
var (
summaryFile string
idsFile string
symbolizeDumpFile string
dryRun bool
verbose bool
outputDir string
llvmCov string
llvmProfdata string
outputFormat string
)
func init() {
flag.StringVar(&summaryFile, "summary", "", "path to summary.json file")
flag.StringVar(&idsFile, "ids", "", "path to ids.txt")
flag.StringVar(&symbolizeDumpFile, "symbolize-dump", "", "path to the json emited from the symbolizer")
flag.BoolVar(&dryRun, "dry-run", false, "if set the system prints out commands that would be run instead of running them")
flag.BoolVar(&verbose, "v", false, "if set the all commands will be printed out before being run")
flag.StringVar(&outputDir, "output-dir", "", "the directory to output results to")
flag.StringVar(&llvmProfdata, "llvm-profdata", "llvm-profdata", "the location of llvm-profdata")
flag.StringVar(&llvmCov, "llvm-cov", "llvm-cov", "the location of llvm-cov")
flag.StringVar(&outputFormat, "format", "html", "the output format used for llvm-cov")
}
const llvmProfileSinkType = "llvm-profile"
type Test struct {
Name string `json:"name"`
DataSinks map[string][]struct {
Name string `json:"name"`
File string `json:"file"`
} `json:"data_sinks,omitempty"`
}
type Summary struct {
Tests []Test `json:"tests"`
}
// Output is indexed by dump name
func readSummary(summaryFile string) (map[string]Test, error) {
file, err := os.Open(summaryFile)
if err != nil {
return nil, fmt.Errorf("opening file %s: %v", summaryFile, err)
}
defer file.Close()
var summary Summary
dec := json.NewDecoder(file)
if err := dec.Decode(&summary); err != nil {
return nil, err
}
out := make(map[string]Test)
for _, test := range summary.Tests {
for _, profileDump := range test.DataSinks[llvmProfileSinkType] {
out[profileDump.Name] = test
}
}
return out, nil
}
type SymbolizerTriggerDump []struct {
Mods []symbolize.Module `json:"modules"`
SinkType string `json:"type"`
DumpName string `json:"name"`
}
func readSymbolizerDump(dumpFile string) (SymbolizerTriggerDump, error) {
file, err := os.Open(dumpFile)
if err != nil {
return nil, err
}
defer file.Close()
var symbolizeDump SymbolizerTriggerDump
dec := json.NewDecoder(file)
if err := dec.Decode(&symbolizeDump); err != nil {
return nil, err
}
return symbolizeDump, nil
}
// Output is indexed by build id
func readIDsTxt(idsFile string) (map[string]elflib.BinaryFileRef, error) {
file, err := os.Open(idsFile)
if err != nil {
return nil, err
}
defer file.Close()
refs, err := elflib.ReadIDsFile(file)
if err != nil {
return nil, err
}
out := make(map[string]elflib.BinaryFileRef)
for _, ref := range refs {
out[ref.BuildID] = ref
}
return out, nil
}
type indexedInfo struct {
symbolizeDump SymbolizerTriggerDump
summary map[string]Test
ids map[string]elflib.BinaryFileRef
}
type CovDataEntry struct {
Dump string
Test string
CovDataFile string
ModuleFiles []string
}
func readInfo(dumpFile, summaryFile, idsFile string) (*indexedInfo, error) {
summary, err := readSummary(summaryFile)
if err != nil {
return nil, fmt.Errorf("parsing %s: %v", summaryFile, err)
}
symbolizeDump, err := readSymbolizerDump(symbolizeDumpFile)
if err != nil {
return nil, fmt.Errorf("parsing %s: %v", dumpFile, err)
}
ids, err := readIDsTxt(idsFile)
if err != nil {
return nil, fmt.Errorf("parsing %s: %v", idsFile, err)
}
return &indexedInfo{
symbolizeDump: symbolizeDump,
summary: summary,
ids: ids,
}, nil
}
func mergeInfo(prefix string, info *indexedInfo) ([]CovDataEntry, error) {
out := []CovDataEntry{}
for _, dump := range info.symbolizeDump {
// If we get a dumpfile result that isn't from llvm-profile, ignore it
if dump.SinkType != llvmProfileSinkType {
continue
}
// Get the test data and make sure there are no errors
test, ok := info.summary[dump.DumpName]
if !ok {
fmt.Fprintf(os.Stderr, "WARN: %s not found in summary file\n", dump.DumpName)
continue
}
// This is going to go in a covDataEntry as the location of the coverage data
covDataFile := filepath.Join(prefix, test.DataSinks[llvmProfileSinkType][0].File)
// This is going to go in a covDataEntry as the list of paths to the modules for the data
moduleFiles := []string{}
for _, mod := range dump.Mods {
if fileRef, ok := info.ids[mod.Build]; ok {
moduleFiles = append(moduleFiles, fileRef.Filepath)
} else {
return nil, fmt.Errorf("module with build id %s not found in ids.txt file", mod.Build)
}
}
// Finally we can add all the data
out = append(out, CovDataEntry{
Dump: dump.DumpName,
Test: test.Name,
ModuleFiles: moduleFiles,
CovDataFile: covDataFile,
})
}
return out, nil
}
type Action struct {
Path string `json:"cmd"`
Args []string `json:"args"`
}
func (a Action) Run() ([]byte, error) {
if dryRun || verbose {
fmt.Println(a.String())
}
if !dryRun {
return exec.Command(a.Path, a.Args...).CombinedOutput()
}
return nil, nil
}
func (a Action) String() string {
var buf bytes.Buffer
fmt.Fprint(&buf, a.Path)
for _, arg := range a.Args {
fmt.Fprintf(&buf, " %s", arg)
}
return buf.String()
}
func isInstrumented(filepath string) bool {
sections := []string{"__llvm_covmap", "__llvm_prf_names"}
file, err := os.Open(filepath)
if err != nil {
return false
}
defer file.Close()
elfFile, err := elf.NewFile(file)
if err != nil {
return false
}
for _, section := range sections {
if sec := elfFile.Section(section); sec == nil {
return false
}
}
return true
}
func process() error {
// Make the output directory
err := os.MkdirAll(outputDir, os.ModePerm)
if err != nil {
return fmt.Errorf("creating output dir %s: %v", outputDir, err)
}
// Read in all the data
info, err := readInfo(symbolizeDumpFile, summaryFile, idsFile)
if err != nil {
return fmt.Errorf("parsing info: %v", err)
}
// Merge all the information
entries, err := mergeInfo(filepath.Dir(summaryFile), info)
if err != nil {
return fmt.Errorf("merging info: %v", err)
}
// Gather the set of modules and coverage files
modSet := make(map[string]struct{})
var mods []string
var covFiles []string
for _, entry := range entries {
for _, mod := range entry.ModuleFiles {
if _, ok := modSet[mod]; !ok && isInstrumented(mod) {
mods = append(mods, mod)
modSet[mod] = struct{}{}
}
}
covFiles = append(covFiles, entry.CovDataFile)
}
dir, err := ioutil.TempDir("", "covargs")
if err != nil {
return fmt.Errorf("cannot create temporary dir: %v", err)
}
defer os.RemoveAll(dir)
// Make the llvm-profdata response file
profdataFile, err := os.Create(filepath.Join(dir, "llvm-profdata.rsp"))
if err != nil {
return fmt.Errorf("creating llvm-profdata.rsp file: %v", err)
}
for _, covFile := range covFiles {
fmt.Fprintf(profdataFile, "%s\n", covFile)
}
profdataFile.Close()
// Merge everything
mergedFile := filepath.Join(dir, "merged.profdata")
mergeCmd := Action{Path: llvmProfdata, Args: []string{
"merge",
"-o", mergedFile,
"@" + profdataFile.Name(),
}}
data, err := mergeCmd.Run()
if err != nil {
return fmt.Errorf("%v:\n%s", err, string(data))
}
// Make the llvm-cov response file
covFile, err := os.Create(filepath.Join(dir, "llvm-cov.rsp"))
if err != nil {
return fmt.Errorf("creating llvm-cov.rsp file: %v", err)
}
for _, mod := range mods {
fmt.Fprintf(covFile, "-object %s\n", mod)
}
covFile.Close()
// Produce output
showCmd := Action{Path: llvmCov, Args: []string{
"show",
"-format", outputFormat,
"-instr-profile", mergedFile,
"-output-dir", outputDir,
"@" + covFile.Name(),
}}
data, err = showCmd.Run()
if err != nil {
return fmt.Errorf("%v:\n%s", err, string(data))
}
return nil
}
func main() {
flag.Parse()
if err := process(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}