diff --git a/docs/index.md b/docs/index.md index 0746c72ed..9b5a60fea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3764,12 +3764,14 @@ Ginkgo emits a real-time report of the progress of your spec suite to the consol There are several CLI flags that allow you to tweak this output: #### Controlling Verbosity -Ginkgo has four verbosity settings: succinct (the default when running multiple suites), normal (the default when running a single suite), verbose, and very-verbose. +Ginkgo has five verbosity settings: succinct (the default when running multiple suites), normal (the default when running a single suite), verbose, and very-verbose. -You can opt into succinct mode with `ginkgo --succinct`, verbose mode with `ginkgo -v` and very-verbose mode with `ginkgo -vv`. +You can opt into succinct mode with `ginkgo --succinct`, format documentation mode with `ginkgo -fd`, verbose mode with `ginkgo -v` and very-verbose mode with `ginkgo -vv`. These settings control the amount of information emitted with each spec. By default (i.e. succinct and normal) Ginkgo only emits detailed information about specs that fail. That includes the location of the spec/failure and a timeline that includes any captured `GinkgoWriter` content alongside a series of relevant spec events. +You can opt into documentation format output with `ginkgo -fd`. This emits a hierarchical tree of spec descriptions, with each spec's name colored green for passing, red for failing, yellow for pending, and cyan for skipped, along with a summary of failures at the end of the suite. This mode will be familiar to Rspec users and has more concise output for very large test suites. + The two verbose settings are most helpful when debugging spec suites. They make Ginkgo emit the detailed timeline information for _every_ spec regardless of failure or success. When running in series with `-v` or `-vv` mode Ginkgo will stream out the timeline in real-time while specs are running. A real-time stream isn't possible when running in parallel (the [streams would be interleaved](https://www.youtube.com/watch?v=jyaLZHiJJnE)); instead Ginkgo emits all this information about each spec right after it completes. Very-verbose mode contains additional information over verbose mode. In particular, `-vv` timelines indicate when individual nodes start and end and also include the full failure descriptions for _every_ failure encountered by the spec. Verbose mode does not include the node start/end events (though this can be turned on with `--show-node-events`) and does not include detailed failure information for anything other than the first (primary) failure. (Additional/subsequent failures typically occur in clean-up nodes and are not as relevant as the primary failure that occurs in a subject or setup node). diff --git a/reporters/default_reporter.go b/reporters/default_reporter.go index be5719a8e..3c863e678 100644 --- a/reporters/default_reporter.go +++ b/reporters/default_reporter.go @@ -31,6 +31,7 @@ type DefaultReporter struct { specDenoter string retryDenoter string formatter formatter.Formatter + fdHierarchy []string runningInParallel bool lock *sync.Mutex @@ -67,6 +68,9 @@ func NewDefaultReporter(conf types.ReporterConfig, writer io.Writer) *DefaultRep /* The Reporter Interface */ func (r *DefaultReporter) SuiteWillBegin(report types.Report) { + if r.conf.FdOutput { + return + } if r.conf.Verbosity().Is(types.VerbosityLevelSuccinct) { r.emit(r.f("[%d] {{bold}}%s{{/}} ", report.SuiteConfig.RandomSeed, report.SuiteDescription)) if len(report.SuiteLabels) > 0 { @@ -123,6 +127,10 @@ func (r *DefaultReporter) SuiteWillBegin(report types.Report) { } func (r *DefaultReporter) SuiteDidEnd(report types.Report) { + if r.conf.FdOutput { + r.emitSuiteFooter(report) + return + } failures := report.SpecReports.WithState(types.SpecStateFailureStates) if len(failures) > 0 { r.emitBlock("\n") @@ -147,8 +155,10 @@ func (r *DefaultReporter) SuiteDidEnd(report types.Report) { r.emitBlock(r.fi(1, highlightColor+"%s{{/}} %s", heading, locationBlock)) } } + r.emitSuiteFooter(report) +} - //summarize the suite +func (r *DefaultReporter) emitSuiteFooter(report types.Report) { if r.conf.Verbosity().Is(types.VerbosityLevelSuccinct) && report.SuiteSucceeded { r.emit(r.f(" {{green}}SUCCESS!{{/}} %s ", report.RunTime)) return @@ -219,6 +229,10 @@ func (r *DefaultReporter) wrapTextBlock(sectionName string, fn func()) { } func (r *DefaultReporter) DidRun(report types.SpecReport) { + if r.conf.FdOutput { + r.didRunFd(report) + return + } v := r.conf.Verbosity() inParallel := report.RunningInParallel @@ -358,6 +372,51 @@ func (r *DefaultReporter) DidRun(report types.SpecReport) { r.emitDelimiter(0) } +func (r *DefaultReporter) didRunFd(report types.SpecReport) { + r.lock.Lock() + defer r.lock.Unlock() + + if !report.LeafNodeType.Is(types.NodeTypeIt) { + return + } + + hierarchy := report.ContainerHierarchyTexts + + // blank line when top-level container changes + if len(r.fdHierarchy) > 0 && + (len(hierarchy) == 0 || hierarchy[0] != r.fdHierarchy[0]) { + fmt.Fprintln(r.writer) + } + + // emit newly-diverged container lines + divergeAt := 0 + for divergeAt < len(r.fdHierarchy) && divergeAt < len(hierarchy) && + r.fdHierarchy[divergeAt] == hierarchy[divergeAt] { + divergeAt++ + } + for i := divergeAt; i < len(hierarchy); i++ { + fmt.Fprintf(r.writer, "%s%s\n", strings.Repeat(" ", i), hierarchy[i]) + } + + // leaf label + depth := len(hierarchy) + indent := strings.Repeat(" ", depth) + label := report.LeafNodeText + + switch report.State { + case types.SpecStateFailed, types.SpecStatePanicked: + label = fmt.Sprintf("%s (FAILED)", label) + case types.SpecStatePending: + label = fmt.Sprintf("%s (PENDING)", label) + case types.SpecStateSkipped: + label = fmt.Sprintf("%s (SKIPPED)", label) + } + + color := r.highlightColorForState(report.State) + fmt.Fprintf(r.writer, "%s%s\n", indent, r.f(color+"%s{{/}}", label)) + r.fdHierarchy = hierarchy +} + func (r *DefaultReporter) highlightColorForState(state types.SpecState) string { switch state { case types.SpecStatePassed: diff --git a/reporters/default_reporter_test.go b/reporters/default_reporter_test.go index 9788a5fd4..d965a42f5 100644 --- a/reporters/default_reporter_test.go +++ b/reporters/default_reporter_test.go @@ -2951,4 +2951,114 @@ var _ = Describe("DefaultReporter", func() { "", ), ) + + Describe("Rendering with FdOutput", func() { + var buf strings.Builder + var reporter *reporters.DefaultReporter + + BeforeEach(func() { + buf.Reset() + reporter = reporters.NewDefaultReporterUnderTest(types.ReporterConfig{FdOutput: true}, &buf) + }) + + Context("with a passing report", func() { + BeforeEach(func() { + reporter.SuiteWillBegin(types.Report{SuiteDescription: "Something"}) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"}, + LeafNodeText: "creates the directory", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + }) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"}, + LeafNodeText: "does not error", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + }) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is a symlink"}, + LeafNodeText: "does not error", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + }) + reporter.SuiteDidEnd(types.Report{ + SpecReports: types.SpecReports{ + {LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed}, + {LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed}, + {LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed}, + }, + }) + }) + + It("emits no banner", func() { + Expect(buf.String()).NotTo(ContainSubstring("Running Suite")) + }) + It("indents container hierarchy", func() { + Expect(buf.String()).To(ContainSubstring("checkAttachmentDir")) + Expect(buf.String()).To(ContainSubstring(" when the path is missing")) + }) + It("indents leaf nodes", func() { + Expect(buf.String()).To(ContainSubstring("creates the directory")) + }) + It("deduplicates shared hierarchy", func() { + Expect(strings.Count(buf.String(), "when the path is missing")).To(Equal(1)) + }) + It("prints the summary", func() { + Expect(buf.String()).To(ContainSubstring("Passed")) + }) + }) + + Context("with a failing report", func() { + BeforeEach(func() { + reporter.SuiteWillBegin(types.Report{}) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"}, + LeafNodeText: "creates the directory", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStateFailed, + Failure: types.Failure{ + Message: "Expected file to exist", + Location: types.CodeLocation{FileName: "main_test.go", LineNumber: 42}, + }, + }) + reporter.SuiteDidEnd(types.Report{ + SpecReports: types.SpecReports{ + {LeafNodeType: types.NodeTypeIt, State: types.SpecStateFailed}, + }, + }) + }) + + It("annotates the failed spec", func() { + Expect(buf.String()).To(ContainSubstring("creates the directory (FAILED)")) + }) + It("prints the failures section", func() { + Expect(buf.String()).To(ContainSubstring("FAIL!")) + }) + }) + + Context("with a blank line between top-level containers", func() { + BeforeEach(func() { + reporter.SuiteWillBegin(types.Report{}) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"DescribeA"}, + LeafNodeText: "does something", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + }) + reporter.DidRun(types.SpecReport{ + ContainerHierarchyTexts: []string{"DescribeB"}, + LeafNodeText: "does something else", + LeafNodeType: types.NodeTypeIt, + State: types.SpecStatePassed, + }) + reporter.SuiteDidEnd(types.Report{SpecReports: types.SpecReports{}}) + }) + + It("emits a blank line between top-level containers", func() { + Expect(buf.String()).To(ContainSubstring("DescribeA")) + Expect(buf.String()).To(ContainSubstring("\n\nDescribeB")) + }) + }) + }) }) diff --git a/types/config.go b/types/config.go index ca64acb27..49270b0c7 100644 --- a/types/config.go +++ b/types/config.go @@ -94,6 +94,7 @@ type ReporterConfig struct { GithubOutput bool SilenceSkips bool ForceNewlines bool + FdOutput bool JSONReport string GoJSONReport string @@ -358,6 +359,8 @@ var ReporterConfigFlags = GinkgoFlags{ Usage: "If set, default reporter will not print out skipped tests."}, {KeyPath: "R.ForceNewlines", Name: "force-newlines", SectionKey: "output", Usage: "If set, default reporter will ensure a newline appears after each test."}, + {KeyPath: "R.FdOutput", Name: "fd", SectionKey: "output", + Usage: "If set, emits RSpec-style 'format documentation' output instead of Ginkgo's default output."}, {KeyPath: "R.JSONReport", Name: "json-report", UsageArgument: "filename.json", SectionKey: "output", Usage: "If set, Ginkgo will generate a JSON-formatted test report at the specified location."},