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