github.com/jdhenke/godel@v0.0.0-20161213181855-abeb3861bf0d/apps/gunit/cmd/test/test.go (about) 1 // Copyright 2016 Palantir Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package test 16 17 import ( 18 "bufio" 19 "fmt" 20 "go/parser" 21 "go/token" 22 "io" 23 "io/ioutil" 24 "os" 25 "os/exec" 26 "path" 27 "regexp" 28 "strings" 29 30 "github.com/nmiyake/pkg/dirs" 31 "github.com/palantir/amalgomate/amalgomated" 32 "github.com/palantir/pkg/cli" 33 "github.com/palantir/pkg/cli/cfgcli" 34 "github.com/palantir/pkg/cli/flag" 35 "github.com/palantir/pkg/matcher" 36 "github.com/palantir/pkg/pkgpath" 37 "github.com/pkg/errors" 38 39 "github.com/palantir/godel/apps/gunit/cmd" 40 "github.com/palantir/godel/apps/gunit/config" 41 ) 42 43 const ( 44 goTestCmdName = "test" 45 goCoverCmdName = "cover" 46 coverageOutputPathFlag = "coverage-output" 47 pkgsParamName = "packages" 48 ) 49 50 var ( 51 goTestCmd = cmd.Library.MustNewCmd("gotest") 52 goCoverCmd = cmd.Library.MustNewCmd("gotest") 53 goJUnitReportCmd = cmd.Library.MustNewCmd("gojunitreport") 54 gtCmd = cmd.Library.MustNewCmd("gt") 55 ) 56 57 var goTestCmdGenerator = &testCmdGenerator{ 58 cmd: goTestCmd, 59 usage: "Runs 'go test' on project packages", 60 runFunc: runGoTest, 61 paramCreator: testParamCreator, 62 } 63 64 func GoTestCommand(supplier amalgomated.CmderSupplier) cli.Command { 65 cmd := goTestCmdGenerator.baseTestCmd(supplier) 66 cmd.Name = goTestCmdName 67 return cmd 68 } 69 70 func RunGoTestAction(supplier amalgomated.CmderSupplier) func(ctx cli.Context) error { 71 return func(ctx cli.Context) error { 72 testParams := testParamCreator(ctx) 73 wd, err := dirs.GetwdEvalSymLinks() 74 if err != nil { 75 return err 76 } 77 return goTestCmdGenerator.runTestCmdForPkgs(nil, testParams, supplier, wd, ctx.App.OnExit) 78 } 79 } 80 81 func GTCommand(supplier amalgomated.CmderSupplier) cli.Command { 82 gtCmd := &testCmdGenerator{ 83 cmd: gtCmd, 84 usage: "Runs 'gt' on project packages", 85 runFunc: runGoTest, 86 paramCreator: testParamCreator, 87 } 88 return gtCmd.baseTestCmd(supplier) 89 } 90 91 func GoCoverCommand(supplier amalgomated.CmderSupplier) cli.Command { 92 coverCmd := &testCmdGenerator{ 93 cmd: goCoverCmd, 94 usage: "Runs 'go cover' on project packages", 95 runFunc: runGoTestCover, 96 paramCreator: coverageParamCreator, 97 } 98 cmd := coverCmd.baseTestCmd(supplier) 99 cmd.Name = goCoverCmdName 100 cmd.Flags = append(cmd.Flags, 101 flag.StringFlag{ 102 Name: coverageOutputPathFlag, 103 Usage: "Path to coverage output file", 104 }, 105 ) 106 return cmd 107 } 108 109 type testCmdGenerator struct { 110 cmd amalgomated.Cmd 111 usage string 112 runFunc runTestFunc 113 paramCreator paramCreatorFunc 114 } 115 116 func (g *testCmdGenerator) baseTestCmd(supplier amalgomated.CmderSupplier) cli.Command { 117 return cli.Command{ 118 Name: g.cmd.Name(), 119 Usage: g.usage, 120 Flags: []flag.Flag{ 121 flag.StringSlice{ 122 Name: pkgsParamName, 123 Usage: "Packages to test", 124 Optional: true, 125 }, 126 }, 127 Action: func(ctx cli.Context) error { 128 pkgsParam := ctx.Slice(pkgsParamName) 129 testParams := g.paramCreator(ctx) 130 wd, err := dirs.GetwdEvalSymLinks() 131 if err != nil { 132 return err 133 } 134 return g.runTestCmdForPkgs(pkgsParam, testParams, supplier, wd, ctx.App.OnExit) 135 }, 136 } 137 } 138 139 func (g *testCmdGenerator) runTestCmdForPkgs(pkgsParam []string, testParams testCtxParams, supplier amalgomated.CmderSupplier, wd string, onExitManager cli.OnExit) error { 140 cfg, err := config.Load(cfgcli.ConfigPath, cfgcli.ConfigJSON) 141 if err != nil { 142 return err 143 } 144 145 // tagsMatcher is the matcher that represents the files that must be excluded to match the tag. 146 var tagsMatcher matcher.Matcher 147 if len(testParams.tags) == 0 { 148 // if no tags were provided, exclude all files matched by any tags (run only non-tagged tests) 149 tagsMatcher = cmd.AllTagsMatcher(cfg) 150 } else { 151 // otherwise, exclude files not matched by the specified tags 152 tagsMatcher, err = cmd.TagsMatcher(testParams.tags, cfg) 153 if err != nil { 154 return err 155 } 156 tagsMatcher = matcher.Not(tagsMatcher) 157 } 158 159 m := matcher.Any(tagsMatcher, cfg.Exclude) 160 pkgs, err := cmd.PkgPaths(pkgsParam, wd, m) 161 if err != nil { 162 return err 163 } 164 165 if len(pkgs) == 0 { 166 return errors.Errorf("no packages to test") 167 } 168 169 placeholderFiles, err := createPlaceholderTestFiles(pkgs, wd) 170 if err != nil { 171 return errors.Wrapf(err, "failed to create placeholder files for packages %v", pkgs) 172 } 173 174 cleanup := func() { 175 for _, currFileToRemove := range placeholderFiles { 176 if err := os.Remove(currFileToRemove); err != nil { 177 fmt.Printf("%+v\n", errors.Wrapf(err, "failed to remove file %s", currFileToRemove)) 178 } 179 } 180 } 181 182 // register cleanup task on exit 183 cleanupID := onExitManager.Register(cleanup) 184 185 // clean up placeholder files after function 186 defer func() { 187 // unregister cleanup task from CLI 188 onExitManager.Unregister(cleanupID) 189 190 // run cleanup 191 cleanup() 192 }() 193 194 return g.runTestCmd(supplier, pkgs, testParams, wd) 195 } 196 197 func (g *testCmdGenerator) runTestCmd(supplier amalgomated.CmderSupplier, pkgs []string, params testCtxParams, wd string) (rErr error) { 198 // if JUnit output is desired, set up temporary file to which raw output is written 199 var rawFile *os.File 200 rawWriter := ioutil.Discard 201 if params.junitOutputPath != "" { 202 var err error 203 rawFile, err = ioutil.TempFile("", "") 204 if err != nil { 205 return errors.Wrapf(err, "failed to create temporary file") 206 } 207 rawWriter = rawFile 208 defer func() { 209 if err := os.Remove(rawFile.Name()); err != nil && rErr == nil { 210 rErr = errors.Wrapf(err, "failed to remove temporary file %s in defer", rawFile.Name()) 211 } 212 }() 213 } 214 215 // run the test function 216 params.verbose = params.verbose || params.junitOutputPath != "" 217 cmder, err := supplier(g.cmd) 218 if err != nil { 219 return errors.Wrapf(err, "failed to create command %s", g.cmd.Name()) 220 } 221 failedPkgs, err := g.runFunc(cmder, pkgs, params, rawWriter, wd) 222 223 // close raw file 224 if rawFile != nil { 225 if err := rawFile.Close(); err != nil { 226 return errors.Wrapf(err, "failed to close file %s", rawFile.Name()) 227 } 228 } 229 230 if err != nil && err.Error() != "exit status 1" { 231 // only re-throw if error is not "exit status 1", since those errors are generally recoverable 232 return err 233 } 234 235 if params.junitOutputPath != "" { 236 // open raw file for reading 237 var err error 238 rawFile, err = os.Open(rawFile.Name()) 239 if err != nil { 240 return errors.Wrapf(err, "failed to open temporary file %s for reading", rawFile.Name()) 241 } 242 243 if err := runGoJUnitReport(supplier, wd, rawFile, params.junitOutputPath); err != nil { 244 return err 245 } 246 } 247 248 if len(failedPkgs) > 0 { 249 return fmt.Errorf(failedPkgsErrorMsg(failedPkgs)) 250 } 251 252 return nil 253 } 254 255 type paramCreatorFunc func(ctx cli.Context) testCtxParams 256 257 func testParamCreator(ctx cli.Context) testCtxParams { 258 return testCtxParams{ 259 stdout: ctx.App.Stdout, 260 junitOutputPath: cmd.JUnitOutputPath(ctx), 261 verbose: cmd.Verbose(ctx), 262 tags: cmd.Tags(ctx), 263 } 264 } 265 266 func coverageParamCreator(ctx cli.Context) testCtxParams { 267 param := testParamCreator(ctx) 268 param.coverageOutPath = ctx.String(coverageOutputPathFlag) 269 return param 270 } 271 272 type testCtxParams struct { 273 stdout io.Writer 274 coverageOutPath string 275 junitOutputPath string 276 verbose bool 277 tags []string 278 } 279 280 func longestPkgNameLen(pkgPaths []string, cmdWd string) int { 281 longestPkgLen := 0 282 for _, currPkgPath := range pkgPaths { 283 pkgName, err := pkgpath.NewRelPkgPath(currPkgPath, cmdWd).GoPathSrcRel() 284 if err == nil && len(pkgName) > longestPkgLen { 285 longestPkgLen = len(pkgName) 286 } 287 } 288 return longestPkgLen 289 } 290 291 type runTestFunc func(cmder amalgomated.Cmder, pkgs []string, params testCtxParams, w io.Writer, wd string) ([]string, error) 292 293 func runGoTest(cmder amalgomated.Cmder, pkgs []string, params testCtxParams, w io.Writer, wd string) ([]string, error) { 294 // make test output verbose 295 if params.verbose { 296 cmder = amalgomated.CmderWithPrependedArgs(cmder, "-v") 297 } 298 return executeTestCommand(cmder.Cmd(pkgs, wd), params.stdout, w, longestPkgNameLen(pkgs, wd)) 299 } 300 301 func runGoTestCover(cmder amalgomated.Cmder, pkgs []string, params testCtxParams, w io.Writer, wd string) (rFailedTests []string, rErr error) { 302 // create combined output file 303 outputFile, err := os.Create(params.coverageOutPath) 304 if err != nil { 305 return nil, errors.Wrapf(err, "failed to create specified output file for coverage at %s", params.coverageOutPath) 306 } 307 defer func() { 308 if err := outputFile.Close(); err != nil { 309 fmt.Printf("%+v\n", errors.Wrapf(err, "failed to close file %s in defer", outputFile)) 310 } 311 }() 312 313 // create temporary directory for individual coverage profiles 314 tmpDir, err := ioutil.TempDir("", "") 315 if err != nil { 316 return nil, errors.Wrapf(err, "failed to create temporary directory for coverage output") 317 } 318 defer func() { 319 if err := os.RemoveAll(tmpDir); err != nil && rErr == nil { 320 rErr = errors.Wrapf(err, "failed to remove temporary directory %s in defer", tmpDir) 321 } 322 }() 323 324 isFirstPackage := true 325 var failedTests []string 326 // currently can only run one package at a time 327 for _, currPkg := range pkgs { 328 // if error existed, add package to failed tests 329 longestPkgNameLen := longestPkgNameLen(pkgs, wd) 330 failedPkgs, currPkgCoverageFilePath, err := coverSinglePkg(cmder, params.stdout, w, wd, params.verbose, currPkg, tmpDir, longestPkgNameLen) 331 if err != nil { 332 failedTests = append(failedTests, failedPkgs...) 333 } 334 335 if err := appendSingleCoverageOutputToCombined(currPkgCoverageFilePath, isFirstPackage, outputFile); err != nil { 336 return nil, err 337 } 338 isFirstPackage = false 339 } 340 341 return failedTests, err 342 } 343 344 func appendSingleCoverageOutputToCombined(singleCoverageFilePath string, isFirstPkg bool, combinedOutput io.Writer) (rErr error) { 345 singlePkgCoverageFile, err := os.Open(singleCoverageFilePath) 346 if err != nil { 347 return errors.Wrapf(err, "failed to open file %s", singleCoverageFilePath) 348 } 349 350 defer func() { 351 if err := singlePkgCoverageFile.Close(); err != nil && rErr == nil { 352 rErr = errors.Wrapf(err, "failed to close file %s in defer", singleCoverageFilePath) 353 } 354 }() 355 356 // append current output to combined output file 357 br := bufio.NewReader(singlePkgCoverageFile) 358 if !isFirstPkg { 359 // if this is not the first package, skip the first line (it contains the coverage mode) 360 if _, err := br.ReadString('\n'); err != nil { 361 // do nothing 362 } 363 } 364 if _, err := io.Copy(combinedOutput, br); err != nil { 365 return errors.Wrapf(err, "failed to write output to writer") 366 } 367 368 return nil 369 } 370 371 // coverSinglePkgs runs the cover command on a single package. The raw output of the command written to the provided 372 // writer. The coverage profile for the file is written to a temporary file within the provided directory. The function 373 // returns the names of any packages that failed (should be either empty or a slice containing the package name of the 374 // package that was covered), the location that the coverage profile for this package was written and an error value. 375 func coverSinglePkg(cmder amalgomated.Cmder, stdout io.Writer, rawWriter io.Writer, cmdWd string, verbose bool, currPkg, tmpDir string, longestPkgNameLen int) (rFailedPkgs []string, rTmpFile string, rErr error) { 376 currTmpFile, err := ioutil.TempFile(tmpDir, "") 377 if err != nil { 378 return nil, "", errors.Wrapf(err, "failed to create temporary file for coverage for package %s", currPkg) 379 } 380 defer func() { 381 if err := currTmpFile.Close(); err != nil && rErr == nil { 382 rErr = errors.Wrapf(err, "failed to close file %s in defer", currTmpFile.Name()) 383 } 384 }() 385 386 // make test output verbose and enable coverage 387 var prependedArgs []string 388 if verbose { 389 prependedArgs = append(prependedArgs, "-v") 390 } 391 prependedArgs = append(prependedArgs, "-covermode=count", "-coverprofile="+currTmpFile.Name()) 392 wrappedCmder := amalgomated.CmderWithPrependedArgs(cmder, prependedArgs...) 393 394 // execute test for package 395 failedPkgs, err := executeTestCommand(wrappedCmder.Cmd([]string{currPkg}, cmdWd), stdout, rawWriter, longestPkgNameLen) 396 return failedPkgs, currTmpFile.Name(), err 397 } 398 399 func runGoJUnitReport(supplier amalgomated.CmderSupplier, cmdWd string, rawOutputReader io.Reader, junitOutputPath string) error { 400 cmder, err := supplier(goJUnitReportCmd) 401 if err != nil { 402 return errors.Wrapf(err, "failed to create runner for gojunitreport") 403 } 404 405 execCmd := cmder.Cmd(nil, cmdWd) 406 execCmd.Stdin = bufio.NewReader(rawOutputReader) 407 output, err := execCmd.CombinedOutput() 408 if err != nil { 409 return errors.Wrapf(err, "failed to run gojunitreport") 410 } 411 412 if err := ioutil.WriteFile(junitOutputPath, output, 0644); err != nil { 413 return errors.Wrapf(err, "failed to write output to path %s", junitOutputPath) 414 } 415 return nil 416 } 417 418 // createPlaceholderTestFiles creates placeholder test files in any of the provided packages that do not already contain 419 // test files and returns a slice that contains the created files. If this function returns an error, it will attempt to 420 // remove any of the placeholder files that it created before doing so. The generated files will have the name 421 // "tmp_placeholder_test.go" and will have a package clause that matches the name of the other go files in the 422 // directory. 423 func createPlaceholderTestFiles(pkgs []string, wd string) ([]string, error) { 424 var placeholderFiles []string 425 426 for _, currPkg := range pkgs { 427 currPath := path.Join(wd, currPkg) 428 infos, err := ioutil.ReadDir(currPath) 429 if err != nil { 430 return nil, errors.Wrapf(err, "failed to get directory information for package %s", currPath) 431 } 432 433 pkgHasTest := false 434 for _, currFileInfo := range infos { 435 if !currFileInfo.IsDir() && strings.HasSuffix(currFileInfo.Name(), "_test.go") { 436 pkgHasTest = true 437 break 438 } 439 } 440 441 // no test present -- get package name and write temporary placeholder file 442 if !pkgHasTest { 443 parsedPkgs, err := parser.ParseDir(token.NewFileSet(), currPath, nil, parser.PackageClauseOnly) 444 if err != nil { 445 return nil, errors.Wrapf(err, "failed to parse packages from directory %s", currPath) 446 } 447 448 if len(parsedPkgs) > 0 { 449 // get package name (should only be one since there are no tests in this directory and 450 // go requires one package per directory, but will work even if that is not the case) 451 pkgName := "" 452 for currName := range parsedPkgs { 453 pkgName = currName 454 break 455 } 456 457 currPlaceholderFile := path.Join(currPath, "tmp_placeholder_test.go") 458 459 if err := ioutil.WriteFile(currPlaceholderFile, placeholderTestFileBytes(pkgName), 0644); err != nil { 460 // if write fails, clean up files that were already written before returning 461 for _, currFileToClean := range placeholderFiles { 462 if err := os.Remove(currFileToClean); err != nil { 463 fmt.Printf("failed to remove file %s: %v\n", currFileToClean, err) 464 } 465 } 466 return nil, errors.Wrapf(err, "failed to write placeholder file %s", currPlaceholderFile) 467 } 468 469 placeholderFiles = append(placeholderFiles, currPlaceholderFile) 470 } 471 } 472 } 473 474 return placeholderFiles, nil 475 } 476 477 const placeholderTemplate = `package {{package}} 478 // temporary placeholder test file created by gunit 479 ` 480 481 func placeholderTestFileBytes(pkgName string) []byte { 482 return []byte(strings.Replace(placeholderTemplate, "{{package}}", pkgName, -1)) 483 } 484 485 // executeTestCommand executes the provided command. The output produced by the command's Stdout and Stderr calls are 486 // processed as they are written and an aligned version of the output is written to the Stdout of the current process. 487 // The "longestPkgNameLen" parameter specifies the longest package name (used to align the console output). This 488 // function returns a slice that contains the packages that had test failures (output line started with "FAIL"). The 489 // error value will contain any error that was encountered while executing the command, including if the command 490 // executed successfully but any tests failed. In either case, the packages that encountered errors will also be 491 // returned. 492 func executeTestCommand(execCmd *exec.Cmd, stdout io.Writer, rawOutputWriter io.Writer, longestPkgNameLen int) (rFailedPkgs []string, rErr error) { 493 bw := bufio.NewWriter(rawOutputWriter) 494 495 // stream output to Stdout 496 multiWriter := multiWriter{ 497 consoleWriter: stdout, 498 rawOutputWriter: bw, 499 failedPkgs: []string{}, 500 longestPkgNameLen: longestPkgNameLen, 501 } 502 503 // flush buffered writer at the end of the function 504 defer func() { 505 if err := bw.Flush(); err != nil && rErr == nil { 506 rErr = errors.Wrapf(err, "failed to flush buffered writer in defer") 507 } 508 }() 509 510 // set Stdout and Stderr of command to multiwriter 511 execCmd.Stdout = &multiWriter 512 execCmd.Stderr = &multiWriter 513 514 // run command (which will print its Stdout and Stderr to the Stdout of current process) and return output 515 err := execCmd.Run() 516 return multiWriter.failedPkgs, err 517 } 518 519 type multiWriter struct { 520 consoleWriter io.Writer 521 rawOutputWriter io.Writer 522 failedPkgs []string 523 longestPkgNameLen int 524 } 525 526 var setupFailedRegexp = regexp.MustCompile(`(^FAIL\t.+) (\[setup failed\]$)`) 527 528 func (w *multiWriter) Write(p []byte) (int, error) { 529 // write unaltered output to file 530 n, err := w.rawOutputWriter.Write(p) 531 if err != nil { 532 return n, err 533 } 534 535 lines := strings.Split(string(p), "\n") 536 for i, currLine := range lines { 537 // test output for valid case always starts with "Ok" or "FAIL" 538 if strings.HasPrefix(currLine, "ok") || strings.HasPrefix(currLine, "FAIL") { 539 if setupFailedRegexp.MatchString(currLine) { 540 // if line matches "setup failed" output, modify output to conform to expected style 541 // (namely, replace space between package name and "[setup failed]" with a tab) 542 currLine = setupFailedRegexp.ReplaceAllString(currLine, "$1\t$2") 543 } 544 545 // split into at most 4 parts 546 fields := strings.SplitN(currLine, "\t", 4) 547 548 // valid test lines have at least 3 parts: "[ok|FAIL]\t[pkgName]\t[time|no test files]" 549 if len(fields) >= 3 { 550 currPkgName := strings.TrimSpace(fields[1]) 551 lines[i] = alignLine(fields, w.longestPkgNameLen) 552 // append package name to failures list if this was a failure 553 if strings.HasPrefix(currLine, "FAIL") { 554 w.failedPkgs = append(w.failedPkgs, currPkgName) 555 } 556 } 557 } 558 } 559 560 // write formatted version to console writer 561 if n, err := w.consoleWriter.Write([]byte(strings.Join(lines, "\n"))); err != nil { 562 return n, err 563 } 564 565 // n and err are from the unaltered write to the rawOutputWriter 566 return n, err 567 } 568 569 // alignLine returns a string where the length of the second field (fields[1]) is padded with spaces to make its length 570 // equal to the value of maxPkgLen and the fields are joined with tab characters. Assuming that the first field is 571 // always the same length, this method ensures that the third field will always be aligned together for any fixed value 572 // of maxPkgLen. 573 func alignLine(fields []string, maxPkgLen int) string { 574 currPkgName := fields[1] 575 repeat := maxPkgLen - len(currPkgName) 576 if repeat < 0 { 577 // this should not occur under normal circumstances. However, it appears that it is possible if tests 578 // create test packages in the directory structure while tests are already running. If such a case is 579 // encountered, having output that isn't aligned optimally is better than crashing, so set repeat to 0. 580 repeat = 0 581 } 582 fields[1] = currPkgName + strings.Repeat(" ", repeat) 583 return strings.Join(fields, "\t") 584 } 585 586 func failedPkgsErrorMsg(failedPkgs []string) string { 587 numFailedPkgs := len(failedPkgs) 588 outputParts := append([]string{fmt.Sprintf("%d %v had failing tests:", numFailedPkgs, plural(numFailedPkgs, "package", "packages"))}, failedPkgs...) 589 return strings.Join(outputParts, "\n\t") 590 } 591 592 func plural(num int, singular, plural string) string { 593 if num == 1 { 594 return singular 595 } 596 return plural 597 }