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 }