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