1// Copyright 2013 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// This file implements the visitor that computes the (line, column)-(line-column) range for each function. 6 7package main 8 9import ( 10 "bufio" 11 "bytes" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "go/ast" 16 "go/parser" 17 "go/token" 18 "io" 19 "os" 20 "os/exec" 21 "path" 22 "path/filepath" 23 "runtime" 24 "strings" 25 "text/tabwriter" 26 27 "golang.org/x/tools/cover" 28) 29 30// funcOutput takes two file names as arguments, a coverage profile to read as input and an output 31// file to write ("" means to write to standard output). The function reads the profile and produces 32// as output the coverage data broken down by function, like this: 33// 34// fmt/format.go:30: init 100.0% 35// fmt/format.go:57: clearflags 100.0% 36// ... 37// fmt/scan.go:1046: doScan 100.0% 38// fmt/scan.go:1075: advance 96.2% 39// fmt/scan.go:1119: doScanf 96.8% 40// total: (statements) 91.9% 41 42func funcOutput(profile, outputFile string) error { 43 profiles, err := cover.ParseProfiles(profile) 44 if err != nil { 45 return err 46 } 47 48 dirs, err := findPkgs(profiles) 49 if err != nil { 50 return err 51 } 52 53 var out *bufio.Writer 54 if outputFile == "" { 55 out = bufio.NewWriter(os.Stdout) 56 } else { 57 fd, err := os.Create(outputFile) 58 if err != nil { 59 return err 60 } 61 defer fd.Close() 62 out = bufio.NewWriter(fd) 63 } 64 defer out.Flush() 65 66 tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) 67 defer tabber.Flush() 68 69 var total, covered int64 70 for _, profile := range profiles { 71 fn := profile.FileName 72 file, err := findFile(dirs, fn) 73 if err != nil { 74 return err 75 } 76 funcs, err := findFuncs(file) 77 if err != nil { 78 return err 79 } 80 // Now match up functions and profile blocks. 81 for _, f := range funcs { 82 c, t := f.coverage(profile) 83 fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, percent(c, t)) 84 total += t 85 covered += c 86 } 87 } 88 fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", percent(covered, total)) 89 90 return nil 91} 92 93// findFuncs parses the file and returns a slice of FuncExtent descriptors. 94func findFuncs(name string) ([]*FuncExtent, error) { 95 fset := token.NewFileSet() 96 parsedFile, err := parser.ParseFile(fset, name, nil, 0) 97 if err != nil { 98 return nil, err 99 } 100 visitor := &FuncVisitor{ 101 fset: fset, 102 name: name, 103 astFile: parsedFile, 104 } 105 ast.Walk(visitor, visitor.astFile) 106 return visitor.funcs, nil 107} 108 109// FuncExtent describes a function's extent in the source by file and position. 110type FuncExtent struct { 111 name string 112 startLine int 113 startCol int 114 endLine int 115 endCol int 116} 117 118// FuncVisitor implements the visitor that builds the function position list for a file. 119type FuncVisitor struct { 120 fset *token.FileSet 121 name string // Name of file. 122 astFile *ast.File 123 funcs []*FuncExtent 124} 125 126// Visit implements the ast.Visitor interface. 127func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { 128 switch n := node.(type) { 129 case *ast.FuncDecl: 130 if n.Body == nil { 131 // Do not count declarations of assembly functions. 132 break 133 } 134 start := v.fset.Position(n.Pos()) 135 end := v.fset.Position(n.End()) 136 fe := &FuncExtent{ 137 name: n.Name.Name, 138 startLine: start.Line, 139 startCol: start.Column, 140 endLine: end.Line, 141 endCol: end.Column, 142 } 143 v.funcs = append(v.funcs, fe) 144 } 145 return v 146} 147 148// coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. 149func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { 150 // We could avoid making this n^2 overall by doing a single scan and annotating the functions, 151 // but the sizes of the data structures is never very large and the scan is almost instantaneous. 152 var covered, total int64 153 // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. 154 for _, b := range profile.Blocks { 155 if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { 156 // Past the end of the function. 157 break 158 } 159 if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { 160 // Before the beginning of the function 161 continue 162 } 163 total += int64(b.NumStmt) 164 if b.Count > 0 { 165 covered += int64(b.NumStmt) 166 } 167 } 168 return covered, total 169} 170 171// Pkg describes a single package, compatible with the JSON output from 'go list'; see 'go help list'. 172type Pkg struct { 173 ImportPath string 174 Dir string 175 Error *struct { 176 Err string 177 } 178} 179 180func findPkgs(profiles []*cover.Profile) (map[string]*Pkg, error) { 181 // Run go list to find the location of every package we care about. 182 pkgs := make(map[string]*Pkg) 183 var list []string 184 for _, profile := range profiles { 185 if strings.HasPrefix(profile.FileName, ".") || filepath.IsAbs(profile.FileName) { 186 // Relative or absolute path. 187 continue 188 } 189 pkg := path.Dir(profile.FileName) 190 if _, ok := pkgs[pkg]; !ok { 191 pkgs[pkg] = nil 192 list = append(list, pkg) 193 } 194 } 195 196 if len(list) == 0 { 197 return pkgs, nil 198 } 199 200 // Note: usually run as "go tool cover" in which case $GOROOT is set, 201 // in which case runtime.GOROOT() does exactly what we want. 202 goTool := filepath.Join(runtime.GOROOT(), "bin/go") 203 cmd := exec.Command(goTool, append([]string{"list", "-e", "-json"}, list...)...) 204 var stderr bytes.Buffer 205 cmd.Stderr = &stderr 206 stdout, err := cmd.Output() 207 if err != nil { 208 return nil, fmt.Errorf("cannot run go list: %v\n%s", err, stderr.Bytes()) 209 } 210 dec := json.NewDecoder(bytes.NewReader(stdout)) 211 for { 212 var pkg Pkg 213 err := dec.Decode(&pkg) 214 if err == io.EOF { 215 break 216 } 217 if err != nil { 218 return nil, fmt.Errorf("decoding go list json: %v", err) 219 } 220 pkgs[pkg.ImportPath] = &pkg 221 } 222 return pkgs, nil 223} 224 225// findFile finds the location of the named file in GOROOT, GOPATH etc. 226func findFile(pkgs map[string]*Pkg, file string) (string, error) { 227 if strings.HasPrefix(file, ".") || filepath.IsAbs(file) { 228 // Relative or absolute path. 229 return file, nil 230 } 231 pkg := pkgs[path.Dir(file)] 232 if pkg != nil { 233 if pkg.Dir != "" { 234 return filepath.Join(pkg.Dir, path.Base(file)), nil 235 } 236 if pkg.Error != nil { 237 return "", errors.New(pkg.Error.Err) 238 } 239 } 240 return "", fmt.Errorf("did not find package for %s in go list output", file) 241} 242 243func percent(covered, total int64) float64 { 244 if total == 0 { 245 total = 1 // Avoid zero denominator. 246 } 247 return 100.0 * float64(covered) / float64(total) 248} 249