Skip to content

Commit e1d0b38

Browse files
committed
Add PreviewSpecs() to enable programmatic preview access to the suite report
1 parent 1d2fb67 commit e1d0b38

File tree

7 files changed

+202
-27
lines changed

7 files changed

+202
-27
lines changed

core_dsl.go

+65-26
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var flagSet types.GinkgoFlagSet
3838
var deprecationTracker = types.NewDeprecationTracker()
3939
var suiteConfig = types.NewDefaultSuiteConfig()
4040
var reporterConfig = types.NewDefaultReporterConfig()
41-
var suiteDidRun = false
41+
var suiteDidRun, suiteDidPreview = false, false
4242
var outputInterceptor internal.OutputInterceptor
4343
var client parallel_support.Client
4444

@@ -247,32 +247,12 @@ func RunSpecs(t GinkgoTestingT, description string, args ...interface{}) bool {
247247
if suiteDidRun {
248248
exitIfErr(types.GinkgoErrors.RerunningSuite())
249249
}
250-
suiteDidRun = true
251-
252-
suiteLabels := Labels{}
253-
configErrors := []error{}
254-
for _, arg := range args {
255-
switch arg := arg.(type) {
256-
case types.SuiteConfig:
257-
suiteConfig = arg
258-
case types.ReporterConfig:
259-
reporterConfig = arg
260-
case Labels:
261-
suiteLabels = append(suiteLabels, arg...)
262-
default:
263-
configErrors = append(configErrors, types.GinkgoErrors.UnknownTypePassedToRunSpecs(arg))
264-
}
250+
if suiteDidPreview {
251+
exitIfErr(types.GinkgoErrors.RunAndPreviewSuite())
265252
}
266-
exitIfErrors(configErrors)
253+
suiteDidRun = true
267254

268-
configErrors = types.VetConfig(flagSet, suiteConfig, reporterConfig)
269-
if len(configErrors) > 0 {
270-
fmt.Fprintf(formatter.ColorableStdErr, formatter.F("{{red}}Ginkgo detected configuration issues:{{/}}\n"))
271-
for _, err := range configErrors {
272-
fmt.Fprintf(formatter.ColorableStdErr, err.Error())
273-
}
274-
os.Exit(1)
275-
}
255+
suiteLabels := extractSuiteConfiguration(args)
276256

277257
var reporter reporters.Reporter
278258
if suiteConfig.ParallelTotal == 1 {
@@ -310,7 +290,6 @@ func RunSpecs(t GinkgoTestingT, description string, args ...interface{}) bool {
310290

311291
err := global.Suite.BuildTree()
312292
exitIfErr(err)
313-
314293
suitePath, err := os.Getwd()
315294
exitIfErr(err)
316295
suitePath, err = filepath.Abs(suitePath)
@@ -335,6 +314,66 @@ func RunSpecs(t GinkgoTestingT, description string, args ...interface{}) bool {
335314
return passed
336315
}
337316

317+
func extractSuiteConfiguration(args []interface{}) Labels {
318+
suiteLabels := Labels{}
319+
configErrors := []error{}
320+
for _, arg := range args {
321+
switch arg := arg.(type) {
322+
case types.SuiteConfig:
323+
suiteConfig = arg
324+
case types.ReporterConfig:
325+
reporterConfig = arg
326+
case Labels:
327+
suiteLabels = append(suiteLabels, arg...)
328+
default:
329+
configErrors = append(configErrors, types.GinkgoErrors.UnknownTypePassedToRunSpecs(arg))
330+
}
331+
}
332+
exitIfErrors(configErrors)
333+
334+
configErrors = types.VetConfig(flagSet, suiteConfig, reporterConfig)
335+
if len(configErrors) > 0 {
336+
fmt.Fprintf(formatter.ColorableStdErr, formatter.F("{{red}}Ginkgo detected configuration issues:{{/}}\n"))
337+
for _, err := range configErrors {
338+
fmt.Fprintf(formatter.ColorableStdErr, err.Error())
339+
}
340+
os.Exit(1)
341+
}
342+
343+
return suiteLabels
344+
}
345+
346+
/*
347+
PreviewSpecs walks the testing tree and produces a report without actually invoking the specs.
348+
See http://onsi.github.io/ginkgo/#previewing-specs for more information.
349+
*/
350+
func PreviewSpecs(description string, args ...any) Report {
351+
if suiteDidRun {
352+
exitIfErr(types.GinkgoErrors.RunAndPreviewSuite())
353+
}
354+
355+
suiteLabels := extractSuiteConfiguration(args)
356+
if suiteConfig.ParallelTotal != 1 {
357+
exitIfErr(types.GinkgoErrors.PreviewInParallelConfiguration())
358+
}
359+
suiteConfig.DryRun = true
360+
reporter := reporters.NoopReporter{}
361+
outputInterceptor = internal.NoopOutputInterceptor{}
362+
client = nil
363+
writer := GinkgoWriter.(*internal.Writer)
364+
365+
err := global.Suite.BuildTree()
366+
exitIfErr(err)
367+
suitePath, err := os.Getwd()
368+
exitIfErr(err)
369+
suitePath, err = filepath.Abs(suitePath)
370+
exitIfErr(err)
371+
372+
global.Suite.Run(description, suiteLabels, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig)
373+
374+
return global.Suite.GetPreviewReport()
375+
}
376+
338377
/*
339378
Skip instructs Ginkgo to skip the current spec
340379

docs/index.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -3183,6 +3183,30 @@ A single interrupt (e.g. `SIGINT`/`SIGTERM`) interrupts the current running node
31833183

31843184
If you want to get information about what is currently running in a suite _without_ interrupting it, check out the [Getting Visibility Into Long-Running Specs](#getting-visibility-into-long-running-specs) section above.
31853185

3186+
### Previewing Specs
3187+
3188+
Ginkgo provides a few different mechansisms for previewing and analyzing the specs defined in a suite. You can use the [`outline`](#creating-an-outline-of-specs) cli command to get a machine-readable list of specs defined in the suite. Outline parses the Go AST tree of the suite to determine the specs and therefore does not require the suite to be compiled. This comes with a limitation, however: outline does not offer insight into which specs will run for a given set of filters and it cannot handle dynamically generated specs (example specs generated by a `for` loop).
3189+
3190+
For a more complete preview you can run `ginkgo --dry-run -v`. This compiles the spec, builds the spec tree, and then walks the tree printing out spec information using Ginkgo's default output as it goes. This allows you to see which specs will run for a given set of filters and also allows you to see dynamically generated specs. Note that you cannot use `--dry-run` with `-p` or `-procs`: you must run in series.
3191+
3192+
If, you need finer-grained control over previews you can use `PreviewSpecs` in your suite in lieu of `RunSpecs`. `PreviewSpecs` behaves like `--dry-run` in that it will compile the suite, build the spec tree, and then walk the tree while honoring any filter and randomization flags. However `PreviewSpecs` generates and returns a full [`Report` object](#reporting-nodes---reportbeforesuite-and-reportaftersuite) that can be manipulated and inspected as needed. Specs that will be run will have `State = SpecStatePassed` and specs that will be skipped will have `SpecStateSkipped`.
3193+
3194+
Currently you must run in series to invoke `PreviewSpecs` and you cannot run both `PreviewSpecs` and `RunSpecs` in the same suite. If you are opting into `PreviewSpecs` in lieu of `--dry-run` one suggested pattern is to key off of the `--dry-run` configuration to run `PreviewSpecs` instead of `RunSpecs`:
3195+
3196+
```go
3197+
func TestMySuite(t *testing.T) {
3198+
config, _ := GinkgoConfiguration()
3199+
if config.DryRun {
3200+
report := PreviewSpecs("My Suite", Label("suite-label"))
3201+
//...do things with report. e.g. reporters.GenerateJUnitReport(report, "./preview.xml")
3202+
} else {
3203+
RunSpecs(t, "My Suite", Label("suite-label"))
3204+
}
3205+
}
3206+
```
3207+
3208+
Note that since `RunSuite` accepts a description string and decorators that can influence the spec tree, you'll want to use the same arguments with `PreviewSpecs`.
3209+
31863210
### Running Multiple Suites
31873211

31883212
So far we've covered writing and running specs in individual suites. Of course, the `ginkgo` CLI also supports running multiple suites with a single invocation on the command line. We'll close out this chapter on running specs by covering how Ginkgo runs multiple suites.
@@ -5265,7 +5289,7 @@ The columns are:
52655289
52665290
You can set a different output format with the `-format` flag. Accepted formats are `csv`, `indent`, and `json`. The `ident` format is like `csv`, but uses indentation to show the nesting of containers and specs. Both the `csv` and `json` formats can be read by another program, e.g., an editor plugin that displays a tree view of Ginkgo tests in a file, or presents a menu for the user to quickly navigate to a container or spec.
52675291
5268-
`ginkgo outline` is intended for integration with third-party libraries and applications. If you simply want to know how a suite will run without running it try `ginkgo -v --dry-run` instead.
5292+
`ginkgo outline` is intended for integration with third-party libraries and applications - however it has an important limitation. Since parses the go syntax tree it cannot identify specs that are dynamically generated. Nor does it capture run-time concerns such as which specs will be skipped by a given set of filters or the order in which specs will run. If you want a quick overview of such things you can use `ginkgo -v --dry-run` instead. If you want finer-grained control over the suite preview, you should use [`PreviewSpecs`](#previewing-specs).
52695293
52705294
### Other Subcommands
52715295

dsl/core/core_dsl.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var GinkgoLabelFilter = ginkgo.GinkgoLabelFilter
3434
var PauseOutputInterception = ginkgo.PauseOutputInterception
3535
var ResumeOutputInterception = ginkgo.ResumeOutputInterception
3636
var RunSpecs = ginkgo.RunSpecs
37+
var PreviewSpecs = ginkgo.PreviewSpecs
3738
var Skip = ginkgo.Skip
3839
var Fail = ginkgo.Fail
3940
var AbortSuite = ginkgo.AbortSuite
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package preview_fixture_test
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"testing"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
func TestPreviewFixture(t *testing.T) {
13+
RegisterFailHandler(Fail)
14+
if os.Getenv("RUN") == "true" {
15+
RunSpecs(t, "PreviewFixture Suite", Label("suite-label"))
16+
}
17+
if os.Getenv("PREVIEW") == "true" {
18+
report := PreviewSpecs("PreviewFixture Suite", Label("suite-label"))
19+
for _, spec := range report.SpecReports {
20+
fmt.Println(spec.State, spec.FullText())
21+
}
22+
}
23+
}
24+
25+
var _ = Describe("specs", func() {
26+
It("A", Label("elephant"), func() {
27+
28+
})
29+
30+
It("B", Label("elephant"), func() {
31+
32+
})
33+
34+
It("C", func() {
35+
36+
})
37+
38+
It("D", func() {
39+
40+
})
41+
})

integration/preview_test.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package integration_test
2+
3+
import (
4+
"os"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
"github.com/onsi/gomega/gbytes"
9+
"github.com/onsi/gomega/gexec"
10+
)
11+
12+
var _ = Describe("Preview", func() {
13+
BeforeEach(func() {
14+
fm.MountFixture("preview")
15+
})
16+
17+
It("previews the specs, honoring the passed in flags", func() {
18+
os.Setenv("PREVIEW", "true")
19+
DeferCleanup(os.Unsetenv, "PREVIEW")
20+
session := startGinkgo(fm.PathTo("preview"), "--label-filter=elephant")
21+
Eventually(session).Should(gexec.Exit(0))
22+
Ω(session).Should(gbytes.Say("passed specs A"))
23+
Ω(session).Should(gbytes.Say("passed specs B"))
24+
Ω(session).Should(gbytes.Say("skipped specs C"))
25+
Ω(session).Should(gbytes.Say("skipped specs D"))
26+
})
27+
28+
It("fails if running in parallel", func() {
29+
os.Setenv("PREVIEW", "true")
30+
DeferCleanup(os.Unsetenv, "PREVIEW")
31+
session := startGinkgo(fm.PathTo("preview"), "--procs=2")
32+
Eventually(session).Should(gexec.Exit(1))
33+
Ω(session.Err).Should(gbytes.Say(`Ginkgo only supports PreviewSpecs\(\) in serial mode\.`))
34+
})
35+
36+
It("fails if you attempt to both run and preview specs", func() {
37+
os.Setenv("PREVIEW", "true")
38+
DeferCleanup(os.Unsetenv, "PREVIEW")
39+
os.Setenv("RUN", "true")
40+
DeferCleanup(os.Unsetenv, "RUN")
41+
session := startGinkgo(fm.PathTo("preview"))
42+
Eventually(session).Should(gexec.Exit(1))
43+
Ω(session).Should(gbytes.Say(`It looks like you are calling RunSpecs and PreviewSpecs in the same invocation`))
44+
})
45+
})

internal/suite.go

+10
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,16 @@ func (suite *Suite) CurrentSpecReport() types.SpecReport {
328328
return report
329329
}
330330

331+
// Only valid in the preview context. In general suite.report only includes
332+
// the specs run by _this_ node - it is only at the end of the suite that
333+
// the parallel reports are aggregated. However in the preview context we run
334+
// in series and
335+
func (suite *Suite) GetPreviewReport() types.Report {
336+
suite.selectiveLock.Lock()
337+
defer suite.selectiveLock.Unlock()
338+
return suite.report
339+
}
340+
331341
func (suite *Suite) AddReportEntry(entry ReportEntry) error {
332342
if suite.phase != PhaseRun {
333343
return types.GinkgoErrors.AddReportEntryNotDuringRunPhase(entry.Location)

types/errors.go

+15
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ func (g ginkgoErrors) RerunningSuite() error {
7070
}
7171
}
7272

73+
func (g ginkgoErrors) RunAndPreviewSuite() error {
74+
return GinkgoError{
75+
Heading: "Running and Previewing Suite",
76+
Message: formatter.F(`It looks like you are calling RunSpecs and PreviewSpecs in the same invocation of Ginkgo. Ginkgo does not currently support that. Please change your code to only call one or the other.`),
77+
DocLink: "previewing-specs",
78+
}
79+
}
80+
7381
/* Tree construction errors */
7482

7583
func (g ginkgoErrors) PushingNodeInRunPhase(nodeType NodeType, cl CodeLocation) error {
@@ -578,6 +586,13 @@ func (g ginkgoErrors) DryRunInParallelConfiguration() error {
578586
}
579587
}
580588

589+
func (g ginkgoErrors) PreviewInParallelConfiguration() error {
590+
return GinkgoError{
591+
Heading: "Ginkgo only supports PreviewSpecs() in serial mode.",
592+
Message: "Please try running ginkgo again, but without -p or -procs to ensure the suite is running in series.",
593+
}
594+
}
595+
581596
func (g ginkgoErrors) GracePeriodCannotBeZero() error {
582597
return GinkgoError{
583598
Heading: "Ginkgo requires a positive --grace-period.",

0 commit comments

Comments
 (0)