Skip to content

Commit 73c59df

Browse files
woodieonsi
authored andcommitted
feat: add -fd flag for RSpec-style documentation output
1 parent 76a2074 commit 73c59df

4 files changed

Lines changed: 177 additions & 4 deletions

File tree

‎docs/index.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3839,12 +3839,14 @@ Ginkgo emits a real-time report of the progress of your spec suite to the consol
38393839
There are several CLI flags that allow you to tweak this output:
38403840

38413841
#### Controlling Verbosity
3842-
Ginkgo has four verbosity settings: succinct (the default when running multiple suites), normal (the default when running a single suite), verbose, and very-verbose.
3842+
Ginkgo has five verbosity settings: succinct (the default when running multiple suites), normal (the default when running a single suite), verbose, and very-verbose.
38433843

3844-
You can opt into succinct mode with `ginkgo --succinct`, verbose mode with `ginkgo -v` and very-verbose mode with `ginkgo -vv`.
3844+
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`.
38453845

38463846
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.
38473847

3848+
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.
3849+
38483850
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.
38493851

38503852
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).

‎reporters/default_reporter.go‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type DefaultReporter struct {
3131
specDenoter string
3232
retryDenoter string
3333
formatter formatter.Formatter
34+
fdHierarchy []string
3435

3536
runningInParallel bool
3637
lock *sync.Mutex
@@ -67,6 +68,9 @@ func NewDefaultReporter(conf types.ReporterConfig, writer io.Writer) *DefaultRep
6768
/* The Reporter Interface */
6869

6970
func (r *DefaultReporter) SuiteWillBegin(report types.Report) {
71+
if r.conf.FdOutput {
72+
return
73+
}
7074
if r.conf.Verbosity().Is(types.VerbosityLevelSuccinct) {
7175
r.emit(r.f("[%d] {{bold}}%s{{/}} ", report.SuiteConfig.RandomSeed, report.SuiteDescription))
7276
if len(report.SuiteLabels) > 0 {
@@ -123,6 +127,10 @@ func (r *DefaultReporter) SuiteWillBegin(report types.Report) {
123127
}
124128

125129
func (r *DefaultReporter) SuiteDidEnd(report types.Report) {
130+
if r.conf.FdOutput {
131+
r.emitSuiteFooter(report)
132+
return
133+
}
126134
failures := report.SpecReports.WithState(types.SpecStateFailureStates)
127135
if len(failures) > 0 {
128136
r.emitBlock("\n")
@@ -147,8 +155,10 @@ func (r *DefaultReporter) SuiteDidEnd(report types.Report) {
147155
r.emitBlock(r.fi(1, highlightColor+"%s{{/}} %s", heading, locationBlock))
148156
}
149157
}
158+
r.emitSuiteFooter(report)
159+
}
150160

151-
//summarize the suite
161+
func (r *DefaultReporter) emitSuiteFooter(report types.Report) {
152162
if r.conf.Verbosity().Is(types.VerbosityLevelSuccinct) && report.SuiteSucceeded {
153163
r.emit(r.f(" {{green}}SUCCESS!{{/}} %s ", report.RunTime))
154164
return
@@ -219,6 +229,10 @@ func (r *DefaultReporter) wrapTextBlock(sectionName string, fn func()) {
219229
}
220230

221231
func (r *DefaultReporter) DidRun(report types.SpecReport) {
232+
if r.conf.FdOutput {
233+
r.didRunFd(report)
234+
return
235+
}
222236
v := r.conf.Verbosity()
223237
inParallel := report.RunningInParallel
224238

@@ -358,6 +372,51 @@ func (r *DefaultReporter) DidRun(report types.SpecReport) {
358372
r.emitDelimiter(0)
359373
}
360374

375+
func (r *DefaultReporter) didRunFd(report types.SpecReport) {
376+
r.lock.Lock()
377+
defer r.lock.Unlock()
378+
379+
if !report.LeafNodeType.Is(types.NodeTypeIt) {
380+
return
381+
}
382+
383+
hierarchy := report.ContainerHierarchyTexts
384+
385+
// blank line when top-level container changes
386+
if len(r.fdHierarchy) > 0 &&
387+
(len(hierarchy) == 0 || hierarchy[0] != r.fdHierarchy[0]) {
388+
fmt.Fprintln(r.writer)
389+
}
390+
391+
// emit newly-diverged container lines
392+
divergeAt := 0
393+
for divergeAt < len(r.fdHierarchy) && divergeAt < len(hierarchy) &&
394+
r.fdHierarchy[divergeAt] == hierarchy[divergeAt] {
395+
divergeAt++
396+
}
397+
for i := divergeAt; i < len(hierarchy); i++ {
398+
fmt.Fprintf(r.writer, "%s%s\n", strings.Repeat(" ", i), hierarchy[i])
399+
}
400+
401+
// leaf label
402+
depth := len(hierarchy)
403+
indent := strings.Repeat(" ", depth)
404+
label := report.LeafNodeText
405+
406+
switch report.State {
407+
case types.SpecStateFailed, types.SpecStatePanicked:
408+
label = fmt.Sprintf("%s (FAILED)", label)
409+
case types.SpecStatePending:
410+
label = fmt.Sprintf("%s (PENDING)", label)
411+
case types.SpecStateSkipped:
412+
label = fmt.Sprintf("%s (SKIPPED)", label)
413+
}
414+
415+
color := r.highlightColorForState(report.State)
416+
fmt.Fprintf(r.writer, "%s%s\n", indent, r.f(color+"%s{{/}}", label))
417+
r.fdHierarchy = hierarchy
418+
}
419+
361420
func (r *DefaultReporter) highlightColorForState(state types.SpecState) string {
362421
switch state {
363422
case types.SpecStatePassed:

‎reporters/default_reporter_test.go‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2951,4 +2951,114 @@ var _ = Describe("DefaultReporter", func() {
29512951
"",
29522952
),
29532953
)
2954+
2955+
Describe("Rendering with FdOutput", func() {
2956+
var buf strings.Builder
2957+
var reporter *reporters.DefaultReporter
2958+
2959+
BeforeEach(func() {
2960+
buf.Reset()
2961+
reporter = reporters.NewDefaultReporterUnderTest(types.ReporterConfig{FdOutput: true}, &buf)
2962+
})
2963+
2964+
Context("with a passing report", func() {
2965+
BeforeEach(func() {
2966+
reporter.SuiteWillBegin(types.Report{SuiteDescription: "Something"})
2967+
reporter.DidRun(types.SpecReport{
2968+
ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"},
2969+
LeafNodeText: "creates the directory",
2970+
LeafNodeType: types.NodeTypeIt,
2971+
State: types.SpecStatePassed,
2972+
})
2973+
reporter.DidRun(types.SpecReport{
2974+
ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"},
2975+
LeafNodeText: "does not error",
2976+
LeafNodeType: types.NodeTypeIt,
2977+
State: types.SpecStatePassed,
2978+
})
2979+
reporter.DidRun(types.SpecReport{
2980+
ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is a symlink"},
2981+
LeafNodeText: "does not error",
2982+
LeafNodeType: types.NodeTypeIt,
2983+
State: types.SpecStatePassed,
2984+
})
2985+
reporter.SuiteDidEnd(types.Report{
2986+
SpecReports: types.SpecReports{
2987+
{LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed},
2988+
{LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed},
2989+
{LeafNodeType: types.NodeTypeIt, State: types.SpecStatePassed},
2990+
},
2991+
})
2992+
})
2993+
2994+
It("emits no banner", func() {
2995+
Expect(buf.String()).NotTo(ContainSubstring("Running Suite"))
2996+
})
2997+
It("indents container hierarchy", func() {
2998+
Expect(buf.String()).To(ContainSubstring("checkAttachmentDir"))
2999+
Expect(buf.String()).To(ContainSubstring(" when the path is missing"))
3000+
})
3001+
It("indents leaf nodes", func() {
3002+
Expect(buf.String()).To(ContainSubstring("creates the directory"))
3003+
})
3004+
It("deduplicates shared hierarchy", func() {
3005+
Expect(strings.Count(buf.String(), "when the path is missing")).To(Equal(1))
3006+
})
3007+
It("prints the summary", func() {
3008+
Expect(buf.String()).To(ContainSubstring("Passed"))
3009+
})
3010+
})
3011+
3012+
Context("with a failing report", func() {
3013+
BeforeEach(func() {
3014+
reporter.SuiteWillBegin(types.Report{})
3015+
reporter.DidRun(types.SpecReport{
3016+
ContainerHierarchyTexts: []string{"checkAttachmentDir", "when the path is missing"},
3017+
LeafNodeText: "creates the directory",
3018+
LeafNodeType: types.NodeTypeIt,
3019+
State: types.SpecStateFailed,
3020+
Failure: types.Failure{
3021+
Message: "Expected file to exist",
3022+
Location: types.CodeLocation{FileName: "main_test.go", LineNumber: 42},
3023+
},
3024+
})
3025+
reporter.SuiteDidEnd(types.Report{
3026+
SpecReports: types.SpecReports{
3027+
{LeafNodeType: types.NodeTypeIt, State: types.SpecStateFailed},
3028+
},
3029+
})
3030+
})
3031+
3032+
It("annotates the failed spec", func() {
3033+
Expect(buf.String()).To(ContainSubstring("creates the directory (FAILED)"))
3034+
})
3035+
It("prints the failures section", func() {
3036+
Expect(buf.String()).To(ContainSubstring("FAIL!"))
3037+
})
3038+
})
3039+
3040+
Context("with a blank line between top-level containers", func() {
3041+
BeforeEach(func() {
3042+
reporter.SuiteWillBegin(types.Report{})
3043+
reporter.DidRun(types.SpecReport{
3044+
ContainerHierarchyTexts: []string{"DescribeA"},
3045+
LeafNodeText: "does something",
3046+
LeafNodeType: types.NodeTypeIt,
3047+
State: types.SpecStatePassed,
3048+
})
3049+
reporter.DidRun(types.SpecReport{
3050+
ContainerHierarchyTexts: []string{"DescribeB"},
3051+
LeafNodeText: "does something else",
3052+
LeafNodeType: types.NodeTypeIt,
3053+
State: types.SpecStatePassed,
3054+
})
3055+
reporter.SuiteDidEnd(types.Report{SpecReports: types.SpecReports{}})
3056+
})
3057+
3058+
It("emits a blank line between top-level containers", func() {
3059+
Expect(buf.String()).To(ContainSubstring("DescribeA"))
3060+
Expect(buf.String()).To(ContainSubstring("\n\nDescribeB"))
3061+
})
3062+
})
3063+
})
29543064
})

‎types/config.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ type ReporterConfig struct {
9595
GithubOutput bool
9696
SilenceSkips bool
9797
ForceNewlines bool
98+
FdOutput bool
9899

99100
JSONReport string
100101
GoJSONReport string
@@ -361,7 +362,8 @@ var ReporterConfigFlags = GinkgoFlags{
361362
Usage: "If set, default reporter will not print out skipped tests."},
362363
{KeyPath: "R.ForceNewlines", Name: "force-newlines", SectionKey: "output",
363364
Usage: "If set, default reporter will ensure a newline appears after each test."},
364-
365+
{KeyPath: "R.FdOutput", Name: "fd", SectionKey: "output",
366+
Usage: "If set, emits RSpec-style 'format documentation' output instead of Ginkgo's default output."},
365367
{KeyPath: "R.JSONReport", Name: "json-report", UsageArgument: "filename.json", SectionKey: "output",
366368
Usage: "If set, Ginkgo will generate a JSON-formatted test report at the specified location."},
367369
{KeyPath: "R.GoJSONReport", Name: "gojson-report", UsageArgument: "filename.json", SectionKey: "output",

0 commit comments

Comments
 (0)