import (
"bufio"
+ "bytes"
+ "encoding/json"
+ "errors"
"fmt"
"go/ast"
- "go/build"
"go/parser"
"go/token"
+ "io"
"os"
+ "os/exec"
+ "path"
"path/filepath"
+ "runtime"
+ "strings"
"text/tabwriter"
)
return err
}
+ dirs, err := findPkgs(profiles)
+ if err != nil {
+ return err
+ }
+
var out *bufio.Writer
if outputFile == "" {
out = bufio.NewWriter(os.Stdout)
var total, covered int64
for _, profile := range profiles {
fn := profile.FileName
- file, err := findFile(fn)
+ file, err := findFile(dirs, fn)
if err != nil {
return err
}
return covered, total
}
-// findFile finds the location of the named file in GOROOT, GOPATH etc.
-func findFile(file string) (string, error) {
- dir, file := filepath.Split(file)
- pkg, err := build.Import(dir, ".", build.FindOnly)
+// Pkg describes a single package, compatible with the JSON output from 'go list'; see 'go help list'.
+type Pkg struct {
+ ImportPath string
+ Dir string
+ Error *struct {
+ Err string
+ }
+}
+
+func findPkgs(profiles []*Profile) (map[string]*Pkg, error) {
+ // Run go list to find the location of every package we care about.
+ pkgs := make(map[string]*Pkg)
+ var list []string
+ for _, profile := range profiles {
+ if strings.HasPrefix(profile.FileName, ".") || filepath.IsAbs(profile.FileName) {
+ // Relative or absolute path.
+ continue
+ }
+ pkg := path.Dir(profile.FileName)
+ if _, ok := pkgs[pkg]; !ok {
+ pkgs[pkg] = nil
+ list = append(list, pkg)
+ }
+ }
+
+ // Note: usually run as "go tool cover" in which case $GOROOT is set,
+ // in which case runtime.GOROOT() does exactly what we want.
+ goTool := filepath.Join(runtime.GOROOT(), "bin/go")
+ cmd := exec.Command(goTool, append([]string{"list", "-e", "-json"}, list...)...)
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+ stdout, err := cmd.Output()
if err != nil {
- return "", fmt.Errorf("can't find %q: %v", file, err)
+ return nil, fmt.Errorf("cannot run go list: %v\n%s", err, stderr.Bytes())
+ }
+ dec := json.NewDecoder(bytes.NewReader(stdout))
+ for {
+ var pkg Pkg
+ err := dec.Decode(&pkg)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, fmt.Errorf("decoding go list json: %v", err)
+ }
+ pkgs[pkg.ImportPath] = &pkg
+ }
+ return pkgs, nil
+}
+
+// findFile finds the location of the named file in GOROOT, GOPATH etc.
+func findFile(pkgs map[string]*Pkg, file string) (string, error) {
+ if strings.HasPrefix(file, ".") || filepath.IsAbs(file) {
+ // Relative or absolute path.
+ return file, nil
+ }
+ pkg := pkgs[path.Dir(file)]
+ if pkg != nil {
+ if pkg.Dir != "" {
+ return filepath.Join(pkg.Dir, path.Base(file)), nil
+ }
+ if pkg.Error != nil {
+ return "", errors.New(pkg.Error.Err)
+ }
}
- return filepath.Join(pkg.Dir, file), nil
+ return "", fmt.Errorf("did not find package for %s in go list output", file)
}
func percent(covered, total int64) float64 {
coverFiles = append(coverFiles, p.GoFiles...)
coverFiles = append(coverFiles, p.CgoFiles...)
coverFiles = append(coverFiles, p.TestGoFiles...)
- p.Internal.CoverVars = declareCoverVars(p.ImportPath, coverFiles...)
+ p.Internal.CoverVars = declareCoverVars(p, coverFiles...)
if testCover && testCoverMode == "atomic" {
ensureImport(p, "sync/atomic")
}
// declareCoverVars attaches the required cover variables names
// to the files, to be used when annotating the files.
-func declareCoverVars(importPath string, files ...string) map[string]*load.CoverVar {
+func declareCoverVars(p *load.Package, files ...string) map[string]*load.CoverVar {
coverVars := make(map[string]*load.CoverVar)
coverIndex := 0
// We create the cover counters as new top-level variables in the package.
// so we append 12 hex digits from the SHA-256 of the import path.
// The point is only to avoid accidents, not to defeat users determined to
// break things.
- sum := sha256.Sum256([]byte(importPath))
+ sum := sha256.Sum256([]byte(p.ImportPath))
h := fmt.Sprintf("%x", sum[:6])
for _, file := range files {
if isTestFile(file) {
continue
}
+ // For a package that is "local" (imported via ./ import or command line, outside GOPATH),
+ // we record the full path to the file name.
+ // Otherwise we record the import path, then a forward slash, then the file name.
+ // This makes profiles within GOPATH file system-independent.
+ // These names appear in the cmd/cover HTML interface.
+ var longFile string
+ if p.Internal.Local {
+ longFile = filepath.Join(p.Dir, file)
+ } else {
+ longFile = path.Join(p.ImportPath, file)
+ }
coverVars[file] = &load.CoverVar{
- File: filepath.Join(importPath, file),
+ File: longFile,
Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
}
coverIndex++