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 }