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 }