/* The stenographer is used by Ginkgo's reporters to generate output. Move along, nothing to see here. */ package stenographer import ( "fmt" "io" "runtime" "strings" "github.com/onsi/ginkgo/types" ) const defaultStyle = "\x1b[0m" const boldStyle = "\x1b[1m" const redColor = "\x1b[91m" const greenColor = "\x1b[32m" const yellowColor = "\x1b[33m" const cyanColor = "\x1b[36m" const grayColor = "\x1b[90m" const lightGrayColor = "\x1b[37m" type cursorStateType int const ( cursorStateTop cursorStateType = iota cursorStateStreaming cursorStateMidBlock cursorStateEndBlock ) type Stenographer interface { AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) AnnounceAggregatedParallelRun(nodes int, succinct bool) AnnounceParallelRun(node int, nodes int, succinct bool) AnnounceTotalNumberOfSpecs(total int, succinct bool) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) AnnounceSpecWillRun(spec *types.SpecSummary) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) AnnounceCapturedOutput(output string) AnnounceSuccessfulSpec(spec *types.SpecSummary) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) SummarizeFailures(summaries []*types.SpecSummary) } func New(color bool, enableFlakes bool, writer io.Writer) Stenographer { denoter := "•" if runtime.GOOS == "windows" { denoter = "+" } return &consoleStenographer{ color: color, denoter: denoter, cursorState: cursorStateTop, enableFlakes: enableFlakes, w: writer, } } type consoleStenographer struct { color bool denoter string cursorState cursorStateType enableFlakes bool w io.Writer } var alternatingColors = []string{defaultStyle, grayColor} func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) { if succinct { s.print(0, "[%d] %s ", randomSeed, s.colorize(boldStyle, description)) return } s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=") s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed)) if randomizingAll { s.print(0, " - Will randomize all specs") } s.printNewLine() } func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) { if succinct { s.print(0, "- node #%d ", node) return } s.println(0, "Parallel test node %s/%s.", s.colorize(boldStyle, "%d", node), s.colorize(boldStyle, "%d", nodes), ) s.printNewLine() } func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) { if succinct { s.print(0, "- %d nodes ", nodes) return } s.println(0, "Running in parallel across %s nodes", s.colorize(boldStyle, "%d", nodes), ) s.printNewLine() } func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) { if succinct { s.print(0, "- %d/%d specs ", specsToRun, total) s.stream() return } s.println(0, "Will run %s of %s specs", s.colorize(boldStyle, "%d", specsToRun), s.colorize(boldStyle, "%d", total), ) s.printNewLine() } func (s *consoleStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) { if succinct { s.print(0, "- %d specs ", total) s.stream() return } s.println(0, "Will run %s specs", s.colorize(boldStyle, "%d", total), ) s.printNewLine() } func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) { if succinct && summary.SuiteSucceeded { s.print(0, " %s %s ", s.colorize(greenColor, "SUCCESS!"), summary.RunTime) return } s.printNewLine() color := greenColor if !summary.SuiteSucceeded { color = redColor } s.println(0, s.colorize(boldStyle+color, "Ran %d of %d Specs in %.3f seconds", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, summary.RunTime.Seconds())) status := "" if summary.SuiteSucceeded { status = s.colorize(boldStyle+greenColor, "SUCCESS!") } else { status = s.colorize(boldStyle+redColor, "FAIL!") } flakes := "" if s.enableFlakes { flakes = " | " + s.colorize(yellowColor+boldStyle, "%d Flaked", summary.NumberOfFlakedSpecs) } s.print(0, "%s -- %s | %s | %s | %s\n", status, s.colorize(greenColor+boldStyle, "%d Passed", summary.NumberOfPassedSpecs), s.colorize(redColor+boldStyle, "%d Failed", summary.NumberOfFailedSpecs)+flakes, s.colorize(yellowColor+boldStyle, "%d Pending", summary.NumberOfPendingSpecs), s.colorize(cyanColor+boldStyle, "%d Skipped", summary.NumberOfSkippedSpecs), ) } func (s *consoleStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) { s.startBlock() for i, text := range spec.ComponentTexts[1 : len(spec.ComponentTexts)-1] { s.print(0, s.colorize(alternatingColors[i%2], text)+" ") } indentation := 0 if len(spec.ComponentTexts) > 2 { indentation = 1 s.printNewLine() } index := len(spec.ComponentTexts) - 1 s.print(indentation, s.colorize(boldStyle, spec.ComponentTexts[index])) s.printNewLine() s.print(indentation, s.colorize(lightGrayColor, spec.ComponentCodeLocations[index].String())) s.printNewLine() s.midBlock() } func (s *consoleStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { s.announceSetupFailure("BeforeSuite", summary, succinct, fullTrace) } func (s *consoleStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) { s.announceSetupFailure("AfterSuite", summary, succinct, fullTrace) } func (s *consoleStenographer) announceSetupFailure(name string, summary *types.SetupSummary, succinct bool, fullTrace bool) { s.startBlock() var message string switch summary.State { case types.SpecStateFailed: message = "Failure" case types.SpecStatePanicked: message = "Panic" case types.SpecStateTimedOut: message = "Timeout" } s.println(0, s.colorize(redColor+boldStyle, "%s [%.3f seconds]", message, summary.RunTime.Seconds())) indentation := s.printCodeLocationBlock([]string{name}, []types.CodeLocation{summary.CodeLocation}, summary.ComponentType, 0, summary.State, true) s.printNewLine() s.printFailure(indentation, summary.State, summary.Failure, fullTrace) s.endBlock() } func (s *consoleStenographer) AnnounceCapturedOutput(output string) { if output == "" { return } s.startBlock() s.println(0, output) s.midBlock() } func (s *consoleStenographer) AnnounceSuccessfulSpec(spec *types.SpecSummary) { s.print(0, s.colorize(greenColor, s.denoter)) s.stream() } func (s *consoleStenographer) AnnounceSuccessfulSlowSpec(spec *types.SpecSummary, succinct bool) { s.printBlockWithMessage( s.colorize(greenColor, "%s [SLOW TEST:%.3f seconds]", s.denoter, spec.RunTime.Seconds()), "", spec, succinct, ) } func (s *consoleStenographer) AnnounceSuccessfulMeasurement(spec *types.SpecSummary, succinct bool) { s.printBlockWithMessage( s.colorize(greenColor, "%s [MEASUREMENT]", s.denoter), s.measurementReport(spec, succinct), spec, succinct, ) } func (s *consoleStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) { if noisy { s.printBlockWithMessage( s.colorize(yellowColor, "P [PENDING]"), "", spec, false, ) } else { s.print(0, s.colorize(yellowColor, "P")) s.stream() } } func (s *consoleStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) { // Skips at runtime will have a non-empty spec.Failure. All others should be succinct. if succinct || spec.Failure == (types.SpecFailure{}) { s.print(0, s.colorize(cyanColor, "S")) s.stream() } else { s.startBlock() s.println(0, s.colorize(cyanColor+boldStyle, "S [SKIPPING]%s [%.3f seconds]", s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) s.printNewLine() s.printSkip(indentation, spec.Failure) s.endBlock() } } func (s *consoleStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) { s.printSpecFailure(fmt.Sprintf("%s... Timeout", s.denoter), spec, succinct, fullTrace) } func (s *consoleStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) { s.printSpecFailure(fmt.Sprintf("%s! Panic", s.denoter), spec, succinct, fullTrace) } func (s *consoleStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) { s.printSpecFailure(fmt.Sprintf("%s Failure", s.denoter), spec, succinct, fullTrace) } func (s *consoleStenographer) SummarizeFailures(summaries []*types.SpecSummary) { failingSpecs := []*types.SpecSummary{} for _, summary := range summaries { if summary.HasFailureState() { failingSpecs = append(failingSpecs, summary) } } if len(failingSpecs) == 0 { return } s.printNewLine() s.printNewLine() plural := "s" if len(failingSpecs) == 1 { plural = "" } s.println(0, s.colorize(redColor+boldStyle, "Summarizing %d Failure%s:", len(failingSpecs), plural)) for _, summary := range failingSpecs { s.printNewLine() if summary.HasFailureState() { if summary.TimedOut() { s.print(0, s.colorize(redColor+boldStyle, "[Timeout...] ")) } else if summary.Panicked() { s.print(0, s.colorize(redColor+boldStyle, "[Panic!] ")) } else if summary.Failed() { s.print(0, s.colorize(redColor+boldStyle, "[Fail] ")) } s.printSpecContext(summary.ComponentTexts, summary.ComponentCodeLocations, summary.Failure.ComponentType, summary.Failure.ComponentIndex, summary.State, true) s.printNewLine() s.println(0, s.colorize(lightGrayColor, summary.Failure.Location.String())) } } } func (s *consoleStenographer) startBlock() { if s.cursorState == cursorStateStreaming { s.printNewLine() s.printDelimiter() } else if s.cursorState == cursorStateMidBlock { s.printNewLine() } } func (s *consoleStenographer) midBlock() { s.cursorState = cursorStateMidBlock } func (s *consoleStenographer) endBlock() { s.printDelimiter() s.cursorState = cursorStateEndBlock } func (s *consoleStenographer) stream() { s.cursorState = cursorStateStreaming } func (s *consoleStenographer) printBlockWithMessage(header string, message string, spec *types.SpecSummary, succinct bool) { s.startBlock() s.println(0, header) indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, types.SpecComponentTypeInvalid, 0, spec.State, succinct) if message != "" { s.printNewLine() s.println(indentation, message) } s.endBlock() } func (s *consoleStenographer) printSpecFailure(message string, spec *types.SpecSummary, succinct bool, fullTrace bool) { s.startBlock() s.println(0, s.colorize(redColor+boldStyle, "%s%s [%.3f seconds]", message, s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds())) indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct) s.printNewLine() s.printFailure(indentation, spec.State, spec.Failure, fullTrace) s.endBlock() } func (s *consoleStenographer) failureContext(failedComponentType types.SpecComponentType) string { switch failedComponentType { case types.SpecComponentTypeBeforeSuite: return " in Suite Setup (BeforeSuite)" case types.SpecComponentTypeAfterSuite: return " in Suite Teardown (AfterSuite)" case types.SpecComponentTypeBeforeEach: return " in Spec Setup (BeforeEach)" case types.SpecComponentTypeJustBeforeEach: return " in Spec Setup (JustBeforeEach)" case types.SpecComponentTypeAfterEach: return " in Spec Teardown (AfterEach)" } return "" } func (s *consoleStenographer) printSkip(indentation int, spec types.SpecFailure) { s.println(indentation, s.colorize(cyanColor, spec.Message)) s.printNewLine() s.println(indentation, spec.Location.String()) } func (s *consoleStenographer) printFailure(indentation int, state types.SpecState, failure types.SpecFailure, fullTrace bool) { if state == types.SpecStatePanicked { s.println(indentation, s.colorize(redColor+boldStyle, failure.Message)) s.println(indentation, s.colorize(redColor, failure.ForwardedPanic)) s.println(indentation, failure.Location.String()) s.printNewLine() s.println(indentation, s.colorize(redColor, "Full Stack Trace")) s.println(indentation, failure.Location.FullStackTrace) } else { s.println(indentation, s.colorize(redColor, failure.Message)) s.printNewLine() s.println(indentation, failure.Location.String()) if fullTrace { s.printNewLine() s.println(indentation, s.colorize(redColor, "Full Stack Trace")) s.println(indentation, failure.Location.FullStackTrace) } } } func (s *consoleStenographer) printSpecContext(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { startIndex := 1 indentation := 0 if len(componentTexts) == 1 { startIndex = 0 } for i := startIndex; i < len(componentTexts); i++ { if (state.IsFailure() || state == types.SpecStateSkipped) && i == failedComponentIndex { color := redColor if state == types.SpecStateSkipped { color = cyanColor } blockType := "" switch failedComponentType { case types.SpecComponentTypeBeforeSuite: blockType = "BeforeSuite" case types.SpecComponentTypeAfterSuite: blockType = "AfterSuite" case types.SpecComponentTypeBeforeEach: blockType = "BeforeEach" case types.SpecComponentTypeJustBeforeEach: blockType = "JustBeforeEach" case types.SpecComponentTypeAfterEach: blockType = "AfterEach" case types.SpecComponentTypeIt: blockType = "It" case types.SpecComponentTypeMeasure: blockType = "Measurement" } if succinct { s.print(0, s.colorize(color+boldStyle, "[%s] %s ", blockType, componentTexts[i])) } else { s.println(indentation, s.colorize(color+boldStyle, "%s [%s]", componentTexts[i], blockType)) s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) } } else { if succinct { s.print(0, s.colorize(alternatingColors[i%2], "%s ", componentTexts[i])) } else { s.println(indentation, componentTexts[i]) s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i])) } } indentation++ } return indentation } func (s *consoleStenographer) printCodeLocationBlock(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int { indentation := s.printSpecContext(componentTexts, componentCodeLocations, failedComponentType, failedComponentIndex, state, succinct) if succinct { if len(componentTexts) > 0 { s.printNewLine() s.print(0, s.colorize(lightGrayColor, "%s", componentCodeLocations[len(componentCodeLocations)-1])) } s.printNewLine() indentation = 1 } else { indentation-- } return indentation } func (s *consoleStenographer) orderedMeasurementKeys(measurements map[string]*types.SpecMeasurement) []string { orderedKeys := make([]string, len(measurements)) for key, measurement := range measurements { orderedKeys[measurement.Order] = key } return orderedKeys } func (s *consoleStenographer) measurementReport(spec *types.SpecSummary, succinct bool) string { if len(spec.Measurements) == 0 { return "Found no measurements" } message := []string{} orderedKeys := s.orderedMeasurementKeys(spec.Measurements) if succinct { message = append(message, fmt.Sprintf("%s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) for _, key := range orderedKeys { measurement := spec.Measurements[key] message = append(message, fmt.Sprintf(" %s - %s: %s%s, %s: %s%s ± %s%s, %s: %s%s", s.colorize(boldStyle, "%s", measurement.Name), measurement.SmallestLabel, s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), measurement.Units, measurement.AverageLabel, s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), measurement.Units, s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), measurement.Units, measurement.LargestLabel, s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), measurement.Units, )) } } else { message = append(message, fmt.Sprintf("Ran %s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples))) for _, key := range orderedKeys { measurement := spec.Measurements[key] info := "" if measurement.Info != nil { message = append(message, fmt.Sprintf("%v", measurement.Info)) } message = append(message, fmt.Sprintf("%s:\n%s %s: %s%s\n %s: %s%s\n %s: %s%s ± %s%s", s.colorize(boldStyle, "%s", measurement.Name), info, measurement.SmallestLabel, s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest), measurement.Units, measurement.LargestLabel, s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest), measurement.Units, measurement.AverageLabel, s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average), measurement.Units, s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation), measurement.Units, )) } } return strings.Join(message, "\n") }