...

Source file src/cmd/cover/func.go

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

View as plain text