diff --git a/core_dsl.go b/core_dsl.go index a6a3a8dcf..2d7a70ecc 100644 --- a/core_dsl.go +++ b/core_dsl.go @@ -38,7 +38,7 @@ var flagSet types.GinkgoFlagSet var deprecationTracker = types.NewDeprecationTracker() var suiteConfig = types.NewDefaultSuiteConfig() var reporterConfig = types.NewDefaultReporterConfig() -var suiteDidRun, suiteDidPreview = false, false +var suiteDidRun = false var outputInterceptor internal.OutputInterceptor var client parallel_support.Client @@ -247,10 +247,12 @@ func RunSpecs(t GinkgoTestingT, description string, args ...interface{}) bool { if suiteDidRun { exitIfErr(types.GinkgoErrors.RerunningSuite()) } - if suiteDidPreview { - exitIfErr(types.GinkgoErrors.RunAndPreviewSuite()) - } suiteDidRun = true + err := global.PushClone() + if err != nil { + exitIfErr(err) + } + defer global.PopClone() suiteLabels := extractSuiteConfiguration(args) @@ -288,7 +290,7 @@ func RunSpecs(t GinkgoTestingT, description string, args ...interface{}) bool { registerReportAfterSuiteNodeForAutogeneratedReports(reporterConfig) } - err := global.Suite.BuildTree() + err = global.Suite.BuildTree() exitIfErr(err) suitePath, err := os.Getwd() exitIfErr(err) @@ -348,21 +350,24 @@ PreviewSpecs walks the testing tree and produces a report without actually invok See http://onsi.github.io/ginkgo/#previewing-specs for more information. */ func PreviewSpecs(description string, args ...any) Report { - if suiteDidRun { - exitIfErr(types.GinkgoErrors.RunAndPreviewSuite()) + err := global.PushClone() + if err != nil { + exitIfErr(err) } + defer global.PopClone() suiteLabels := extractSuiteConfiguration(args) - if suiteConfig.ParallelTotal != 1 { - exitIfErr(types.GinkgoErrors.PreviewInParallelConfiguration()) - } - suiteConfig.DryRun = true + priorDryRun, priorParallelTotal, priorParallelProcess := suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess + suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess = true, 1, 1 + defer func() { + suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess = priorDryRun, priorParallelTotal, priorParallelProcess + }() reporter := reporters.NoopReporter{} outputInterceptor = internal.NoopOutputInterceptor{} client = nil writer := GinkgoWriter.(*internal.Writer) - err := global.Suite.BuildTree() + err = global.Suite.BuildTree() exitIfErr(err) suitePath, err := os.Getwd() exitIfErr(err) diff --git a/docs/index.md b/docs/index.md index fd440a4a9..dd9983a37 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3191,7 +3191,7 @@ For a more complete preview you can run `ginkgo --dry-run -v`. This compiles th 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`. -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`: +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`: ```go func TestMySuite(t *testing.T) { diff --git a/integration/_fixtures/preview_fixture/preview_fixture_suite_test.go b/integration/_fixtures/preview_fixture/preview_fixture_suite_test.go index 5d50491ea..1dca17e90 100644 --- a/integration/_fixtures/preview_fixture/preview_fixture_suite_test.go +++ b/integration/_fixtures/preview_fixture/preview_fixture_suite_test.go @@ -11,15 +11,15 @@ import ( func TestPreviewFixture(t *testing.T) { RegisterFailHandler(Fail) - if os.Getenv("RUN") == "true" { - RunSpecs(t, "PreviewFixture Suite", Label("suite-label")) - } if os.Getenv("PREVIEW") == "true" { report := PreviewSpecs("PreviewFixture Suite", Label("suite-label")) for _, spec := range report.SpecReports { fmt.Println(spec.State, spec.FullText()) } } + if os.Getenv("RUN") == "true" { + RunSpecs(t, "PreviewFixture Suite", Label("suite-label")) + } } var _ = Describe("specs", func() { diff --git a/integration/preview_test.go b/integration/preview_test.go index b4d54a7d4..2c1ae1231 100644 --- a/integration/preview_test.go +++ b/integration/preview_test.go @@ -1,6 +1,7 @@ package integration_test import ( + "fmt" "os" . "github.com/onsi/ginkgo/v2" @@ -25,21 +26,28 @@ var _ = Describe("Preview", func() { Ω(session).Should(gbytes.Say("skipped specs D")) }) - It("fails if running in parallel", func() { + It("succeeds if you attempt to both run and preview specs", func() { os.Setenv("PREVIEW", "true") DeferCleanup(os.Unsetenv, "PREVIEW") - session := startGinkgo(fm.PathTo("preview"), "--procs=2") - Eventually(session).Should(gexec.Exit(1)) - Ω(session.Err).Should(gbytes.Say(`Ginkgo only supports PreviewSpecs\(\) in serial mode\.`)) + os.Setenv("RUN", "true") + DeferCleanup(os.Unsetenv, "RUN") + session := startGinkgo(fm.PathTo("preview")) + Eventually(session).Should(gexec.Exit(0)) + Ω(session).Should(gbytes.Say(`passed specs A`)) + Ω(session).Should(gbytes.Say(`passed specs B`)) + Ω(session).Should(gbytes.Say(`passed specs C`)) + Ω(session).Should(gbytes.Say(`passed specs D`)) + Ω(session).Should(gbytes.Say(`Ran 4 of 4 Specs`)) }) - It("fails if you attempt to both run and preview specs", func() { + It("works if you run in parallel", func() { os.Setenv("PREVIEW", "true") DeferCleanup(os.Unsetenv, "PREVIEW") os.Setenv("RUN", "true") DeferCleanup(os.Unsetenv, "RUN") - session := startGinkgo(fm.PathTo("preview")) - Eventually(session).Should(gexec.Exit(1)) - Ω(session).Should(gbytes.Say(`It looks like you are calling RunSpecs and PreviewSpecs in the same invocation`)) + session := startGinkgo(fm.PathTo("preview"), "-p") + Eventually(session).Should(gexec.Exit(0)) + fmt.Println(string(session.Out.Contents())) + Ω(session).Should(gbytes.Say(`Ran 4 of 4 Specs`)) }) }) diff --git a/internal/global/init.go b/internal/global/init.go index f2c0fd89c..464e3c97f 100644 --- a/internal/global/init.go +++ b/internal/global/init.go @@ -6,6 +6,7 @@ import ( var Suite *internal.Suite var Failer *internal.Failer +var backupSuite *internal.Suite func init() { InitializeGlobals() @@ -15,3 +16,13 @@ func InitializeGlobals() { Failer = internal.NewFailer() Suite = internal.NewSuite() } + +func PushClone() error { + var err error + backupSuite, err = Suite.Clone() + return err +} + +func PopClone() { + Suite = backupSuite +} diff --git a/internal/node.go b/internal/node.go index e108894da..16f0dc227 100644 --- a/internal/node.go +++ b/internal/node.go @@ -597,6 +597,12 @@ func (n Node) IsZero() bool { /* Nodes */ type Nodes []Node +func (n Nodes) Clone() Nodes { + nodes := make(Nodes, len(n)) + copy(nodes, n) + return nodes +} + func (n Nodes) CopyAppend(nodes ...Node) Nodes { numN := len(n) out := make(Nodes, numN+len(nodes)) diff --git a/internal/node_test.go b/internal/node_test.go index 9a4a99093..a26d16919 100644 --- a/internal/node_test.go +++ b/internal/node_test.go @@ -1181,6 +1181,22 @@ var _ = Describe("Node", func() { }) var _ = Describe("Nodes", func() { + Describe("Clone", func() { + var n1, n2, n3, n4 Node + + BeforeEach(func() { + n1, n2, n3, n4 = N(), N(), N(), N() + }) + + It("clones the slice", func() { + original := Nodes{n1, n2, n3} + clone := original.Clone() + Ω(original).Should(Equal(clone)) + clone[2] = n4 + Ω(original).Should(Equal(Nodes{n1, n2, n3})) + }) + }) + Describe("CopyAppend", func() { var n1, n2, n3, n4 Node diff --git a/internal/suite.go b/internal/suite.go index 9c6847710..fe6e8288a 100644 --- a/internal/suite.go +++ b/internal/suite.go @@ -77,6 +77,20 @@ func NewSuite() *Suite { } } +func (suite *Suite) Clone() (*Suite, error) { + if suite.phase != PhaseBuildTopLevel { + return nil, fmt.Errorf("cnanot clone suite after tree has been built") + } + return &Suite{ + tree: &TreeNode{}, + phase: PhaseBuildTopLevel, + ProgressReporterManager: NewProgressReporterManager(), + topLevelContainers: suite.topLevelContainers.Clone(), + suiteNodes: suite.suiteNodes.Clone(), + selectiveLock: &sync.Mutex{}, + }, nil +} + func (suite *Suite) BuildTree() error { // During PhaseBuildTopLevel, the top level containers are stored in suite.topLevelCotainers and entered // We now enter PhaseBuildTree where these top level containers are entered and added to the spec tree diff --git a/internal/suite_test.go b/internal/suite_test.go index f68145bf8..1e7194034 100644 --- a/internal/suite_test.go +++ b/internal/suite_test.go @@ -1,7 +1,6 @@ package internal_test import ( - "fmt" "io" . "github.com/onsi/ginkgo/v2" @@ -57,7 +56,6 @@ var _ = Describe("Suite", func() { }) It("only traverses top-level containers when told to BuildTree", func() { - fmt.Fprintln(GinkgoWriter, "HELLO!") Ω(rt).Should(HaveTrackedNothing()) Ω(suite.BuildTree()).Should(Succeed()) Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) @@ -72,6 +70,57 @@ var _ = Describe("Suite", func() { }) }) + Describe("cloning a suite", func() { + var err1, err2, err3 error + BeforeEach(func() { + suite.PushNode(N(types.NodeTypeBeforeSuite, "before suite", func() { + rt.Run("before-suite") + })) + err1 = suite.PushNode(N(ntCon, "a top-level container", func() { + rt.Run("traversing outer") + err2 = suite.PushNode(N(ntCon, "a nested container", func() { + rt.Run("traversing nested") + err3 = suite.PushNode(N(ntIt, "an it", rt.T("running it"))) + })) + })) + }) + + It("fails if the tree has already been built", func() { + Ω(suite.BuildTree()).Should(Succeed()) + _, err := suite.Clone() + Ω(err).Should(MatchError("cnanot clone suite after tree has been built")) + }) + + It("generates the same tree as the original", func() { + clone, err := suite.Clone() + Ω(err).ShouldNot(HaveOccurred()) + + Ω(suite.BuildTree()).Should(Succeed()) + Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) + rt.Reset() + suite.Run("suite", Labels{}, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + Ω(rt).Should(HaveTracked("before-suite", "running it")) + + Ω(err1).ShouldNot(HaveOccurred()) + Ω(err2).ShouldNot(HaveOccurred()) + Ω(err3).ShouldNot(HaveOccurred()) + + suite = clone // this is what the swapping of globals looks in reality + + rt.Reset() + Ω(clone.BuildTree()).Should(Succeed()) + Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) + rt.Reset() + clone.Run("suite", Labels{}, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + Ω(rt).Should(HaveTracked("before-suite", "running it")) + + Ω(err1).ShouldNot(HaveOccurred()) + Ω(err2).ShouldNot(HaveOccurred()) + Ω(err3).ShouldNot(HaveOccurred()) + }) + + }) + Describe("InRunPhase", func() { It("returns true when in the run phase and false when not in the run phase", func() { falsey := true diff --git a/types/errors.go b/types/errors.go index 1be2259fe..4fbdc3e9b 100644 --- a/types/errors.go +++ b/types/errors.go @@ -70,14 +70,6 @@ func (g ginkgoErrors) RerunningSuite() error { } } -func (g ginkgoErrors) RunAndPreviewSuite() error { - return GinkgoError{ - Heading: "Running and Previewing Suite", - 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.`), - DocLink: "previewing-specs", - } -} - /* Tree construction errors */ func (g ginkgoErrors) PushingNodeInRunPhase(nodeType NodeType, cl CodeLocation) error { @@ -586,13 +578,6 @@ func (g ginkgoErrors) DryRunInParallelConfiguration() error { } } -func (g ginkgoErrors) PreviewInParallelConfiguration() error { - return GinkgoError{ - Heading: "Ginkgo only supports PreviewSpecs() in serial mode.", - Message: "Please try running ginkgo again, but without -p or -procs to ensure the suite is running in series.", - } -} - func (g ginkgoErrors) GracePeriodCannotBeZero() error { return GinkgoError{ Heading: "Ginkgo requires a positive --grace-period.",