gotest.tools/gotestsum@v1.11.0/testjson/format.go (about)

     1  package testjson
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/bitfield/gotestdox"
    12  	"github.com/fatih/color"
    13  )
    14  
    15  func debugFormat(out io.Writer) eventFormatterFunc {
    16  	return func(event TestEvent, _ *Execution) error {
    17  		_, err := fmt.Fprintf(out, "%s %s %s (%.3f) [%d] %s\n",
    18  			event.Package,
    19  			event.Test,
    20  			event.Action,
    21  			event.Elapsed,
    22  			event.Time.Unix(),
    23  			event.Output)
    24  		return err
    25  	}
    26  }
    27  
    28  // go test -v
    29  func standardVerboseFormat(out io.Writer) EventFormatter {
    30  	buf := bufio.NewWriter(out)
    31  	return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
    32  		if event.Action == ActionOutput {
    33  			_, _ = buf.WriteString(event.Output)
    34  			return buf.Flush()
    35  		}
    36  		return nil
    37  	})
    38  }
    39  
    40  // go test
    41  func standardQuietFormat(out io.Writer) EventFormatter {
    42  	buf := bufio.NewWriter(out)
    43  	return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
    44  		if !event.PackageEvent() {
    45  			return nil
    46  		}
    47  		if event.Output == "PASS\n" {
    48  			return nil
    49  		}
    50  
    51  		// Coverage line go1.20+
    52  		if strings.Contains(event.Output, event.Package+"\tcoverage:") {
    53  			return nil
    54  		}
    55  		if isCoverageOutputPreGo119(event.Output) {
    56  			return nil
    57  		}
    58  
    59  		if isWarningNoTestsToRunOutput(event.Output) {
    60  			return nil
    61  		}
    62  
    63  		_, _ = buf.WriteString(event.Output)
    64  		return buf.Flush()
    65  	})
    66  }
    67  
    68  // go test -json
    69  func standardJSONFormat(out io.Writer) EventFormatter {
    70  	buf := bufio.NewWriter(out)
    71  	// nolint:errcheck // errors are returned by Flush
    72  	return eventFormatterFunc(func(event TestEvent, _ *Execution) error {
    73  		buf.Write(event.raw)
    74  		buf.WriteRune('\n')
    75  		return buf.Flush()
    76  	})
    77  }
    78  
    79  func testNameFormatTestEvent(out io.Writer, event TestEvent) {
    80  	pkgPath := RelativePackagePath(event.Package)
    81  
    82  	fmt.Fprintf(out, "%s %s%s (%.2fs)\n",
    83  		colorEvent(event)(strings.ToUpper(string(event.Action))),
    84  		joinPkgToTestName(pkgPath, event.Test),
    85  		formatRunID(event.RunID),
    86  		event.Elapsed)
    87  }
    88  
    89  func testDoxFormat(out io.Writer, opts FormatOptions) EventFormatter {
    90  	buf := bufio.NewWriter(out)
    91  	type Result struct {
    92  		Event    TestEvent
    93  		Sentence string
    94  	}
    95  	getIcon := icon
    96  	if opts.UseHiVisibilityIcons {
    97  		getIcon = iconHiVis
    98  	}
    99  	results := map[string][]Result{}
   100  	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
   101  		switch {
   102  		case event.PackageEvent():
   103  			if !event.Action.IsTerminal() {
   104  				return nil
   105  			}
   106  			if opts.HideEmptyPackages && len(results[event.Package]) == 0 {
   107  				return nil
   108  			}
   109  			fmt.Fprintf(buf, "%s:\n", event.Package)
   110  			tests := results[event.Package]
   111  			sort.Slice(tests, func(i, j int) bool {
   112  				return tests[i].Sentence < tests[j].Sentence
   113  			})
   114  			for _, r := range tests {
   115  				fmt.Fprintf(buf, " %s %s (%.2fs)\n",
   116  					getIcon(r.Event.Action),
   117  					r.Sentence,
   118  					r.Event.Elapsed)
   119  			}
   120  			fmt.Fprintln(buf)
   121  			return buf.Flush()
   122  		case event.Action.IsTerminal():
   123  			// Fuzz test cases tend not to have interesting names,
   124  			// so only report these if they're failures
   125  			if isFuzzCase(event) {
   126  				return nil
   127  			}
   128  			results[event.Package] = append(results[event.Package], Result{
   129  				Event:    event,
   130  				Sentence: gotestdox.Prettify(event.Test),
   131  			})
   132  		}
   133  		return nil
   134  	})
   135  }
   136  
   137  func isFuzzCase(event TestEvent) bool {
   138  	return strings.HasPrefix(event.Test, "Fuzz") &&
   139  		event.Action == ActionPass &&
   140  		TestName(event.Test).IsSubTest()
   141  }
   142  
   143  func testNameFormat(out io.Writer) EventFormatter {
   144  	buf := bufio.NewWriter(out)
   145  	// nolint:errcheck
   146  	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
   147  		formatTest := func() error {
   148  			testNameFormatTestEvent(buf, event)
   149  			return buf.Flush()
   150  		}
   151  
   152  		switch {
   153  		case isPkgFailureOutput(event):
   154  			buf.WriteString(event.Output)
   155  			return buf.Flush()
   156  
   157  		case event.PackageEvent():
   158  			if !event.Action.IsTerminal() {
   159  				return nil
   160  			}
   161  
   162  			result := colorEvent(event)(strings.ToUpper(string(event.Action)))
   163  			pkg := exec.Package(event.Package)
   164  			if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
   165  				event.Action = ActionSkip // always color these as skip actions
   166  				result = colorEvent(event)("EMPTY")
   167  			}
   168  
   169  			event.Elapsed = 0 // hide elapsed for now, for backwards compat
   170  			buf.WriteString(result)
   171  			buf.WriteRune(' ')
   172  			buf.WriteString(packageLine(event, exec.Package(event.Package)))
   173  			return buf.Flush()
   174  
   175  		case event.Action == ActionFail:
   176  			pkg := exec.Package(event.Package)
   177  			tc := pkg.LastFailedByName(event.Test)
   178  			pkg.WriteOutputTo(buf, tc.ID)
   179  			return formatTest()
   180  
   181  		case event.Action == ActionPass || event.Action == ActionSkip:
   182  			return formatTest()
   183  		}
   184  		return nil
   185  	})
   186  }
   187  
   188  // joinPkgToTestName for formatting.
   189  // If the package path isn't the current directory, we add a period to separate
   190  // the test name and the package path. If it is the current directory, we don't
   191  // show it at all. This prevents output like ..MyTest when the test is in the
   192  // current directory.
   193  func joinPkgToTestName(pkg string, test string) string {
   194  	if pkg == "." {
   195  		return test
   196  	}
   197  	return pkg + "." + test
   198  }
   199  
   200  // formatRunID returns a formatted string of the runID.
   201  func formatRunID(runID int) string {
   202  	if runID <= 0 {
   203  		return ""
   204  	}
   205  	return fmt.Sprintf(" (re-run %d)", runID)
   206  }
   207  
   208  // isPkgFailureOutput returns true if the event is package output, and the output
   209  // doesn't match any of the expected framing messages. Events which match this
   210  // pattern should be package-level failures (ex: exit(1) or panic in an init() or
   211  // TestMain).
   212  func isPkgFailureOutput(event TestEvent) bool {
   213  	out := event.Output
   214  	return all(
   215  		event.PackageEvent(),
   216  		event.Action == ActionOutput,
   217  		out != "PASS\n",
   218  		out != "FAIL\n",
   219  		!isWarningNoTestsToRunOutput(out),
   220  		!strings.HasPrefix(out, "FAIL\t"+event.Package),
   221  		!strings.HasPrefix(out, "ok  \t"+event.Package),
   222  		!strings.HasPrefix(out, "?   \t"+event.Package),
   223  		!isShuffleSeedOutput(out),
   224  	)
   225  }
   226  
   227  func all(cond ...bool) bool {
   228  	for _, c := range cond {
   229  		if !c {
   230  			return false
   231  		}
   232  	}
   233  	return true
   234  }
   235  
   236  func pkgNameFormat(out io.Writer, opts FormatOptions) eventFormatterFunc {
   237  	buf := bufio.NewWriter(out)
   238  	return func(event TestEvent, exec *Execution) error {
   239  		if !event.PackageEvent() {
   240  			return nil
   241  		}
   242  		_, _ = buf.WriteString(shortFormatPackageEvent(opts, event, exec))
   243  		return buf.Flush()
   244  	}
   245  }
   246  
   247  func icon(action Action) string {
   248  	switch action {
   249  	case ActionPass:
   250  		return color.GreenString("✓")
   251  	case ActionSkip:
   252  		return color.YellowString("∅")
   253  	case ActionFail:
   254  		return color.RedString("✖")
   255  	default:
   256  		return ""
   257  	}
   258  }
   259  
   260  func iconHiVis(action Action) string {
   261  	switch action {
   262  	case ActionPass:
   263  		return "✅"
   264  	case ActionSkip:
   265  		return "➖"
   266  	case ActionFail:
   267  		return "❌"
   268  	default:
   269  		return ""
   270  	}
   271  }
   272  
   273  func shortFormatPackageEvent(opts FormatOptions, event TestEvent, exec *Execution) string {
   274  	pkg := exec.Package(event.Package)
   275  
   276  	getIcon := icon
   277  	if opts.UseHiVisibilityIcons {
   278  		getIcon = iconHiVis
   279  	}
   280  
   281  	fmtEvent := func(action string) string {
   282  		return action + "  " + packageLine(event, exec.Package(event.Package))
   283  	}
   284  	switch event.Action {
   285  	case ActionSkip:
   286  		if opts.HideEmptyPackages {
   287  			return ""
   288  		}
   289  		return fmtEvent(getIcon(event.Action))
   290  	case ActionPass:
   291  		if pkg.Total == 0 {
   292  			if opts.HideEmptyPackages {
   293  				return ""
   294  			}
   295  			return fmtEvent(getIcon(ActionSkip))
   296  		}
   297  		return fmtEvent(getIcon(event.Action))
   298  	case ActionFail:
   299  		return fmtEvent(getIcon(event.Action))
   300  	}
   301  	return ""
   302  }
   303  
   304  func packageLine(event TestEvent, pkg *Package) string {
   305  	var buf strings.Builder
   306  	buf.WriteString(RelativePackagePath(event.Package))
   307  
   308  	switch {
   309  	case pkg.cached:
   310  		buf.WriteString(" (cached)")
   311  	case event.Elapsed != 0:
   312  		d := elapsedDuration(event.Elapsed)
   313  		buf.WriteString(fmt.Sprintf(" (%s)", d))
   314  	}
   315  
   316  	if pkg.coverage != "" {
   317  		buf.WriteString(" (" + pkg.coverage + ")")
   318  	}
   319  
   320  	if event.Action == ActionFail && pkg.shuffleSeed != "" {
   321  		buf.WriteString(" (" + pkg.shuffleSeed + ")")
   322  	}
   323  	buf.WriteString("\n")
   324  	return buf.String()
   325  }
   326  
   327  func pkgNameWithFailuresFormat(out io.Writer, opts FormatOptions) eventFormatterFunc {
   328  	buf := bufio.NewWriter(out)
   329  	return func(event TestEvent, exec *Execution) error {
   330  		if !event.PackageEvent() {
   331  			if event.Action == ActionFail {
   332  				pkg := exec.Package(event.Package)
   333  				tc := pkg.LastFailedByName(event.Test)
   334  				pkg.WriteOutputTo(buf, tc.ID) // nolint:errcheck
   335  				return buf.Flush()
   336  			}
   337  			return nil
   338  		}
   339  		buf.WriteString(shortFormatPackageEvent(opts, event, exec)) // nolint:errcheck
   340  		return buf.Flush()
   341  	}
   342  }
   343  
   344  func colorEvent(event TestEvent) func(format string, a ...interface{}) string {
   345  	switch event.Action {
   346  	case ActionPass:
   347  		return color.GreenString
   348  	case ActionFail:
   349  		return color.RedString
   350  	case ActionSkip:
   351  		return color.YellowString
   352  	}
   353  	return color.WhiteString
   354  }
   355  
   356  // EventFormatter is a function which handles an event and returns a string to
   357  // output for the event.
   358  type EventFormatter interface {
   359  	Format(event TestEvent, output *Execution) error
   360  }
   361  
   362  type eventFormatterFunc func(event TestEvent, output *Execution) error
   363  
   364  func (e eventFormatterFunc) Format(event TestEvent, output *Execution) error {
   365  	return e(event, output)
   366  }
   367  
   368  type FormatOptions struct {
   369  	HideEmptyPackages    bool
   370  	UseHiVisibilityIcons bool
   371  }
   372  
   373  // NewEventFormatter returns a formatter for printing events.
   374  func NewEventFormatter(out io.Writer, format string, formatOpts FormatOptions) EventFormatter {
   375  	switch format {
   376  	case "none":
   377  		return eventFormatterFunc(func(TestEvent, *Execution) error { return nil })
   378  	case "debug":
   379  		return debugFormat(out)
   380  	case "standard-json":
   381  		return standardJSONFormat(out)
   382  	case "standard-verbose":
   383  		return standardVerboseFormat(out)
   384  	case "standard-quiet":
   385  		return standardQuietFormat(out)
   386  	case "dots", "dots-v1":
   387  		return dotsFormatV1(out)
   388  	case "dots-v2":
   389  		return newDotFormatter(out, formatOpts)
   390  	case "gotestdox", "testdox":
   391  		return testDoxFormat(out, formatOpts)
   392  	case "testname", "short-verbose":
   393  		if os.Getenv("GITHUB_ACTIONS") == "true" {
   394  			return githubActionsFormat(out)
   395  		}
   396  		return testNameFormat(out)
   397  	case "pkgname", "short":
   398  		return pkgNameFormat(out, formatOpts)
   399  	case "pkgname-and-test-fails", "short-with-failures":
   400  		return pkgNameWithFailuresFormat(out, formatOpts)
   401  	case "github-actions", "github-action":
   402  		return githubActionsFormat(out)
   403  	default:
   404  		return nil
   405  	}
   406  }
   407  
   408  func githubActionsFormat(out io.Writer) EventFormatter {
   409  	buf := bufio.NewWriter(out)
   410  
   411  	type name struct {
   412  		Package string
   413  		Test    string
   414  	}
   415  	output := map[name][]string{}
   416  
   417  	return eventFormatterFunc(func(event TestEvent, exec *Execution) error {
   418  		key := name{Package: event.Package, Test: event.Test}
   419  
   420  		// test case output
   421  		if event.Test != "" && event.Action == ActionOutput {
   422  			if !isFramingLine(event.Output, event.Test) {
   423  				output[key] = append(output[key], event.Output)
   424  			}
   425  			return nil
   426  		}
   427  
   428  		// test case end event
   429  		if event.Test != "" && event.Action.IsTerminal() {
   430  			if len(output[key]) > 0 {
   431  				buf.WriteString("::group::")
   432  			} else {
   433  				buf.WriteString("  ")
   434  			}
   435  			testNameFormatTestEvent(buf, event)
   436  
   437  			for _, item := range output[key] {
   438  				buf.WriteString(item)
   439  			}
   440  			if len(output[key]) > 0 {
   441  				buf.WriteString("\n::endgroup::\n")
   442  			}
   443  			delete(output, key)
   444  			return buf.Flush()
   445  		}
   446  
   447  		// package event
   448  		if !event.Action.IsTerminal() {
   449  			return nil
   450  		}
   451  
   452  		result := colorEvent(event)(strings.ToUpper(string(event.Action)))
   453  		pkg := exec.Package(event.Package)
   454  		if event.Action == ActionSkip || (event.Action == ActionPass && pkg.Total == 0) {
   455  			event.Action = ActionSkip // always color these as skip actions
   456  			result = colorEvent(event)("EMPTY")
   457  		}
   458  
   459  		buf.WriteString("  ")
   460  		buf.WriteString(result)
   461  		buf.WriteString(" Package ")
   462  		buf.WriteString(packageLine(event, exec.Package(event.Package)))
   463  		buf.WriteString("\n")
   464  		return buf.Flush()
   465  	})
   466  }