github.com/LawrenceWoodman/roveralls@v0.0.0-20171119193843-51b78509b607/roveralls.go (about)

     1  // Copyright (c) 2016 Lawrence Woodman <lwoodman@vlifesystems.com>
     2  // Licensed under an MIT licence.  Please see LICENCE.md for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  )
    18  
    19  // This is a horrible kludge so that errors can be tested properly
    20  var program *Program
    21  
    22  // Usage is used by flag package if an error occurs when parsing flags
    23  var Usage = func() {
    24  	subUsage(program.outErr)
    25  }
    26  
    27  func subUsage(out io.Writer) {
    28  	fmt.Fprintf(out, usageMsg())
    29  }
    30  
    31  func usageMsg() string {
    32  	var b bytes.Buffer
    33  	const desc = `
    34  roveralls runs coverage tests on a package and all its sub-packages.  The
    35  coverage profile is output as a single file called 'roveralls.coverprofile'
    36  for use by tools such as goveralls.
    37  `
    38  	fmt.Fprintf(&b, "%s\n", desc)
    39  	fmt.Fprintf(&b, "Usage:\n")
    40  	program.flagSet.SetOutput(&b)
    41  	program.flagSet.PrintDefaults()
    42  	program.flagSet.SetOutput(program.outErr)
    43  	return b.String()
    44  }
    45  
    46  func usagePartialMsg() string {
    47  	var b bytes.Buffer
    48  	fmt.Fprintf(&b, "Usage:\n")
    49  	program.flagSet.SetOutput(&b)
    50  	program.flagSet.PrintDefaults()
    51  	program.flagSet.SetOutput(program.outErr)
    52  	return b.String()
    53  }
    54  
    55  const (
    56  	defaultIgnores = ".git,vendor"
    57  	outFilename    = "roveralls.coverprofile"
    58  )
    59  
    60  type goTestError struct {
    61  	stderr string
    62  	stdout string
    63  }
    64  
    65  func (e goTestError) Error() string {
    66  	return fmt.Sprintf("error from go test: %s\noutput: %s",
    67  		e.stderr, e.stdout)
    68  }
    69  
    70  type walkingError struct {
    71  	dir string
    72  	err error
    73  }
    74  
    75  func (e walkingError) Error() string {
    76  	return fmt.Sprintf("could not walk working directory '%s': %s",
    77  		e.dir, e.err)
    78  }
    79  
    80  // Program contains the configuration and state of the program
    81  type Program struct {
    82  	ignore  string
    83  	cover   string
    84  	help    bool
    85  	short   bool
    86  	verbose bool
    87  	ignores map[string]bool
    88  	cmdArgs []string
    89  	flagSet *flag.FlagSet
    90  	out     io.Writer
    91  	outErr  io.Writer
    92  	gopath  string
    93  }
    94  
    95  func initProgram(
    96  	cmdArgs []string,
    97  	out io.Writer,
    98  	outErr io.Writer,
    99  	gopath string,
   100  ) {
   101  	program = &Program{out: out, outErr: outErr, cmdArgs: cmdArgs, gopath: gopath}
   102  	program.initFlagSet()
   103  }
   104  
   105  // Run starts the program
   106  func (p *Program) Run() int {
   107  	if err := p.flagSet.Parse(p.cmdArgs[1:]); err != nil {
   108  		return 1
   109  	}
   110  	if isProblem := p.handleGOPATH(); isProblem {
   111  		return 1
   112  	}
   113  
   114  	if isProblem := p.handleFlags(); isProblem {
   115  		return 1
   116  	}
   117  	if p.help {
   118  		subUsage(p.out)
   119  		return 0
   120  	}
   121  
   122  	if err := p.testCoverage(); err != nil {
   123  		fmt.Fprintf(p.outErr, "\n%s\n", err)
   124  		return 1
   125  	}
   126  	return 0
   127  }
   128  
   129  func (p *Program) ignoreDir(relDir string) bool {
   130  	_, ignore := p.ignores[relDir]
   131  	return ignore
   132  }
   133  
   134  func (p *Program) initFlagSet() {
   135  	p.flagSet = flag.NewFlagSet("", flag.ContinueOnError)
   136  	p.flagSet.SetOutput(p.outErr)
   137  	p.flagSet.StringVar(
   138  		&p.cover,
   139  		"covermode",
   140  		"count",
   141  		"Mode to run when testing files: `count,set,atomic`",
   142  	)
   143  	p.flagSet.StringVar(
   144  		&p.ignore,
   145  		"ignore",
   146  		defaultIgnores,
   147  		"Comma separated list of directory names to ignore: `dir1,dir2,...`",
   148  	)
   149  	p.flagSet.BoolVar(&p.verbose, "v", false, "Verbose output")
   150  	p.flagSet.BoolVar(
   151  		&p.short,
   152  		"short",
   153  		false,
   154  		"Tell long-running tests to shorten their run time",
   155  	)
   156  	p.flagSet.BoolVar(&p.help, "help", false, "Display this help")
   157  }
   158  
   159  // returns true if a problem, else false
   160  func (p *Program) handleGOPATH() bool {
   161  	gopath := filepath.Clean(p.gopath)
   162  	if p.verbose {
   163  		fmt.Fprintln(p.out, "GOPATH:", gopath)
   164  	}
   165  
   166  	if len(gopath) == 0 || gopath == "." {
   167  		fmt.Fprintf(p.outErr, "invalid GOPATH '%s'\n", gopath)
   168  		return true
   169  	}
   170  	return false
   171  }
   172  
   173  // returns true if a problem, else false
   174  func (p *Program) handleFlags() bool {
   175  	validCoverModes := map[string]bool{"set": true, "count": true, "atomic": true}
   176  	if _, ok := validCoverModes[p.cover]; !ok {
   177  		fmt.Fprintf(p.outErr, "invalid covermode '%s'\n", p.cover)
   178  		subUsage(p.outErr)
   179  		return true
   180  	}
   181  
   182  	arr := strings.Split(p.ignore, ",")
   183  	p.ignores = make(map[string]bool, len(arr))
   184  	for _, v := range arr {
   185  		p.ignores[v] = true
   186  	}
   187  	return false
   188  }
   189  
   190  var modeRegexp = regexp.MustCompile("mode: [a-z]+\n")
   191  
   192  func (p *Program) testCoverage() error {
   193  	var buff bytes.Buffer
   194  
   195  	wd, err := os.Getwd()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	if p.verbose {
   200  		fmt.Fprintln(p.out, "Working dir:", wd)
   201  	}
   202  
   203  	walker := p.makeWalker(wd, &buff)
   204  	if err := filepath.Walk(wd, walker); err != nil {
   205  		return walkingError{
   206  			dir: wd,
   207  			err: err,
   208  		}
   209  	}
   210  
   211  	final := buff.String()
   212  	final = modeRegexp.ReplaceAllString(final, "")
   213  	final = fmt.Sprintf("mode: %s\n%s", p.cover, final)
   214  
   215  	if err := ioutil.WriteFile(outFilename, []byte(final), 0644); err != nil {
   216  		return fmt.Errorf("error writing to: %s, %s", outFilename, err)
   217  	}
   218  	return nil
   219  }
   220  
   221  func (p *Program) makeWalker(
   222  	wd string,
   223  	buff *bytes.Buffer,
   224  ) func(string, os.FileInfo, error) error {
   225  	return func(path string, info os.FileInfo, err error) error {
   226  		if !info.IsDir() {
   227  			return nil
   228  		}
   229  
   230  		rel, err := filepath.Rel(wd, path)
   231  		if err != nil {
   232  			return fmt.Errorf("error creating relative path")
   233  		}
   234  
   235  		if p.ignoreDir(rel) {
   236  			return filepath.SkipDir
   237  		}
   238  
   239  		files, err := filepath.Glob(filepath.Join(path, "*_test.go"))
   240  		if err != nil {
   241  			return fmt.Errorf("error checking for test files")
   242  		}
   243  		if len(files) == 0 {
   244  			if p.verbose {
   245  				fmt.Fprintf(p.out, "No Go test files in dir: %s, skipping\n", rel)
   246  			}
   247  			return nil
   248  		}
   249  		return p.processDir(wd, path, buff)
   250  	}
   251  }
   252  
   253  func (p *Program) processDir(wd string, path string, buff *bytes.Buffer) error {
   254  	var cmd *exec.Cmd
   255  	var cmdOut bytes.Buffer
   256  	var cmdErr bytes.Buffer
   257  
   258  	if err := os.Chdir(path); err != nil {
   259  		return err
   260  	}
   261  	defer os.Chdir(wd)
   262  
   263  	outDir, err := ioutil.TempDir("", "roveralls")
   264  	if err != nil {
   265  		return err
   266  	}
   267  	defer os.RemoveAll(outDir)
   268  
   269  	if p.verbose {
   270  		rel, err := filepath.Rel(wd, path)
   271  		if err != nil {
   272  			return fmt.Errorf("can't create relative path")
   273  		}
   274  		fmt.Fprintf(p.out, "Processing dir: %s\n", rel)
   275  		if p.short {
   276  			fmt.Fprintf(p.out,
   277  				"Processing: go test -short -covermode=%s -coverprofile=profile.coverprofile -outputdir=%s\n",
   278  				p.cover, outDir)
   279  		} else {
   280  			fmt.Fprintf(p.out,
   281  				"Processing: go test -covermode=%s -coverprofile=profile.coverprofile -outputdir=%s\n",
   282  				p.cover, outDir)
   283  		}
   284  	}
   285  
   286  	if p.short {
   287  		cmd = exec.Command("go",
   288  			"test",
   289  			"-short",
   290  			"-covermode="+p.cover,
   291  			"-coverprofile=profile.coverprofile",
   292  			"-outputdir="+outDir,
   293  		)
   294  	} else {
   295  		cmd = exec.Command("go",
   296  			"test",
   297  			"-covermode="+p.cover,
   298  			"-coverprofile=profile.coverprofile",
   299  			"-outputdir="+outDir,
   300  		)
   301  	}
   302  	cmd.Stdout = &cmdOut
   303  	cmd.Stderr = &cmdErr
   304  	if err := cmd.Run(); err != nil {
   305  		return goTestError{
   306  			stderr: cmdErr.String(),
   307  			stdout: cmdOut.String(),
   308  		}
   309  	}
   310  
   311  	b, err := ioutil.ReadFile(filepath.Join(outDir, "profile.coverprofile"))
   312  	if err != nil {
   313  		return err
   314  	}
   315  
   316  	_, err = buff.Write(b)
   317  	return err
   318  }
   319  
   320  func main() {
   321  	initProgram(os.Args, os.Stdout, os.Stderr, os.Getenv("GOPATH"))
   322  	os.Exit(program.Run())
   323  }