Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 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 | |
| 7 | package main |
| 8 | |
| 9 | import ( |
| 10 | "bufio" |
| 11 | "fmt" |
| 12 | "go/ast" |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 13 | "go/build" |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 14 | "go/parser" |
| 15 | "go/token" |
| 16 | "os" |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 17 | "path/filepath" |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 18 | "text/tabwriter" |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 19 | |
Andrew Gerrand | 5ebbcd1 | 2014-11-10 08:50:40 +1100 | [diff] [blame] | 20 | "golang.org/x/tools/cover" |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 21 | ) |
| 22 | |
| 23 | // funcOutput takes two file names as arguments, a coverage profile to read as input and an output |
| 24 | // file to write ("" means to write to standard output). The function reads the profile and produces |
| 25 | // as output the coverage data broken down by function, like this: |
| 26 | // |
Fatih Arslan | ba91af2 | 2014-08-21 16:28:12 -0700 | [diff] [blame] | 27 | // fmt/format.go:30: init 100.0% |
| 28 | // fmt/format.go:57: clearflags 100.0% |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 29 | // ... |
Fatih Arslan | ba91af2 | 2014-08-21 16:28:12 -0700 | [diff] [blame] | 30 | // fmt/scan.go:1046: doScan 100.0% |
| 31 | // fmt/scan.go:1075: advance 96.2% |
| 32 | // fmt/scan.go:1119: doScanf 96.8% |
| 33 | // total: (statements) 91.9% |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 34 | |
| 35 | func funcOutput(profile, outputFile string) error { |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 36 | profiles, err := cover.ParseProfiles(profile) |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 37 | if err != nil { |
| 38 | return err |
| 39 | } |
| 40 | |
| 41 | var out *bufio.Writer |
| 42 | if outputFile == "" { |
| 43 | out = bufio.NewWriter(os.Stdout) |
| 44 | } else { |
| 45 | fd, err := os.Create(outputFile) |
| 46 | if err != nil { |
| 47 | return err |
| 48 | } |
| 49 | defer fd.Close() |
| 50 | out = bufio.NewWriter(fd) |
| 51 | } |
| 52 | defer out.Flush() |
| 53 | |
| 54 | tabber := tabwriter.NewWriter(out, 1, 8, 1, '\t', 0) |
| 55 | defer tabber.Flush() |
| 56 | |
| 57 | var total, covered int64 |
Rob Pike | 51613a1 | 2013-10-03 16:37:34 -0700 | [diff] [blame] | 58 | for _, profile := range profiles { |
| 59 | fn := profile.FileName |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 60 | file, err := findFile(fn) |
| 61 | if err != nil { |
| 62 | return err |
| 63 | } |
| 64 | funcs, err := findFuncs(file) |
| 65 | if err != nil { |
| 66 | return err |
| 67 | } |
| 68 | // Now match up functions and profile blocks. |
| 69 | for _, f := range funcs { |
| 70 | c, t := f.coverage(profile) |
Fatih Arslan | ba91af2 | 2014-08-21 16:28:12 -0700 | [diff] [blame] | 71 | fmt.Fprintf(tabber, "%s:%d:\t%s\t%.1f%%\n", fn, f.startLine, f.name, 100.0*float64(c)/float64(t)) |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 72 | total += t |
| 73 | covered += c |
| 74 | } |
| 75 | } |
| 76 | fmt.Fprintf(tabber, "total:\t(statements)\t%.1f%%\n", 100.0*float64(covered)/float64(total)) |
| 77 | |
| 78 | return nil |
| 79 | } |
| 80 | |
| 81 | // findFuncs parses the file and returns a slice of FuncExtent descriptors. |
| 82 | func findFuncs(name string) ([]*FuncExtent, error) { |
| 83 | fset := token.NewFileSet() |
| 84 | parsedFile, err := parser.ParseFile(fset, name, nil, 0) |
| 85 | if err != nil { |
| 86 | return nil, err |
| 87 | } |
| 88 | visitor := &FuncVisitor{ |
| 89 | fset: fset, |
| 90 | name: name, |
| 91 | astFile: parsedFile, |
| 92 | } |
| 93 | ast.Walk(visitor, visitor.astFile) |
| 94 | return visitor.funcs, nil |
| 95 | } |
| 96 | |
| 97 | // FuncExtent describes a function's extent in the source by file and position. |
| 98 | type FuncExtent struct { |
| 99 | name string |
| 100 | startLine int |
| 101 | startCol int |
| 102 | endLine int |
| 103 | endCol int |
| 104 | } |
| 105 | |
| 106 | // FuncVisitor implements the visitor that builds the function position list for a file. |
| 107 | type FuncVisitor struct { |
| 108 | fset *token.FileSet |
| 109 | name string // Name of file. |
| 110 | astFile *ast.File |
| 111 | funcs []*FuncExtent |
| 112 | } |
| 113 | |
| 114 | // Visit implements the ast.Visitor interface. |
| 115 | func (v *FuncVisitor) Visit(node ast.Node) ast.Visitor { |
| 116 | switch n := node.(type) { |
| 117 | case *ast.FuncDecl: |
| 118 | start := v.fset.Position(n.Pos()) |
| 119 | end := v.fset.Position(n.End()) |
| 120 | fe := &FuncExtent{ |
| 121 | name: n.Name.Name, |
| 122 | startLine: start.Line, |
| 123 | startCol: start.Column, |
| 124 | endLine: end.Line, |
| 125 | endCol: end.Column, |
| 126 | } |
| 127 | v.funcs = append(v.funcs, fe) |
| 128 | } |
| 129 | return v |
| 130 | } |
| 131 | |
| 132 | // coverage returns the fraction of the statements in the function that were covered, as a numerator and denominator. |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 133 | func (f *FuncExtent) coverage(profile *cover.Profile) (num, den int64) { |
Rob Pike | 4622486 | 2013-08-09 12:55:21 +1000 | [diff] [blame] | 134 | // We could avoid making this n^2 overall by doing a single scan and annotating the functions, |
| 135 | // but the sizes of the data structures is never very large and the scan is almost instantaneous. |
| 136 | var covered, total int64 |
| 137 | // The blocks are sorted, so we can stop counting as soon as we reach the end of the relevant block. |
| 138 | for _, b := range profile.Blocks { |
| 139 | if b.StartLine > f.endLine || (b.StartLine == f.endLine && b.StartCol >= f.endCol) { |
| 140 | // Past the end of the function. |
| 141 | break |
| 142 | } |
| 143 | if b.EndLine < f.startLine || (b.EndLine == f.startLine && b.EndCol <= f.startCol) { |
| 144 | // Before the beginning of the function |
| 145 | continue |
| 146 | } |
| 147 | total += int64(b.NumStmt) |
| 148 | if b.Count > 0 { |
| 149 | covered += int64(b.NumStmt) |
| 150 | } |
| 151 | } |
| 152 | if total == 0 { |
| 153 | total = 1 // Avoid zero denominator. |
| 154 | } |
| 155 | return covered, total |
| 156 | } |
Andrew Wilkins | df34f98 | 2013-12-03 20:55:21 -0800 | [diff] [blame] | 157 | |
| 158 | // findFile finds the location of the named file in GOROOT, GOPATH etc. |
| 159 | func findFile(file string) (string, error) { |
| 160 | dir, file := filepath.Split(file) |
| 161 | pkg, err := build.Import(dir, ".", build.FindOnly) |
| 162 | if err != nil { |
| 163 | return "", fmt.Errorf("can't find %q: %v", file, err) |
| 164 | } |
| 165 | return filepath.Join(pkg.Dir, file), nil |
| 166 | } |