| // Copyright 2013 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // This file implements the visitor that computes the (line, column)-(line-column) range for each function. | |
| package main | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "go/ast" | |
| "go/parser" | |
| "go/token" | |
| "io" | |
| "os" | |
| "os/exec" | |
| "path" | |
| "path/filepath" | |
| "runtime" | |
| "strings" | |
| "text/tabwriter" | |
| "golang.org/x/tools/cover" | |
| ) | |
| // funcOutput takes two file names as arguments, a coverage profile to read as input and an output | |
| // file to write ("" means to write to standard output). The function reads the profile and produces | |
| // as output the coverage data broken down by function, like this: | |
| // | |
| // fmt/format.go:30: init 100.0% | |
| // fmt/format.go:57: clearflags 100.0% | |
| // ... | |
| // fmt/scan.go:1046: doScan 100.0% | |
| // fmt/scan.go:1075: advance 96.2% | |
| // fmt/scan.go:1119: doScanf 96.8% | |
| // total: (statements) 91.9% | |
| func funcOutput(profile, outputFile string) error { | |
| profiles, err := cover.ParseProfiles(profile) | |
| if err != nil { | |
| return err | |
| } | |
| dirs, err := findPkgs(profiles) | |
| if err != nil { | |
| return err | |
| } | |
| var out *bufio.Writer | |
| if outputFile == "" { | |
| out = bufio.NewWriter(os.Stdout) | |
| } else { | |
| fd, err := os.Create(outputFile) | |
| if err != nil { | |
| return err | |
| } | |
| defer fd.Close() | |
| out = bufio.NewWriter(fd) | |
| } | |
| defer out.Flush() | |
| tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) | |
| defer tabber.Flush() | |
| var total, covered int64 | |
| for _, profile := range profiles { | |
| fn := profile.FileName | |
| file, err := findFile(dirs, fn) | |
| if err != nil { | |
| return err | |
| } | |
| funcs, err := findFuncs(file) | |
| if err != nil { | |
| return err | |
| } | |
| // Now match up functions and profile blocks. | |
| for _, f := range funcs { | |
| c, t := f.coverage(profile) | |
| fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, percent(c, t)) | |
| total += t | |
| covered += c | |
| } | |
| } | |
| fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", percent(covered, total)) | |
| return nil | |
| } | |
| // findFuncs parses the file and returns a slice of FuncExtent descriptors. | |
| func findFuncs(name string) ([]*FuncExtent, error) { | |
| fset := token.NewFileSet() | |
| parsedFile, err := parser.ParseFile(fset, name, nil, parser.SkipObjectResolution) | |
| if err != nil { | |
| return nil, err | |
| } | |
| visitor := &FuncVisitor{ | |
| fset: fset, | |
| name: name, | |
| astFile: parsedFile, | |
| } | |
| ast.Walk(visitor, visitor.astFile) | |
| return visitor.funcs, nil | |
| } | |
| // FuncExtent describes a function's extent in the source by file and position. | |
| type FuncExtent struct { | |
| name string | |
| startLine int | |
| startCol int | |
| endLine int | |
| endCol int | |
| } | |
| // FuncVisitor implements the visitor that builds the function position list for a file. | |
| type FuncVisitor struct { | |
| fset *token.FileSet | |
| name string // Name of file. | |
| astFile *ast.File | |
| funcs []*FuncExtent | |
| } | |
| // Visit implements the ast.Visitor interface. | |
| func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { | |
| switch n := node.(type) { | |
| case *ast.FuncDecl: | |
| if n.Body == nil { | |
| // Do not count declarations of assembly functions. | |
| break | |
| } | |
| start := v.fset.Position(n.Pos()) | |
| end := v.fset.Position(n.End()) | |
| fe := &FuncExtent{ | |
| name: n.Name.Name, | |
| startLine: start.Line, | |
| startCol: start.Column, | |
| endLine: end.Line, | |
| endCol: end.Column, | |
| } | |
| v.funcs = append(v.funcs, fe) | |
| } | |
| return v | |
| } | |
| // coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. | |
| func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { | |
| // We could avoid making this n^2 overall by doing a single scan and annotating the functions, | |
| // but the sizes of the data structures is never very large and the scan is almost instantaneous. | |
| var covered, total int64 | |
| // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. | |
| for _, b := range profile.Blocks { | |
| if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { | |
| // Past the end of the function. | |
| break | |
| } | |
| if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { | |
| // Before the beginning of the function | |
| continue | |
| } | |
| total += int64(b.NumStmt) | |
| if b.Count > 0 { | |
| covered += int64(b.NumStmt) | |
| } | |
| } | |
| return covered, total | |
| } | |
| // 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 []*cover.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) | |
| } | |
| } | |
| if len(list) == 0 { | |
| return pkgs, nil | |
| } | |
| // 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 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 "", fmt.Errorf("did not find package for %s in go list output", file) | |
| } | |
| func percent(covered, total int64) float64 { | |
| if total == 0 { | |
| total = 1 // Avoid zero denominator. | |
| } | |
| return 100.0 * float64(covered) / float64(total) | |
| } |