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  }