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

     1  package testjson
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"strings"
     7  	"time"
     8  	"unicode"
     9  	"unicode/utf8"
    10  
    11  	"github.com/fatih/color"
    12  )
    13  
    14  // Summary enumerates the sections which can be printed by PrintSummary
    15  type Summary int
    16  
    17  // nolint: golint
    18  const (
    19  	SummarizeNone    Summary = 0
    20  	SummarizeSkipped Summary = (1 << iota) / 2
    21  	SummarizeFailed
    22  	SummarizeErrors
    23  	SummarizeOutput
    24  	SummarizeAll = SummarizeSkipped | SummarizeFailed | SummarizeErrors | SummarizeOutput
    25  )
    26  
    27  var summaryValues = map[Summary]string{
    28  	SummarizeSkipped: "skipped",
    29  	SummarizeFailed:  "failed",
    30  	SummarizeErrors:  "errors",
    31  	SummarizeOutput:  "output",
    32  }
    33  
    34  var summaryFromValue = map[string]Summary{
    35  	"none":    SummarizeNone,
    36  	"skipped": SummarizeSkipped,
    37  	"failed":  SummarizeFailed,
    38  	"errors":  SummarizeErrors,
    39  	"output":  SummarizeOutput,
    40  	"all":     SummarizeAll,
    41  }
    42  
    43  func (s Summary) String() string {
    44  	if s == SummarizeNone {
    45  		return "none"
    46  	}
    47  	var result []string
    48  	for v := Summary(1); v <= s; v <<= 1 {
    49  		if s.Includes(v) {
    50  			result = append(result, summaryValues[v])
    51  		}
    52  	}
    53  	return strings.Join(result, ",")
    54  }
    55  
    56  // Includes returns true if Summary includes all the values set by other.
    57  func (s Summary) Includes(other Summary) bool {
    58  	return s&other == other
    59  }
    60  
    61  // NewSummary returns a new Summary from a string value. If the string does not
    62  // match any known values returns false for the second value.
    63  func NewSummary(value string) (Summary, bool) {
    64  	s, ok := summaryFromValue[value]
    65  	return s, ok
    66  }
    67  
    68  // PrintSummary of a test Execution. Prints a section for each summary type
    69  // followed by a DONE line to out.
    70  func PrintSummary(out io.Writer, execution *Execution, opts Summary) {
    71  	execSummary := newExecSummary(execution, opts)
    72  	if opts.Includes(SummarizeSkipped) {
    73  		writeTestCaseSummary(out, execSummary, formatSkipped())
    74  	}
    75  	if opts.Includes(SummarizeFailed) {
    76  		writeTestCaseSummary(out, execSummary, formatFailed())
    77  	}
    78  
    79  	errors := execution.Errors()
    80  	if opts.Includes(SummarizeErrors) {
    81  		writeErrorSummary(out, errors)
    82  	}
    83  
    84  	fmt.Fprintf(out, "\n%s %d tests%s%s%s in %s\n",
    85  		formatExecStatus(execution),
    86  		execution.Total(),
    87  		formatTestCount(len(execution.Skipped()), "skipped", ""),
    88  		formatTestCount(len(execution.Failed()), "failure", "s"),
    89  		formatTestCount(countErrors(errors), "error", "s"),
    90  		FormatDurationAsSeconds(execution.Elapsed(), 3))
    91  }
    92  
    93  func formatTestCount(count int, category string, pluralize string) string {
    94  	switch count {
    95  	case 0:
    96  		return ""
    97  	case 1:
    98  	default:
    99  		category += pluralize
   100  	}
   101  	return fmt.Sprintf(", %d %s", count, category)
   102  }
   103  
   104  func formatExecStatus(exec *Execution) string {
   105  	if !exec.done {
   106  		return ""
   107  	}
   108  	var runs string
   109  	if exec.lastRunID > 0 {
   110  		runs = fmt.Sprintf(" %d runs,", exec.lastRunID+1)
   111  	}
   112  	return "DONE" + runs
   113  }
   114  
   115  // FormatDurationAsSeconds formats a time.Duration as a float with an s suffix.
   116  func FormatDurationAsSeconds(d time.Duration, precision int) string {
   117  	if d == neverFinished {
   118  		return "unknown"
   119  	}
   120  	return fmt.Sprintf("%.[2]*[1]fs", d.Seconds(), precision)
   121  }
   122  
   123  func writeErrorSummary(out io.Writer, errors []string) {
   124  	if len(errors) > 0 {
   125  		fmt.Fprintln(out, color.MagentaString("\n=== Errors"))
   126  	}
   127  	for _, err := range errors {
   128  		fmt.Fprintln(out, err)
   129  	}
   130  }
   131  
   132  // countErrors in stderr lines. Build errors may include multiple lines where
   133  // subsequent lines are indented.
   134  // FIXME: Panics will include multiple lines, and are still overcounted.
   135  func countErrors(errors []string) int {
   136  	var count int
   137  	for _, line := range errors {
   138  		r, _ := utf8.DecodeRuneInString(line)
   139  		if !unicode.IsSpace(r) {
   140  			count++
   141  		}
   142  	}
   143  	return count
   144  }
   145  
   146  type executionSummary interface {
   147  	Failed() []TestCase
   148  	Skipped() []TestCase
   149  	OutputLines(TestCase) []string
   150  }
   151  
   152  type noOutputSummary struct {
   153  	*Execution
   154  }
   155  
   156  func (s *noOutputSummary) OutputLines(_ TestCase) []string {
   157  	return nil
   158  }
   159  
   160  func newExecSummary(execution *Execution, opts Summary) executionSummary {
   161  	if opts.Includes(SummarizeOutput) {
   162  		return execution
   163  	}
   164  	return &noOutputSummary{Execution: execution}
   165  }
   166  
   167  func writeTestCaseSummary(out io.Writer, execution executionSummary, conf testCaseFormatConfig) {
   168  	testCases := conf.getter(execution)
   169  	if len(testCases) == 0 {
   170  		return
   171  	}
   172  	fmt.Fprintln(out, "\n=== "+conf.header)
   173  	for idx, tc := range testCases {
   174  		fmt.Fprintf(out, "=== %s: %s %s%s (%s)\n",
   175  			conf.prefix,
   176  			RelativePackagePath(tc.Package),
   177  			tc.Test,
   178  			formatRunID(tc.RunID),
   179  			FormatDurationAsSeconds(tc.Elapsed, 2))
   180  		for _, line := range execution.OutputLines(tc) {
   181  			if isFramingLine(line, tc.Test.Name()) {
   182  				continue
   183  			}
   184  			fmt.Fprint(out, line)
   185  		}
   186  		if _, isNoOutput := execution.(*noOutputSummary); !isNoOutput && idx+1 != len(testCases) {
   187  			fmt.Fprintln(out)
   188  		}
   189  	}
   190  }
   191  
   192  type testCaseFormatConfig struct {
   193  	header string
   194  	prefix string
   195  	getter func(executionSummary) []TestCase
   196  }
   197  
   198  func formatFailed() testCaseFormatConfig {
   199  	withColor := color.RedString
   200  	return testCaseFormatConfig{
   201  		header: withColor("Failed"),
   202  		prefix: withColor("FAIL"),
   203  		getter: func(execution executionSummary) []TestCase {
   204  			return execution.Failed()
   205  		},
   206  	}
   207  }
   208  
   209  func formatSkipped() testCaseFormatConfig {
   210  	withColor := color.YellowString
   211  	return testCaseFormatConfig{
   212  		header: withColor("Skipped"),
   213  		prefix: withColor("SKIP"),
   214  		getter: func(execution executionSummary) []TestCase {
   215  			return execution.Skipped()
   216  		},
   217  	}
   218  }
   219  
   220  func isFramingLine(line string, testName string) bool {
   221  	return strings.HasPrefix(line, "=== RUN   Test") ||
   222  		strings.HasPrefix(line, "=== PAUSE Test") ||
   223  		strings.HasPrefix(line, "=== CONT  Test") ||
   224  		strings.HasPrefix(line, "--- FAIL: "+testName+" ") ||
   225  		strings.HasPrefix(line, "--- SKIP: "+testName+" ") ||
   226  		strings.HasPrefix(line, "--- PASS: "+testName+" ")
   227  }