diff --git a/example.go b/example.go index 281605e5c..915e4f6b1 100644 --- a/example.go +++ b/example.go @@ -16,6 +16,7 @@ type example struct { failure types.ExampleFailure didInterceptFailure bool interceptedFailure failureData + exampleIndex int } func newExample(subject exampleSubject) *example { @@ -173,7 +174,7 @@ func (ex *example) processOutcomeAndFailure(containerIndex int, componentType ty return } -func (ex *example) summary() *types.ExampleSummary { +func (ex *example) summary(suiteID string) *types.ExampleSummary { componentTexts := make([]string, len(ex.containers)+1) componentCodeLocations := make([]types.CodeLocation, len(ex.containers)+1) @@ -194,6 +195,8 @@ func (ex *example) summary() *types.ExampleSummary { RunTime: ex.runTime, Failure: ex.failure, Measurements: ex.measurementsReport(), + SuiteID: suiteID, + ExampleIndex: ex.exampleIndex, } } diff --git a/example_collection.go b/example_collection.go index 7f1bb1be9..82beb5cbc 100644 --- a/example_collection.go +++ b/example_collection.go @@ -17,6 +17,7 @@ type exampleCollection struct { exampleCountBeforeParallelization int reporters []Reporter startTime time.Time + suiteID string runningExample *example config config.GinkgoConfigType } @@ -28,9 +29,12 @@ func newExampleCollection(t GinkgoTestingT, description string, examples []*exam examples: examples, reporters: reporters, config: config, + suiteID: types.GenerateRandomID(), exampleCountBeforeParallelization: len(examples), } + collection.enumerateAndAssignExampleIndices() + r := rand.New(rand.NewSource(config.RandomSeed)) if config.RandomizeAllSpecs { collection.shuffle(r) @@ -53,6 +57,12 @@ func newExampleCollection(t GinkgoTestingT, description string, examples []*exam return collection } +func (collection *exampleCollection) enumerateAndAssignExampleIndices() { + for index, example := range collection.examples { + example.exampleIndex = index + } +} + func (collection *exampleCollection) applyProgrammaticFocus() { hasFocusedTests := false for _, example := range collection.examples { @@ -166,14 +176,14 @@ func (collection *exampleCollection) reportSuiteWillBegin() { } func (collection *exampleCollection) reportExampleWillRun(example *example) { - summary := example.summary() + summary := example.summary(collection.suiteID) for _, reporter := range collection.reporters { reporter.ExampleWillRun(summary) } } func (collection *exampleCollection) reportExampleDidComplete(example *example) { - summary := example.summary() + summary := example.summary(collection.suiteID) for _, reporter := range collection.reporters { reporter.ExampleDidComplete(summary) } @@ -231,6 +241,7 @@ func (collection *exampleCollection) summary() *types.SuiteSummary { return &types.SuiteSummary{ SuiteDescription: collection.description, SuiteSucceeded: success, + SuiteID: collection.suiteID, NumberOfExamplesBeforeParallelization: collection.exampleCountBeforeParallelization, NumberOfTotalExamples: len(collection.examples), diff --git a/example_collection_test.go b/example_collection_test.go index e7905294b..cd823ae27 100644 --- a/example_collection_test.go +++ b/example_collection_test.go @@ -2,6 +2,7 @@ package ginkgo import ( "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" @@ -14,7 +15,7 @@ func init() { Describe("Example Collection", func() { var ( fakeT *fakeTestingT - fakeR *fakeReporter + fakeR *reporters.FakeReporter examplesThatWereRun []string @@ -35,10 +36,28 @@ func init() { BeforeEach(func() { fakeT = &fakeTestingT{} - fakeR = &fakeReporter{} + fakeR = reporters.NewFakeReporter() examplesThatWereRun = make([]string, 0) }) + Describe("enumerating and assigning example indices", func() { + var examples []*example + BeforeEach(func() { + examples = []*example{ + exampleWithItFunc("C", flagTypeNone, false), + exampleWithItFunc("A", flagTypeNone, false), + exampleWithItFunc("B", flagTypeNone, false), + } + collection = newExampleCollection(fakeT, "collection description", examples, []Reporter{fakeR}, config.GinkgoConfigType{}) + }) + + It("should enumerate and assign example indices", func() { + Ω(examples[0].summary("suite-id").ExampleIndex).Should(Equal(0)) + Ω(examples[1].summary("suite-id").ExampleIndex).Should(Equal(1)) + Ω(examples[2].summary("suite-id").ExampleIndex).Should(Equal(2)) + }) + }) + Describe("shuffling the collection", func() { BeforeEach(func() { collection = newExampleCollection(fakeT, "collection description", []*example{ @@ -62,9 +81,9 @@ func init() { }) Describe("reporting to multiple reporter", func() { - var otherFakeR *fakeReporter + var otherFakeR *reporters.FakeReporter BeforeEach(func() { - otherFakeR = &fakeReporter{} + otherFakeR = reporters.NewFakeReporter() collection = newExampleCollection(fakeT, "collection description", []*example{ exampleWithItFunc("C", flagTypeNone, false), @@ -75,9 +94,9 @@ func init() { }) It("reports to both reporters", func() { - Ω(otherFakeR.beginSummary).Should(Equal(fakeR.beginSummary)) - Ω(otherFakeR.endSummary).Should(Equal(fakeR.endSummary)) - Ω(otherFakeR.exampleSummaries).Should(Equal(fakeR.exampleSummaries)) + Ω(otherFakeR.BeginSummary).Should(Equal(fakeR.BeginSummary)) + Ω(otherFakeR.EndSummary).Should(Equal(fakeR.EndSummary)) + Ω(otherFakeR.ExampleSummaries).Should(Equal(fakeR.ExampleSummaries)) }) }) @@ -117,7 +136,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -131,15 +150,15 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleWillRunSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleWillRunSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -151,6 +170,15 @@ func init() { Ω(summary.NumberOfFailedExamples).Should(Equal(0)) Ω(summary.RunTime.Seconds()).Should(BeNumerically("~", 3*0.001, 0.01)) }) + + It("should publish a consistent suite ID across all summaries", func() { + suiteId := fakeR.BeginSummary.SuiteID + Ω(suiteId).ShouldNot(BeEmpty()) + Ω(fakeR.EndSummary.SuiteID).Should(Equal(suiteId)) + for _, exampleSummary := range fakeR.ExampleSummaries { + Ω(exampleSummary.SuiteID).Should(Equal(suiteId)) + } + }) }) Context("when examples fail", func() { @@ -172,7 +200,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -186,14 +214,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeFalse()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -221,7 +249,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -235,14 +263,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -262,7 +290,7 @@ func init() { It("should mark the suite as failed", func() { Ω(fakeT.didFail).Should(BeTrue()) - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteSucceeded).Should(BeFalse()) }) }) @@ -283,7 +311,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -297,14 +325,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -331,7 +359,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -345,14 +373,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -379,7 +407,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -393,14 +421,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -428,7 +456,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -442,14 +470,14 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(3)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example1.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[2]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(3)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example1.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[2]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -474,7 +502,7 @@ func init() { }) It("publishes the correct starting suite summary", func() { - summary := fakeR.beginSummary + summary := fakeR.BeginSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) @@ -488,13 +516,13 @@ func init() { }) It("publishes the correct example summaries", func() { - Ω(fakeR.exampleSummaries).Should(HaveLen(2)) - Ω(fakeR.exampleSummaries[0]).Should(Equal(example2.summary())) - Ω(fakeR.exampleSummaries[1]).Should(Equal(example3.summary())) + Ω(fakeR.ExampleSummaries).Should(HaveLen(2)) + Ω(fakeR.ExampleSummaries[0]).Should(Equal(example2.summary(fakeR.BeginSummary.SuiteID))) + Ω(fakeR.ExampleSummaries[1]).Should(Equal(example3.summary(fakeR.BeginSummary.SuiteID))) }) It("publishes the correct ending suite summary", func() { - summary := fakeR.endSummary + summary := fakeR.EndSummary Ω(summary.SuiteDescription).Should(Equal("collection description")) Ω(summary.SuiteSucceeded).Should(BeTrue()) Ω(summary.NumberOfExamplesBeforeParallelization).Should(Equal(3)) diff --git a/example_test.go b/example_test.go index 1cfc3dbd5..1561db068 100644 --- a/example_test.go +++ b/example_test.go @@ -215,7 +215,7 @@ func init() { Describe("the summary", func() { It("has the texts and code locations for the container nodes and the it node", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.ComponentTexts).Should(Equal([]string{ "outer", "inner", "it", })) @@ -223,12 +223,25 @@ func init() { outerContainer.codeLocation, innerContainer.codeLocation, it.codeLocation, })) }) + + It("should have the passed in SuiteID", func() { + ex.run() + summary := ex.summary("suite-id") + Ω(summary.SuiteID).Should(Equal("suite-id")) + }) + + It("should include the example's index", func() { + ex.exampleIndex = 17 + ex.run() + summary := ex.summary("suite-id") + Ω(summary.ExampleIndex).Should(Equal(17)) + }) }) Context("when none of the runnable nodes fail", func() { It("has a summary reporting no failure", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStatePassed)) Ω(summary.RunTime.Seconds()).Should(BeNumerically(">", 0.01)) Ω(summary.IsMeasurement).Should(BeFalse()) @@ -265,7 +278,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStateFailed)) Ω(summary.Failure.Message).Should(Equal(failure.message)) @@ -293,7 +306,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStatePanicked)) Ω(summary.Failure.Message).Should(Equal("Test Panicked")) @@ -320,7 +333,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStateTimedOut)) Ω(summary.Failure.Message).Should(Equal("Timed out")) @@ -359,7 +372,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStateFailed)) Ω(summary.Failure.Message).Should(Equal(failure.message)) @@ -385,7 +398,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStatePanicked)) Ω(summary.Failure.Message).Should(Equal("Test Panicked")) @@ -410,7 +423,7 @@ func init() { It("has a summary with the correct failure report", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(summary.State).Should(Equal(types.ExampleStateTimedOut)) Ω(summary.Failure.Message).Should(Equal("Timed out")) @@ -453,7 +466,7 @@ func init() { It("runs the measurement samples number of times and returns statistics", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(runs).Should(Equal(5)) @@ -479,7 +492,7 @@ func init() { It("marks the measurement as failed and doesn't run any more samples", func() { ex.run() - summary := ex.summary() + summary := ex.summary("suite-id") Ω(runs).Should(Equal(3)) @@ -696,7 +709,7 @@ func init() { }) It("should not override the failure data of the earliest failure", func() { - Ω(ex.summary().Failure.Message).Should(Equal("INNER_JUST_BEFORE_A failed")) + Ω(ex.summary("suite-id").Failure.Message).Should(Equal("INNER_JUST_BEFORE_A failed")) }) }) }) diff --git a/ginkgo.go b/ginkgo.go index 7cbcb16b6..ed657c7b9 100644 --- a/ginkgo.go +++ b/ginkgo.go @@ -13,8 +13,12 @@ package ginkgo import ( "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/remote" "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/stenographer" "github.com/onsi/ginkgo/types" + "net/http" + "os" "time" ) @@ -38,12 +42,7 @@ func init() { //The custom reporter is passed in a SuiteSummary when the suite begins and ends, //and an ExmapleSummary just before an example (spec) begins //and just after an example (spec) ends -type Reporter interface { - SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) - ExampleWillRun(exampleSummary *types.ExampleSummary) - ExampleDidComplete(exampleSummary *types.ExampleSummary) - SpecSuiteDidEnd(summary *types.SuiteSummary) -} +type Reporter reporters.Reporter //Asynchronous specs given a channel of the Done type. You must close (or send to) the channel //to tell Ginkgo that your async test is done. @@ -69,13 +68,14 @@ type Benchmarker interface { // // ginkgo bootstrap func RunSpecs(t GinkgoTestingT, description string) bool { - return globalSuite.run(t, description, []Reporter{reporters.NewDefaultReporter(config.DefaultReporterConfig)}, config.GinkgoConfig) + specReporters := []Reporter{buildDefaultReporter()} + return globalSuite.run(t, description, specReporters, config.GinkgoConfig) } //To run your tests with Ginkgo's default reporter and your custom reporter(s), replace //RunSpecs() with this method. func RunSpecsWithDefaultAndCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool { - specReporters = append([]Reporter{reporters.NewDefaultReporter(config.DefaultReporterConfig)}, specReporters...) + specReporters = append([]Reporter{buildDefaultReporter()}, specReporters...) return globalSuite.run(t, description, specReporters, config.GinkgoConfig) } @@ -85,6 +85,16 @@ func RunSpecsWithCustomReporters(t GinkgoTestingT, description string, specRepor return globalSuite.run(t, description, specReporters, config.GinkgoConfig) } +func buildDefaultReporter() Reporter { + remoteReportingServer := os.Getenv("GINKGO_REMOTE_REPORTING_SERVER") + if remoteReportingServer == "" { + stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor) + return reporters.NewDefaultReporter(config.DefaultReporterConfig, stenographer) + } else { + return remote.NewForwardingReporter(remoteReportingServer, &http.Client{}, remote.NewOutputInterceptor()) + } +} + //Fail notifies Ginkgo that the current spec has failed. (Gomega will call Fail for you automatically when an assertion fails.) func Fail(message string, callerSkip ...int) { skip := 0 diff --git a/ginkgo/aggregator/aggregator.go b/ginkgo/aggregator/aggregator.go new file mode 100644 index 000000000..adb93e6e0 --- /dev/null +++ b/ginkgo/aggregator/aggregator.go @@ -0,0 +1,200 @@ +/* + +Aggregator is a reporter used by the Ginkgo CLI to aggregate and present parallel test output +as one coherent stream. You shouldn't need to use this in your code. To run tests in parallel: + + ginkgo -nodes=N + +where N is the number of nodes you desire. + +To disable streaming mode and, instead, have the test output blobbed onto screen when all the parallel nodes complete: + + ginkgo -nodes=N -stream=false + +*/ +package aggregator + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/stenographer" + "github.com/onsi/ginkgo/types" + "time" +) + +type configAndSuite struct { + config config.GinkgoConfigType + summary *types.SuiteSummary +} + +type Aggregator struct { + nodeCount int + config config.DefaultReporterConfigType + stenographer stenographer.Stenographer + result chan bool + + suiteBeginnings chan configAndSuite + aggregatedSuiteBeginnings []configAndSuite + + exampleCompletions chan *types.ExampleSummary + completedExamples []*types.ExampleSummary + + suiteEndings chan *types.SuiteSummary + aggregatedSuiteEndings []*types.SuiteSummary + + startTime time.Time +} + +func NewAggregator(nodeCount int, result chan bool, config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *Aggregator { + aggregator := &Aggregator{ + nodeCount: nodeCount, + result: result, + config: config, + stenographer: stenographer, + + suiteBeginnings: make(chan configAndSuite, 0), + aggregatedSuiteBeginnings: []configAndSuite{}, + + exampleCompletions: make(chan *types.ExampleSummary, 0), + completedExamples: []*types.ExampleSummary{}, + + suiteEndings: make(chan *types.SuiteSummary, 0), + aggregatedSuiteEndings: []*types.SuiteSummary{}, + } + + go aggregator.mux() + + return aggregator +} + +func (aggregator *Aggregator) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + aggregator.suiteBeginnings <- configAndSuite{config, summary} +} + +func (aggregator *Aggregator) ExampleWillRun(exampleSummary *types.ExampleSummary) { + //noop +} + +func (aggregator *Aggregator) ExampleDidComplete(exampleSummary *types.ExampleSummary) { + aggregator.exampleCompletions <- exampleSummary +} + +func (aggregator *Aggregator) SpecSuiteDidEnd(summary *types.SuiteSummary) { + aggregator.suiteEndings <- summary +} + +func (aggregator *Aggregator) mux() { +loop: + for { + select { + case configAndSuite := <-aggregator.suiteBeginnings: + aggregator.registerSuiteBeginning(configAndSuite) + case exampleSummary := <-aggregator.exampleCompletions: + aggregator.registerExampleCompletion(exampleSummary) + case suite := <-aggregator.suiteEndings: + finished, passed := aggregator.registerSuiteEnding(suite) + if finished { + aggregator.result <- passed + break loop + } + } + } +} + +func (aggregator *Aggregator) registerSuiteBeginning(configAndSuite configAndSuite) { + aggregator.aggregatedSuiteBeginnings = append(aggregator.aggregatedSuiteBeginnings, configAndSuite) + + if len(aggregator.aggregatedSuiteBeginnings) == 1 { + aggregator.startTime = time.Now() + } + + if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { + return + } + + aggregator.stenographer.AnnounceSuite(configAndSuite.summary.SuiteDescription, configAndSuite.config.RandomSeed, configAndSuite.config.RandomizeAllSpecs) + + numberOfSpecsToRun := 0 + totalNumberOfSpecs := 0 + for _, configAndSuite := range aggregator.aggregatedSuiteBeginnings { + numberOfSpecsToRun += configAndSuite.summary.NumberOfExamplesThatWillBeRun + totalNumberOfSpecs += configAndSuite.summary.NumberOfTotalExamples + } + + aggregator.stenographer.AnnounceNumberOfSpecs(numberOfSpecsToRun, totalNumberOfSpecs) + aggregator.stenographer.AnnounceAggregatedParallelRun(aggregator.nodeCount) + aggregator.flushCompletedExamples() +} + +func (aggregator *Aggregator) registerExampleCompletion(exampleSummary *types.ExampleSummary) { + aggregator.completedExamples = append(aggregator.completedExamples, exampleSummary) + aggregator.flushCompletedExamples() +} + +func (aggregator *Aggregator) flushCompletedExamples() { + if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount { + return + } + + for _, exampleSummary := range aggregator.completedExamples { + aggregator.announceExample(exampleSummary) + } + + aggregator.completedExamples = []*types.ExampleSummary{} +} + +func (aggregator *Aggregator) announceExample(exampleSummary *types.ExampleSummary) { + if aggregator.config.Verbose && exampleSummary.State != types.ExampleStatePending && exampleSummary.State != types.ExampleStateSkipped { + aggregator.stenographer.AnnounceExampleWillRun(exampleSummary) + } + + aggregator.stenographer.AnnounceCapturedOutput(exampleSummary) + + switch exampleSummary.State { + case types.ExampleStatePassed: + if exampleSummary.IsMeasurement { + aggregator.stenographer.AnnounceSuccesfulMeasurement(exampleSummary, aggregator.config.Succinct) + } else if exampleSummary.RunTime.Seconds() >= aggregator.config.SlowSpecThreshold { + aggregator.stenographer.AnnounceSuccesfulSlowExample(exampleSummary, aggregator.config.Succinct) + } else { + aggregator.stenographer.AnnounceSuccesfulExample(exampleSummary) + } + case types.ExampleStatePending: + aggregator.stenographer.AnnouncePendingExample(exampleSummary, aggregator.config.NoisyPendings, aggregator.config.Succinct) + case types.ExampleStateSkipped: + aggregator.stenographer.AnnounceSkippedExample(exampleSummary) + case types.ExampleStateTimedOut: + aggregator.stenographer.AnnounceExampleTimedOut(exampleSummary, aggregator.config.Succinct) + case types.ExampleStatePanicked: + aggregator.stenographer.AnnounceExamplePanicked(exampleSummary, aggregator.config.Succinct) + case types.ExampleStateFailed: + aggregator.stenographer.AnnounceExampleFailed(exampleSummary, aggregator.config.Succinct) + } +} + +func (aggregator *Aggregator) registerSuiteEnding(suite *types.SuiteSummary) (finished bool, passed bool) { + aggregator.aggregatedSuiteEndings = append(aggregator.aggregatedSuiteEndings, suite) + if len(aggregator.aggregatedSuiteEndings) < aggregator.nodeCount { + return false, false + } + + aggregatedSuiteSummary := &types.SuiteSummary{} + aggregatedSuiteSummary.SuiteSucceeded = true + + for _, suiteSummary := range aggregator.aggregatedSuiteEndings { + if suiteSummary.SuiteSucceeded == false { + aggregatedSuiteSummary.SuiteSucceeded = false + } + + aggregatedSuiteSummary.NumberOfExamplesThatWillBeRun += suiteSummary.NumberOfExamplesThatWillBeRun + aggregatedSuiteSummary.NumberOfTotalExamples += suiteSummary.NumberOfTotalExamples + aggregatedSuiteSummary.NumberOfPassedExamples += suiteSummary.NumberOfPassedExamples + aggregatedSuiteSummary.NumberOfFailedExamples += suiteSummary.NumberOfFailedExamples + aggregatedSuiteSummary.NumberOfPendingExamples += suiteSummary.NumberOfPendingExamples + aggregatedSuiteSummary.NumberOfSkippedExamples += suiteSummary.NumberOfSkippedExamples + } + + aggregatedSuiteSummary.RunTime = time.Since(aggregator.startTime) + aggregator.stenographer.AnnounceSpecRunCompletion(aggregatedSuiteSummary) + + return true, aggregatedSuiteSummary.SuiteSucceeded +} diff --git a/ginkgo/aggregator/aggregator_suite_test.go b/ginkgo/aggregator/aggregator_suite_test.go new file mode 100644 index 000000000..a54644e64 --- /dev/null +++ b/ginkgo/aggregator/aggregator_suite_test.go @@ -0,0 +1,13 @@ +package aggregator_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestGinkgoAggregator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Ginkgo Aggregator Suite") +} diff --git a/ginkgo/aggregator/aggregator_test.go b/ginkgo/aggregator/aggregator_test.go new file mode 100644 index 000000000..654d55cb1 --- /dev/null +++ b/ginkgo/aggregator/aggregator_test.go @@ -0,0 +1,288 @@ +package aggregator_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/ginkgo/aggregator" + st "github.com/onsi/ginkgo/stenographer" + "github.com/onsi/ginkgo/types" + "runtime" + "time" +) + +var _ = Describe("Aggregator", func() { + var ( + aggregator *Aggregator + reporterConfig config.DefaultReporterConfigType + stenographer *st.FakeStenographer + result chan bool + + ginkgoConfig1 config.GinkgoConfigType + ginkgoConfig2 config.GinkgoConfigType + + suiteSummary1 *types.SuiteSummary + suiteSummary2 *types.SuiteSummary + + exampleSummary *types.ExampleSummary + + suiteDescription string + ) + + BeforeEach(func() { + reporterConfig = config.DefaultReporterConfigType{ + NoColor: false, + SlowSpecThreshold: 0.1, + NoisyPendings: true, + Succinct: false, + Verbose: true, + } + stenographer = st.NewFakeStenographer() + result = make(chan bool, 1) + aggregator = NewAggregator(2, result, reporterConfig, stenographer) + + // + // now set up some fixture data + // + + ginkgoConfig1 = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + ParallelNode: 1, + ParallelTotal: 2, + } + + ginkgoConfig2 = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + ParallelNode: 2, + ParallelTotal: 2, + } + + suiteDescription = "My Parallel Suite" + + suiteSummary1 = &types.SuiteSummary{ + SuiteDescription: suiteDescription, + + NumberOfExamplesBeforeParallelization: 30, + NumberOfTotalExamples: 17, + NumberOfExamplesThatWillBeRun: 15, + NumberOfPendingExamples: 1, + NumberOfSkippedExamples: 1, + } + + suiteSummary2 = &types.SuiteSummary{ + SuiteDescription: suiteDescription, + + NumberOfExamplesBeforeParallelization: 30, + NumberOfTotalExamples: 13, + NumberOfExamplesThatWillBeRun: 8, + NumberOfPendingExamples: 2, + NumberOfSkippedExamples: 3, + } + + exampleSummary = &types.ExampleSummary{ + State: types.ExampleStatePassed, + } + }) + + call := func(method string, args ...interface{}) st.FakeStenographerCall { + return st.NewFakeStenographerCall(method, args...) + } + + beginSuite := func() { + stenographer.Reset() + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) + Eventually(func() interface{} { + return len(stenographer.Calls) + }).Should(BeNumerically(">=", 3)) + } + + Describe("Announcing the beginning of the suite", func() { + Context("When one of the parallel-suites starts", func() { + BeforeEach(func() { + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + runtime.Gosched() + }) + + It("should be silent", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + }) + + Context("once all of the parallel-suites have started", func() { + BeforeEach(func() { + aggregator.SpecSuiteWillBegin(ginkgoConfig2, suiteSummary2) + aggregator.SpecSuiteWillBegin(ginkgoConfig1, suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls + }).Should(HaveLen(3)) + }) + + It("should announce the beginning of the suite", func() { + Ω(stenographer.Calls).Should(HaveLen(3)) + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuite", suiteDescription, ginkgoConfig1.RandomSeed, true))) + Ω(stenographer.Calls[1]).Should(Equal(call("AnnounceNumberOfSpecs", 23, 30))) + Ω(stenographer.Calls[2]).Should(Equal(call("AnnounceAggregatedParallelRun", 2))) + }) + }) + }) + + Describe("Announcing examples", func() { + Context("when the parallel-suites have not all started", func() { + BeforeEach(func() { + aggregator.ExampleDidComplete(exampleSummary) + runtime.Gosched() + }) + + It("should not announce any examples", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + + Context("when the parallel-suites subsequently start", func() { + BeforeEach(func() { + beginSuite() + }) + + It("should announce the examples", func() { + Eventually(func() interface{} { + lastCall := stenographer.Calls[len(stenographer.Calls)-1] + return lastCall + }).Should(Equal(call("AnnounceSuccesfulExample", exampleSummary))) + }) + }) + }) + + Context("When the parallel-suites have all started", func() { + BeforeEach(func() { + beginSuite() + stenographer.Reset() + }) + + Context("When an example completes", func() { + BeforeEach(func() { + aggregator.ExampleDidComplete(exampleSummary) + Eventually(func() interface{} { + return stenographer.Calls + }).Should(HaveLen(3)) + }) + + It("should announce that the example will run (when in verbose mode)", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceExampleWillRun", exampleSummary))) + }) + + It("should announce the captured stdout of the example", func() { + Ω(stenographer.Calls[1]).Should(Equal(call("AnnounceCapturedOutput", exampleSummary))) + }) + + It("should announce completion", func() { + Ω(stenographer.Calls[2]).Should(Equal(call("AnnounceSuccesfulExample", exampleSummary))) + }) + }) + }) + }) + + Describe("Announcing the end of the suite", func() { + BeforeEach(func() { + beginSuite() + stenographer.Reset() + }) + + Context("When one of the parallel-suites ends", func() { + BeforeEach(func() { + aggregator.SpecSuiteDidEnd(suiteSummary2) + runtime.Gosched() + }) + + It("should be silent", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + + It("should not notify the channel", func() { + Ω(result).Should(BeEmpty()) + }) + }) + + Context("once all of the parallel-suites end", func() { + BeforeEach(func() { + time.Sleep(200 * time.Millisecond) + + suiteSummary1.SuiteSucceeded = true + suiteSummary1.NumberOfPassedExamples = 15 + suiteSummary1.NumberOfFailedExamples = 0 + suiteSummary2.SuiteSucceeded = false + suiteSummary2.NumberOfPassedExamples = 5 + suiteSummary2.NumberOfFailedExamples = 3 + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls + }).Should(HaveLen(1)) + }) + + It("should announce the end of the suite", func() { + compositeSummary := stenographer.Calls[0].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) + Ω(compositeSummary.NumberOfExamplesThatWillBeRun).Should(Equal(23)) + Ω(compositeSummary.NumberOfTotalExamples).Should(Equal(30)) + Ω(compositeSummary.NumberOfPassedExamples).Should(Equal(20)) + Ω(compositeSummary.NumberOfFailedExamples).Should(Equal(3)) + Ω(compositeSummary.NumberOfPendingExamples).Should(Equal(3)) + Ω(compositeSummary.NumberOfSkippedExamples).Should(Equal(4)) + Ω(compositeSummary.RunTime.Seconds()).Should(BeNumerically(">", 0.2)) + }) + }) + + Context("when all the parallel-suites pass", func() { + BeforeEach(func() { + suiteSummary1.SuiteSucceeded = true + suiteSummary2.SuiteSucceeded = true + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls + }).Should(HaveLen(1)) + }) + + It("should report success", func() { + compositeSummary := stenographer.Calls[0].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeTrue()) + }) + + It("should notify the channel that it succeded", func(done Done) { + Ω(<-result).Should(BeTrue()) + close(done) + }) + }) + + Context("when one of the parallel-suites fails", func() { + BeforeEach(func() { + suiteSummary1.SuiteSucceeded = true + suiteSummary2.SuiteSucceeded = false + + aggregator.SpecSuiteDidEnd(suiteSummary2) + aggregator.SpecSuiteDidEnd(suiteSummary1) + Eventually(func() interface{} { + return stenographer.Calls + }).Should(HaveLen(1)) + }) + + It("should report failure", func() { + compositeSummary := stenographer.Calls[0].Args[0].(*types.SuiteSummary) + + Ω(compositeSummary.SuiteSucceeded).Should(BeFalse()) + }) + + It("should notify the channel that it failed", func(done Done) { + Ω(<-result).Should(BeFalse()) + close(done) + }) + }) + }) +}) diff --git a/ginkgo/main.go b/ginkgo/main.go index 5c459ae60..7918f7fde 100644 --- a/ginkgo/main.go +++ b/ginkgo/main.go @@ -19,6 +19,16 @@ To run tests in particular packages: ginkgo /path/to/package /path/to/another/package +To run tests in parallel + + ginkgo -nodes=N + +where N is the number of nodes. By default the Ginkgo CLI will spin up a server that the individual +test processes stream test output to. The CLI then aggregates these streams into one coherent stream of output. +An alternative is to have the parallel nodes run and then present the resulting, final, output in one monolithic chunk - you can opt into this if streaming is giving you trouble: + + ginkgo -nodes=N -stream=false + To bootstrap a test suite: ginkgo bootstrap @@ -50,6 +60,7 @@ import ( ) var numCPU int +var parallelStream bool var recurse bool var runMagicI bool var race bool @@ -59,6 +70,7 @@ func init() { config.Flags("", false) flag.IntVar(&(numCPU), "nodes", 1, "The number of parallel test nodes to run") + flag.BoolVar(&(parallelStream), "stream", true, "Aggregate parallel test output into one coherent stream (default: true)") flag.BoolVar(&(recurse), "r", false, "Find and run test suites under the current directory recursively") flag.BoolVar(&(runMagicI), "i", false, "Run go test -i first, then run the test suite") flag.BoolVar(&(race), "race", false, "Run tests with race detection enabled") @@ -132,7 +144,7 @@ func runTests() { os.Exit(1) } - runner := newTestRunner(numCPU, runMagicI, race, cover) + runner := newTestRunner(numCPU, parallelStream, runMagicI, race, cover) passed := runner.run(suites) fmt.Printf("\nGinkgo ran in %s\n", time.Since(t)) diff --git a/ginkgo/test_runner.go b/ginkgo/test_runner.go index 286a704f8..7c9c14891 100644 --- a/ginkgo/test_runner.go +++ b/ginkgo/test_runner.go @@ -4,14 +4,19 @@ import ( "bytes" "fmt" "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/ginkgo/aggregator" + "github.com/onsi/ginkgo/remote" + "github.com/onsi/ginkgo/stenographer" "io" "os" "os/exec" "os/signal" + "time" ) type testRunner struct { numCPU int + parallelStream bool runMagicI bool race bool cover bool @@ -19,9 +24,10 @@ type testRunner struct { reports []*bytes.Buffer } -func newTestRunner(numCPU int, runMagicI bool, race bool, cover bool) *testRunner { +func newTestRunner(numCPU int, parallelStream bool, runMagicI bool, race bool, cover bool) *testRunner { return &testRunner{ numCPU: numCPU, + parallelStream: parallelStream, runMagicI: runMagicI, race: race, cover: cover, @@ -49,7 +55,11 @@ func (t *testRunner) runSuite(suite testSuite) bool { if suite.isGinkgo { if t.numCPU > 1 { - return t.runParallelGinkgoSuite(suite) + if t.parallelStream { + return t.runAndStreamParallelGinkgoSuite(suite) + } else { + return t.runParallelGinkgoSuite(suite) + } } else { return t.runSerialGinkgoSuite(suite) } @@ -84,7 +94,7 @@ func (t *testRunner) runParallelGinkgoSuite(suite testSuite) bool { buffer := new(bytes.Buffer) t.reports = append(t.reports, buffer) - go t.runCommand(suite.path, args, buffer, completions) + go t.runCommand(suite.path, args, nil, buffer, completions) } passed := true @@ -101,15 +111,88 @@ func (t *testRunner) runParallelGinkgoSuite(suite testSuite) bool { return passed } +func (t *testRunner) runAndStreamParallelGinkgoSuite(suite testSuite) bool { + result := make(chan bool, 0) + stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor) + aggregator := aggregator.NewAggregator(t.numCPU, result, config.DefaultReporterConfig, stenographer) + + server, err := remote.NewServer() + if err != nil { + panic("Failed to start parallel spec server") + } + + server.RegisterReporters(aggregator) + server.Start() + + serverAddress := server.Address() + + completions := make(chan bool) + + for cpu := 0; cpu < t.numCPU; cpu++ { + config.GinkgoConfig.ParallelNode = cpu + 1 + config.GinkgoConfig.ParallelTotal = t.numCPU + + args := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) + args = append(args, t.commonArgs(suite)...) + + env := os.Environ() + env = append(env, fmt.Sprintf("GINKGO_REMOTE_REPORTING_SERVER=%s", serverAddress)) + + buffer := new(bytes.Buffer) + t.reports = append(t.reports, buffer) + + go t.runCommand(suite.path, args, env, buffer, completions) + } + + for cpu := 0; cpu < t.numCPU; cpu++ { + <-completions + } + + //all test processes are done, at this point + //we should be able to wait for the aggregator to tell us that it's done + + var passed = false + select { + case passed = <-result: + //the aggregator is done and can tell us whether or not the suite passed + case <-time.After(time.Second): + //the aggregator never got back to us! something must have gone wrong + fmt.Println("") + fmt.Println("") + fmt.Println(" ---------------------------------------------------------- ") + fmt.Println(" | |") + fmt.Println(" | Ginkgo timed out waiting for all parallel nodes to end! |") + fmt.Println(" | Here is some salvaged output: |") + fmt.Println(" | |") + fmt.Println(" ---------------------------------------------------------- ") + fmt.Println("") + fmt.Println("") + + os.Stdout.Sync() + + time.Sleep(time.Second) + + for _, report := range t.reports { + fmt.Print(report.String()) + } + + os.Stdout.Sync() + } + + server.Stop() + + return passed +} + func (t *testRunner) runSerialGinkgoSuite(suite testSuite) bool { args := config.BuildFlagArgs("ginkgo", config.GinkgoConfig, config.DefaultReporterConfig) args = append(args, t.commonArgs(suite)...) - return t.runCommand(suite.path, args, os.Stdout, nil) + return t.runCommand(suite.path, args, nil, os.Stdout, nil) } func (t *testRunner) runGoTestSuite(suite testSuite) bool { args := t.commonArgs(suite) - return t.runCommand(suite.path, args, os.Stdout, nil) + return t.runCommand(suite.path, args, nil, os.Stdout, nil) } func (t *testRunner) commonArgs(suite testSuite) []string { @@ -123,10 +206,11 @@ func (t *testRunner) commonArgs(suite testSuite) []string { return args } -func (t *testRunner) runCommand(path string, args []string, stream io.Writer, completions chan bool) bool { +func (t *testRunner) runCommand(path string, args []string, env []string, stream io.Writer, completions chan bool) bool { args = append([]string{"test", "-v", "-timeout=24h", path}, args...) cmd := exec.Command("go", args...) + cmd.Env = env t.executedCommands = append(t.executedCommands, cmd) doneStreaming := make(chan bool, 2) diff --git a/ginkgo_suite_test.go b/ginkgo_suite_test.go index 51aa266e0..cfc1fc8bf 100644 --- a/ginkgo_suite_test.go +++ b/ginkgo_suite_test.go @@ -1,8 +1,6 @@ package ginkgo import ( - "github.com/onsi/ginkgo/config" - "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" "math/rand" @@ -36,29 +34,3 @@ type fakeTestingT struct { func (fakeT *fakeTestingT) Fail() { fakeT.didFail = true } - -type fakeReporter struct { - config config.GinkgoConfigType - - beginSummary *types.SuiteSummary - exampleWillRunSummaries []*types.ExampleSummary - exampleSummaries []*types.ExampleSummary - endSummary *types.SuiteSummary -} - -func (fakeR *fakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { - fakeR.config = config - fakeR.beginSummary = summary -} - -func (fakeR *fakeReporter) ExampleWillRun(exampleSummary *types.ExampleSummary) { - fakeR.exampleWillRunSummaries = append(fakeR.exampleWillRunSummaries, exampleSummary) -} - -func (fakeR *fakeReporter) ExampleDidComplete(exampleSummary *types.ExampleSummary) { - fakeR.exampleSummaries = append(fakeR.exampleSummaries, exampleSummary) -} - -func (fakeR *fakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { - fakeR.endSummary = summary -} diff --git a/remote/fake_output_interceptor_test.go b/remote/fake_output_interceptor_test.go new file mode 100644 index 000000000..a928f93d3 --- /dev/null +++ b/remote/fake_output_interceptor_test.go @@ -0,0 +1,17 @@ +package remote_test + +type fakeOutputInterceptor struct { + DidStartInterceptingOutput bool + DidStopInterceptingOutput bool + InterceptedOutput string +} + +func (interceptor *fakeOutputInterceptor) StartInterceptingOutput() error { + interceptor.DidStartInterceptingOutput = true + return nil +} + +func (interceptor *fakeOutputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + interceptor.DidStopInterceptingOutput = true + return interceptor.InterceptedOutput, nil +} diff --git a/remote/fake_poster_test.go b/remote/fake_poster_test.go new file mode 100644 index 000000000..3543c59c6 --- /dev/null +++ b/remote/fake_poster_test.go @@ -0,0 +1,33 @@ +package remote_test + +import ( + "io" + "io/ioutil" + "net/http" +) + +type post struct { + url string + bodyType string + bodyContent []byte +} + +type fakePoster struct { + posts []post +} + +func newFakePoster() *fakePoster { + return &fakePoster{ + posts: make([]post, 0), + } +} + +func (poster *fakePoster) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { + bodyContent, _ := ioutil.ReadAll(body) + poster.posts = append(poster.posts, post{ + url: url, + bodyType: bodyType, + bodyContent: bodyContent, + }) + return nil, nil +} diff --git a/remote/forwarding_reporter.go b/remote/forwarding_reporter.go new file mode 100644 index 000000000..b19d46b27 --- /dev/null +++ b/remote/forwarding_reporter.go @@ -0,0 +1,73 @@ +package remote + +import ( + "bytes" + "encoding/json" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" + "io" + "net/http" +) + +//An interface to net/http's client to allow the injection of fakes under test +type Poster interface { + Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) +} + +/* +The ForwardingReporter is a Ginkgo reporter that forwards information to +a Ginkgo remote server. + +When streaming parallel test output, this repoter is automatically installed by Ginkgo. + +This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner +detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter +in place of Ginkgo's DefaultReporter. +*/ + +type ForwardingReporter struct { + serverHost string + poster Poster + outputInterceptor OutputInterceptor +} + +func NewForwardingReporter(serverHost string, poster Poster, outputInterceptor OutputInterceptor) *ForwardingReporter { + return &ForwardingReporter{ + serverHost: serverHost, + poster: poster, + outputInterceptor: outputInterceptor, + } +} + +func (reporter *ForwardingReporter) post(path string, data interface{}) { + encoded, _ := json.Marshal(data) + buffer := bytes.NewBuffer(encoded) + reporter.poster.Post("http://"+reporter.serverHost+path, "application/json", buffer) +} + +func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary *types.SuiteSummary) { + data := struct { + Config config.GinkgoConfigType `json:"config"` + Summary *types.SuiteSummary `json:"suite-summary"` + }{ + conf, + summary, + } + + reporter.post("/SpecSuiteWillBegin", data) +} + +func (reporter *ForwardingReporter) ExampleWillRun(exampleSummary *types.ExampleSummary) { + reporter.outputInterceptor.StartInterceptingOutput() + reporter.post("/ExampleWillRun", exampleSummary) +} + +func (reporter *ForwardingReporter) ExampleDidComplete(exampleSummary *types.ExampleSummary) { + output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput() + exampleSummary.CapturedOutput = output + reporter.post("/ExampleDidComplete", exampleSummary) +} + +func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + reporter.post("/SpecSuiteDidEnd", summary) +} diff --git a/remote/forwarding_reporter_test.go b/remote/forwarding_reporter_test.go new file mode 100644 index 000000000..67b0948ca --- /dev/null +++ b/remote/forwarding_reporter_test.go @@ -0,0 +1,128 @@ +package remote_test + +import ( + "encoding/json" + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/remote" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("ForwardingReporter", func() { + var ( + reporter *ForwardingReporter + interceptor *fakeOutputInterceptor + poster *fakePoster + suiteSummary *types.SuiteSummary + exampleSummary *types.ExampleSummary + serverHost string + ) + + BeforeEach(func() { + serverHost = "127.0.0.1:7788" + + poster = newFakePoster() + + interceptor = &fakeOutputInterceptor{ + InterceptedOutput: "The intercepted output!", + } + + reporter = NewForwardingReporter(serverHost, poster, interceptor) + + suiteSummary = &types.SuiteSummary{ + SuiteDescription: "My Test Suite", + } + + exampleSummary = &types.ExampleSummary{ + ComponentTexts: []string{"My", "Example"}, + State: types.ExampleStatePassed, + } + }) + + Context("When a suite begins", func() { + BeforeEach(func() { + reporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) + }) + + It("should POST the SuiteSummary and Ginkgo Config to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteWillBegin")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var sentData struct { + SentConfig config.GinkgoConfigType `json:"config"` + SentSuiteSummary *types.SuiteSummary `json:"suite-summary"` + } + + err := json.Unmarshal(poster.posts[0].bodyContent, &sentData) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(sentData.SentConfig).Should(Equal(config.GinkgoConfig)) + Ω(sentData.SentSuiteSummary).Should(Equal(suiteSummary)) + }) + }) + + Context("When an example will run", func() { + BeforeEach(func() { + reporter.ExampleWillRun(exampleSummary) + }) + + It("should POST the ExampleSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/ExampleWillRun")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.ExampleSummary + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + Ω(summary).Should(Equal(exampleSummary)) + }) + + It("should start intercepting output", func() { + Ω(interceptor.DidStartInterceptingOutput).Should(BeTrue()) + }) + + Context("When an example completes", func() { + BeforeEach(func() { + exampleSummary.State = types.ExampleStatePanicked + reporter.ExampleDidComplete(exampleSummary) + }) + + It("should POST the ExampleSummary to the Ginkgo server and include any intercepted output", func() { + Ω(poster.posts).Should(HaveLen(2)) + Ω(poster.posts[1].url).Should(Equal("http://127.0.0.1:7788/ExampleDidComplete")) + Ω(poster.posts[1].bodyType).Should(Equal("application/json")) + + var summary *types.ExampleSummary + err := json.Unmarshal(poster.posts[1].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + exampleSummary.CapturedOutput = interceptor.InterceptedOutput + Ω(summary).Should(Equal(exampleSummary)) + }) + + It("should stop intercepting output", func() { + Ω(interceptor.DidStopInterceptingOutput).Should(BeTrue()) + }) + }) + }) + + Context("When a suite ends", func() { + BeforeEach(func() { + reporter.SpecSuiteDidEnd(suiteSummary) + }) + + It("should POST the SuiteSummary to the Ginkgo server", func() { + Ω(poster.posts).Should(HaveLen(1)) + Ω(poster.posts[0].url).Should(Equal("http://127.0.0.1:7788/SpecSuiteDidEnd")) + Ω(poster.posts[0].bodyType).Should(Equal("application/json")) + + var summary *types.SuiteSummary + + err := json.Unmarshal(poster.posts[0].bodyContent, &summary) + Ω(err).ShouldNot(HaveOccurred()) + + Ω(summary).Should(Equal(suiteSummary)) + }) + }) +}) diff --git a/remote/output_interceptor.go b/remote/output_interceptor.go new file mode 100644 index 000000000..781aef9ae --- /dev/null +++ b/remote/output_interceptor.go @@ -0,0 +1,79 @@ +package remote + +import ( + "errors" + "io/ioutil" + "os" + "syscall" +) + +/* +The OutputInterceptor is used by the ForwardingReporter to +intercept and capture all stdin and stderr output during a test run. +*/ +type OutputInterceptor interface { + StartInterceptingOutput() error + StopInterceptingAndReturnOutput() (string, error) +} + +func NewOutputInterceptor() OutputInterceptor { + return &outputInterceptor{} +} + +type outputInterceptor struct { + stdoutPlaceholder *os.File + stderrPlaceholder *os.File + redirectFile *os.File + intercepting bool +} + +func (interceptor *outputInterceptor) StartInterceptingOutput() error { + if interceptor.intercepting { + return errors.New("Already intercepting output!") + } + interceptor.intercepting = true + + var err error + + interceptor.redirectFile, err = ioutil.TempFile("", "ginkgo") + if err != nil { + return err + } + + interceptor.stdoutPlaceholder, err = ioutil.TempFile("", "ginkgo") + if err != nil { + return err + } + + interceptor.stderrPlaceholder, err = ioutil.TempFile("", "ginkgo") + if err != nil { + return err + } + + syscall.Dup2(1, int(interceptor.stdoutPlaceholder.Fd())) + syscall.Dup2(2, int(interceptor.stderrPlaceholder.Fd())) + + syscall.Dup2(int(interceptor.redirectFile.Fd()), 1) + syscall.Dup2(int(interceptor.redirectFile.Fd()), 2) + + return nil +} + +func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) { + syscall.Dup2(int(interceptor.stdoutPlaceholder.Fd()), 1) + syscall.Dup2(int(interceptor.stderrPlaceholder.Fd()), 2) + + for _, f := range []*os.File{interceptor.redirectFile, interceptor.stdoutPlaceholder, interceptor.stderrPlaceholder} { + f.Close() + } + + output, err := ioutil.ReadFile(interceptor.redirectFile.Name()) + + for _, f := range []*os.File{interceptor.redirectFile, interceptor.stdoutPlaceholder, interceptor.stderrPlaceholder} { + os.Remove(f.Name()) + } + + interceptor.intercepting = false + + return string(output), err +} diff --git a/remote/output_interceptor_test.go b/remote/output_interceptor_test.go new file mode 100644 index 000000000..b2cbb30b2 --- /dev/null +++ b/remote/output_interceptor_test.go @@ -0,0 +1,64 @@ +package remote_test + +import ( + "fmt" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/remote" + . "github.com/onsi/gomega" + "os" +) + +var _ = Describe("OutputInterceptor", func() { + var interceptor OutputInterceptor + + BeforeEach(func() { + interceptor = NewOutputInterceptor() + }) + + It("should capture all stdout/stderr output", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + + fmt.Fprint(os.Stdout, "STDOUT") + fmt.Fprint(os.Stderr, "STDERR") + print("PRINT") + + output, err := interceptor.StopInterceptingAndReturnOutput() + + Ω(output).Should(Equal("STDOUTSTDERRPRINT")) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should error if told to intercept output twice", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + + print("A") + + err = interceptor.StartInterceptingOutput() + Ω(err).Should(HaveOccurred()) + + print("B") + + output, err := interceptor.StopInterceptingAndReturnOutput() + + Ω(output).Should(Equal("AB")) + Ω(err).ShouldNot(HaveOccurred()) + }) + + It("should allow multiple interception sessions", func() { + err := interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + print("A") + output, err := interceptor.StopInterceptingAndReturnOutput() + Ω(output).Should(Equal("A")) + Ω(err).ShouldNot(HaveOccurred()) + + err = interceptor.StartInterceptingOutput() + Ω(err).ShouldNot(HaveOccurred()) + print("B") + output, err = interceptor.StopInterceptingAndReturnOutput() + Ω(output).Should(Equal("B")) + Ω(err).ShouldNot(HaveOccurred()) + }) +}) diff --git a/remote/remote_suite_test.go b/remote/remote_suite_test.go new file mode 100644 index 000000000..e6b4e9f32 --- /dev/null +++ b/remote/remote_suite_test.go @@ -0,0 +1,13 @@ +package remote_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRemote(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Remote Spec Forwarding Suite") +} diff --git a/remote/server.go b/remote/server.go new file mode 100644 index 000000000..e7c8bc7a6 --- /dev/null +++ b/remote/server.go @@ -0,0 +1,130 @@ +/* + +The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners. +This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser). + +*/ + +package remote + +import ( + "encoding/json" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + "io/ioutil" + "net" + "net/http" +) + +/* +Server spins up on an automatically selected port and listens for communication from the forwarding reporter. +It then forwards that communication to attached reporters. +*/ +type Server struct { + listener net.Listener + reporters []reporters.Reporter +} + +//Create a new server, automatically selecting a port +func NewServer() (*Server, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + return &Server{ + listener: listener, + }, nil +} + +//Start the server. You don't need to `go s.Start()`, just `s.Start()` +func (server *Server) Start() { + httpServer := &http.Server{} + mux := http.NewServeMux() + httpServer.Handler = mux + + mux.HandleFunc("/SpecSuiteWillBegin", func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + body, _ := ioutil.ReadAll(request.Body) + server.specSuiteWillBegin(body) + writer.WriteHeader(200) + }) + + mux.HandleFunc("/ExampleWillRun", func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + body, _ := ioutil.ReadAll(request.Body) + server.exampleWillRun(body) + writer.WriteHeader(200) + }) + + mux.HandleFunc("/ExampleDidComplete", func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + body, _ := ioutil.ReadAll(request.Body) + server.exampleDidComplete(body) + writer.WriteHeader(200) + }) + + mux.HandleFunc("/SpecSuiteDidEnd", func(writer http.ResponseWriter, request *http.Request) { + defer request.Body.Close() + body, _ := ioutil.ReadAll(request.Body) + server.specSuiteDidEnd(body) + writer.WriteHeader(200) + }) + + go httpServer.Serve(server.listener) +} + +//Stop the server +func (server *Server) Stop() { + server.listener.Close() +} + +//The address the server can be reached it. Pass this into the `ForwardingReporter`. +func (server *Server) Address() string { + return server.listener.Addr().String() +} + +//The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters` +func (server *Server) RegisterReporters(reporters ...reporters.Reporter) { + server.reporters = reporters +} + +func (server *Server) specSuiteWillBegin(body []byte) { + var data struct { + Config config.GinkgoConfigType `json:"config"` + Summary *types.SuiteSummary `json:"suite-summary"` + } + + json.Unmarshal(body, &data) + + for _, reporter := range server.reporters { + reporter.SpecSuiteWillBegin(data.Config, data.Summary) + } +} + +func (server *Server) exampleWillRun(body []byte) { + var exampleSummary *types.ExampleSummary + json.Unmarshal(body, &exampleSummary) + + for _, reporter := range server.reporters { + reporter.ExampleWillRun(exampleSummary) + } +} + +func (server *Server) exampleDidComplete(body []byte) { + var exampleSummary *types.ExampleSummary + json.Unmarshal(body, &exampleSummary) + + for _, reporter := range server.reporters { + reporter.ExampleDidComplete(exampleSummary) + } +} + +func (server *Server) specSuiteDidEnd(body []byte) { + var suiteSummary *types.SuiteSummary + json.Unmarshal(body, &suiteSummary) + + for _, reporter := range server.reporters { + reporter.SpecSuiteDidEnd(suiteSummary) + } +} diff --git a/remote/server_test.go b/remote/server_test.go new file mode 100644 index 000000000..b01a4b593 --- /dev/null +++ b/remote/server_test.go @@ -0,0 +1,91 @@ +package remote_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + . "github.com/onsi/ginkgo/remote" + "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "net/http" +) + +var _ = Describe("Server", func() { + var ( + server *Server + reporterA, reporterB *reporters.FakeReporter + forwardingReporter *ForwardingReporter + + suiteSummary *types.SuiteSummary + exampleSummary *types.ExampleSummary + ) + + BeforeEach(func() { + var err error + server, err = NewServer() + Ω(err).ShouldNot(HaveOccurred()) + reporterA = reporters.NewFakeReporter() + reporterB = reporters.NewFakeReporter() + + server.RegisterReporters(reporterA, reporterB) + + forwardingReporter = NewForwardingReporter(server.Address(), &http.Client{}, &fakeOutputInterceptor{}) + + suiteSummary = &types.SuiteSummary{ + SuiteDescription: "My Test Suite", + } + + exampleSummary = &types.ExampleSummary{ + ComponentTexts: []string{"My", "Example"}, + State: types.ExampleStatePassed, + } + + server.Start() + }) + + AfterEach(func() { + server.Stop() + }) + + It("should make its address available", func() { + Ω(server.Address()).Should(MatchRegexp(`127.0.0.1:\d{2,}`)) + }) + + Describe("/SpecSuiteWillBegin", func() { + It("should decode and forward the Ginkgo config and suite summary", func(done Done) { + forwardingReporter.SpecSuiteWillBegin(config.GinkgoConfig, suiteSummary) + Ω(reporterA.Config).Should(Equal(config.GinkgoConfig)) + Ω(reporterB.Config).Should(Equal(config.GinkgoConfig)) + Ω(reporterA.BeginSummary).Should(Equal(suiteSummary)) + Ω(reporterB.BeginSummary).Should(Equal(suiteSummary)) + close(done) + }) + }) + + Describe("/ExampleWillRun", func() { + It("should decode and forward the example summary", func(done Done) { + forwardingReporter.ExampleWillRun(exampleSummary) + Ω(reporterA.ExampleWillRunSummaries[0]).Should(Equal(exampleSummary)) + Ω(reporterB.ExampleWillRunSummaries[0]).Should(Equal(exampleSummary)) + close(done) + }) + }) + + Describe("/ExampleDidComplete", func() { + It("should decode and forward the example summary", func(done Done) { + forwardingReporter.ExampleDidComplete(exampleSummary) + Ω(reporterA.ExampleSummaries[0]).Should(Equal(exampleSummary)) + Ω(reporterB.ExampleSummaries[0]).Should(Equal(exampleSummary)) + close(done) + }) + }) + + Describe("/SpecSuiteDidEnd", func() { + It("should decode and forward the suite summary", func(done Done) { + forwardingReporter.SpecSuiteDidEnd(suiteSummary) + Ω(reporterA.EndSummary).Should(Equal(suiteSummary)) + Ω(reporterB.EndSummary).Should(Equal(suiteSummary)) + close(done) + }) + }) +}) diff --git a/reporters/default_reporter.go b/reporters/default_reporter.go index e4c860ec6..dd0ed304c 100644 --- a/reporters/default_reporter.go +++ b/reporters/default_reporter.go @@ -15,13 +15,13 @@ import ( type DefaultReporter struct { config config.DefaultReporterConfigType - stenographer *stenographer.Stenographer + stenographer stenographer.Stenographer } -func NewDefaultReporter(config config.DefaultReporterConfigType) *DefaultReporter { +func NewDefaultReporter(config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *DefaultReporter { return &DefaultReporter{ config: config, - stenographer: stenographer.New(!config.NoColor), + stenographer: stenographer, } } diff --git a/reporters/default_reporter_test.go b/reporters/default_reporter_test.go new file mode 100644 index 000000000..9e1912b75 --- /dev/null +++ b/reporters/default_reporter_test.go @@ -0,0 +1,246 @@ +package reporters_test + +import ( + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" + st "github.com/onsi/ginkgo/stenographer" + "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" + "time" +) + +var _ = Describe("DefaultReporter", func() { + var ( + reporter *reporters.DefaultReporter + reporterConfig config.DefaultReporterConfigType + stenographer *st.FakeStenographer + + ginkgoConfig config.GinkgoConfigType + suite *types.SuiteSummary + example *types.ExampleSummary + ) + + BeforeEach(func() { + stenographer = st.NewFakeStenographer() + reporterConfig = config.DefaultReporterConfigType{ + NoColor: false, + SlowSpecThreshold: 0.1, + NoisyPendings: true, + Succinct: true, + Verbose: true, + } + + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + }) + + call := func(method string, args ...interface{}) st.FakeStenographerCall { + return st.NewFakeStenographerCall(method, args...) + } + + Describe("SpecSuiteWillBegin", func() { + BeforeEach(func() { + suite = &types.SuiteSummary{ + SuiteDescription: "A Sweet Suite", + NumberOfTotalExamples: 10, + NumberOfExamplesThatWillBeRun: 8, + } + + ginkgoConfig = config.GinkgoConfigType{ + RandomSeed: 1138, + RandomizeAllSpecs: true, + } + }) + + Context("when a serial (non-parallel) suite begins", func() { + BeforeEach(func() { + ginkgoConfig.ParallelTotal = 1 + + reporter.SpecSuiteWillBegin(ginkgoConfig, suite) + }) + + It("should announce the suite, then announce the number of specs", func() { + Ω(stenographer.Calls).Should(HaveLen(2)) + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true))) + Ω(stenographer.Calls[1]).Should(Equal(call("AnnounceNumberOfSpecs", 8, 10))) + }) + }) + + Context("when a parallel suite begins", func() { + BeforeEach(func() { + ginkgoConfig.ParallelTotal = 2 + ginkgoConfig.ParallelNode = 1 + suite.NumberOfExamplesBeforeParallelization = 20 + + reporter.SpecSuiteWillBegin(ginkgoConfig, suite) + }) + + It("should announce the suite, announce that it's a parallel run, then announce the number of specs", func() { + Ω(stenographer.Calls).Should(HaveLen(3)) + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuite", "A Sweet Suite", ginkgoConfig.RandomSeed, true))) + Ω(stenographer.Calls[1]).Should(Equal(call("AnnounceParallelRun", 1, 2, 10, 20))) + Ω(stenographer.Calls[2]).Should(Equal(call("AnnounceNumberOfSpecs", 8, 10))) + }) + }) + }) + + Describe("ExampleWillRun", func() { + Context("When running in verbose mode", func() { + Context("and the example will run", func() { + BeforeEach(func() { + example = &types.ExampleSummary{} + reporter.ExampleWillRun(example) + }) + + It("should announce that the example will run", func() { + Ω(stenographer.Calls).Should(HaveLen(1)) + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceExampleWillRun", example))) + }) + }) + + Context("and the example will not run", func() { + Context("because it is pending", func() { + BeforeEach(func() { + example = &types.ExampleSummary{ + State: types.ExampleStatePending, + } + reporter.ExampleWillRun(example) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + }) + + Context("because it is skipped", func() { + BeforeEach(func() { + example = &types.ExampleSummary{ + State: types.ExampleStateSkipped, + } + reporter.ExampleWillRun(example) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + }) + }) + }) + + Context("When not running in verbose mode", func() { + BeforeEach(func() { + reporterConfig.Verbose = false + reporter = reporters.NewDefaultReporter(reporterConfig, stenographer) + example = &types.ExampleSummary{} + reporter.ExampleWillRun(example) + }) + + It("should announce nothing", func() { + Ω(stenographer.Calls).Should(BeEmpty()) + }) + }) + }) + + Describe("ExampleDidComplete", func() { + JustBeforeEach(func() { + reporter.ExampleDidComplete(example) + }) + + BeforeEach(func() { + example = &types.ExampleSummary{} + }) + + Context("When the example passed", func() { + BeforeEach(func() { + example.State = types.ExampleStatePassed + }) + + Context("When the example was a measurement", func() { + BeforeEach(func() { + example.IsMeasurement = true + }) + + It("should announce the measurement", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuccesfulMeasurement", example, true))) + }) + }) + + Context("When the example is slow", func() { + BeforeEach(func() { + example.RunTime = time.Second + }) + + It("should announce that it was slow", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuccesfulSlowExample", example, true))) + }) + }) + + Context("Otherwise", func() { + It("should announce the succesful example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSuccesfulExample", example))) + }) + }) + }) + + Context("When the example is pending", func() { + BeforeEach(func() { + example.State = types.ExampleStatePending + }) + + It("should announce the pending example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnouncePendingExample", example, true, true))) + }) + }) + + Context("When the example is skipped", func() { + BeforeEach(func() { + example.State = types.ExampleStateSkipped + }) + + It("should announce the skipped example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSkippedExample", example))) + }) + }) + + Context("When the example timed out", func() { + BeforeEach(func() { + example.State = types.ExampleStateTimedOut + }) + + It("should announce the timedout example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceExampleTimedOut", example, true))) + }) + }) + + Context("When the example panicked", func() { + BeforeEach(func() { + example.State = types.ExampleStatePanicked + }) + + It("should announce the panicked example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceExamplePanicked", example, true))) + }) + }) + + Context("When the example failed", func() { + BeforeEach(func() { + example.State = types.ExampleStateFailed + }) + + It("should announce the failed example", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceExampleFailed", example, true))) + }) + }) + }) + + Describe("SpecSuiteDidEnd", func() { + BeforeEach(func() { + suite = &types.SuiteSummary{} + reporter.SpecSuiteDidEnd(suite) + }) + + It("should announce the spec run's completion", func() { + Ω(stenographer.Calls[0]).Should(Equal(call("AnnounceSpecRunCompletion", suite))) + }) + }) +}) diff --git a/reporters/fake_reporter.go b/reporters/fake_reporter.go new file mode 100644 index 000000000..9cfc9df78 --- /dev/null +++ b/reporters/fake_reporter.go @@ -0,0 +1,40 @@ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +//FakeReporter is useful for testing purposes +type FakeReporter struct { + Config config.GinkgoConfigType + + BeginSummary *types.SuiteSummary + ExampleWillRunSummaries []*types.ExampleSummary + ExampleSummaries []*types.ExampleSummary + EndSummary *types.SuiteSummary +} + +func NewFakeReporter() *FakeReporter { + return &FakeReporter{ + ExampleWillRunSummaries: make([]*types.ExampleSummary, 0), + ExampleSummaries: make([]*types.ExampleSummary, 0), + } +} + +func (fakeR *FakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) { + fakeR.Config = config + fakeR.BeginSummary = summary +} + +func (fakeR *FakeReporter) ExampleWillRun(exampleSummary *types.ExampleSummary) { + fakeR.ExampleWillRunSummaries = append(fakeR.ExampleWillRunSummaries, exampleSummary) +} + +func (fakeR *FakeReporter) ExampleDidComplete(exampleSummary *types.ExampleSummary) { + fakeR.ExampleSummaries = append(fakeR.ExampleSummaries, exampleSummary) +} + +func (fakeR *FakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) { + fakeR.EndSummary = summary +} diff --git a/reporters/junit_reporter_test.go b/reporters/junit_reporter_test.go index ed3c003a1..c10f42d99 100644 --- a/reporters/junit_reporter_test.go +++ b/reporters/junit_reporter_test.go @@ -4,7 +4,7 @@ import ( "encoding/xml" . "github.com/onsi/ginkgo" "github.com/onsi/ginkgo/config" - . "github.com/onsi/ginkgo/reporters" + "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" "io/ioutil" @@ -17,10 +17,10 @@ var _ = Describe("JUnit Reporter", func() { reporter Reporter ) - readOutputFile := func() JUnitTestSuite { + readOutputFile := func() reporters.JUnitTestSuite { bytes, err := ioutil.ReadFile(outputFile) Ω(err).ShouldNot(HaveOccurred()) - var suite JUnitTestSuite + var suite reporters.JUnitTestSuite err = xml.Unmarshal(bytes, &suite) Ω(err).ShouldNot(HaveOccurred()) return suite @@ -28,7 +28,7 @@ var _ = Describe("JUnit Reporter", func() { BeforeEach(func() { outputFile = "/tmp/test.xml" - reporter = NewJUnitReporter(outputFile) + reporter = reporters.NewJUnitReporter(outputFile) reporter.SpecSuiteWillBegin(config.GinkgoConfigType{}, &types.SuiteSummary{ SuiteDescription: "My test suite", diff --git a/reporters/reporter.go b/reporters/reporter.go new file mode 100644 index 000000000..8537bac15 --- /dev/null +++ b/reporters/reporter.go @@ -0,0 +1,13 @@ +package reporters + +import ( + "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/types" +) + +type Reporter interface { + SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) + ExampleWillRun(exampleSummary *types.ExampleSummary) + ExampleDidComplete(exampleSummary *types.ExampleSummary) + SpecSuiteDidEnd(summary *types.SuiteSummary) +} diff --git a/stenographer/console_logging.go b/stenographer/console_logging.go index 0d621717b..ce5433af6 100644 --- a/stenographer/console_logging.go +++ b/stenographer/console_logging.go @@ -1,8 +1,3 @@ -/* -The stenographer is used by Ginkgo's default reporter to generate output. - -Move along, nothing to see here. -*/ package stenographer import ( @@ -10,7 +5,7 @@ import ( "strings" ) -func (s *Stenographer) colorize(colorCode string, format string, args ...interface{}) string { +func (s *consoleStenographer) colorize(colorCode string, format string, args ...interface{}) string { var out string if len(args) > 0 { @@ -26,28 +21,28 @@ func (s *Stenographer) colorize(colorCode string, format string, args ...interfa } } -func (s *Stenographer) printBanner(text string, bannerCharacter string) { +func (s *consoleStenographer) printBanner(text string, bannerCharacter string) { fmt.Println(text) fmt.Println(strings.Repeat(bannerCharacter, len(text))) } -func (s *Stenographer) printNewLine() { +func (s *consoleStenographer) printNewLine() { fmt.Println("") } -func (s *Stenographer) printDelimiter() { +func (s *consoleStenographer) printDelimiter() { fmt.Println(s.colorize(grayColor, "%s", strings.Repeat("-", 30))) } -func (s *Stenographer) print(indentation int, format string, args ...interface{}) { +func (s *consoleStenographer) print(indentation int, format string, args ...interface{}) { fmt.Print(s.indent(indentation, format, args...)) } -func (s *Stenographer) println(indentation int, format string, args ...interface{}) { +func (s *consoleStenographer) println(indentation int, format string, args ...interface{}) { fmt.Println(s.indent(indentation, format, args...)) } -func (s *Stenographer) indent(indentation int, format string, args ...interface{}) string { +func (s *consoleStenographer) indent(indentation int, format string, args ...interface{}) string { var text string if len(args) > 0 { diff --git a/stenographer/fake_stenographer.go b/stenographer/fake_stenographer.go new file mode 100644 index 000000000..49cceafc3 --- /dev/null +++ b/stenographer/fake_stenographer.go @@ -0,0 +1,106 @@ +package stenographer + +import ( + "github.com/onsi/ginkgo/types" +) + +func NewFakeStenographerCall(method string, args ...interface{}) FakeStenographerCall { + return FakeStenographerCall{ + Method: method, + Args: args, + } +} + +type FakeStenographer struct { + Calls []FakeStenographerCall +} + +type FakeStenographerCall struct { + Method string + Args []interface{} +} + +func NewFakeStenographer() *FakeStenographer { + stenographer := &FakeStenographer{} + stenographer.Reset() + return stenographer +} + +func (stenographer *FakeStenographer) Reset() { + stenographer.Calls = make([]FakeStenographerCall, 0) +} + +func (stenographer *FakeStenographer) CallsTo(method string) []FakeStenographerCall { + results := make([]FakeStenographerCall, 0) + for _, call := range stenographer.Calls { + if call.Method == method { + results = append(results, call) + } + } + + return results +} + +func (stenographer *FakeStenographer) registerCall(method string, args ...interface{}) { + stenographer.Calls = append(stenographer.Calls, NewFakeStenographerCall(method, args...)) +} + +func (stenographer *FakeStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool) { + stenographer.registerCall("AnnounceSuite", description, randomSeed, randomizingAll) +} + +func (stenographer *FakeStenographer) AnnounceAggregatedParallelRun(nodes int) { + stenographer.registerCall("AnnounceAggregatedParallelRun", nodes) +} + +func (stenographer *FakeStenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int) { + stenographer.registerCall("AnnounceParallelRun", node, nodes, specsToRun, totalSpecs) +} + +func (stenographer *FakeStenographer) AnnounceNumberOfSpecs(specsToRun int, total int) { + stenographer.registerCall("AnnounceNumberOfSpecs", specsToRun, total) +} + +func (stenographer *FakeStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary) { + stenographer.registerCall("AnnounceSpecRunCompletion", summary) +} + +func (stenographer *FakeStenographer) AnnounceExampleWillRun(example *types.ExampleSummary) { + stenographer.registerCall("AnnounceExampleWillRun", example) +} + +func (stenographer *FakeStenographer) AnnounceCapturedOutput(example *types.ExampleSummary) { + stenographer.registerCall("AnnounceCapturedOutput", example) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulExample(example *types.ExampleSummary) { + stenographer.registerCall("AnnounceSuccesfulExample", example) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulSlowExample(example *types.ExampleSummary, succinct bool) { + stenographer.registerCall("AnnounceSuccesfulSlowExample", example, succinct) +} + +func (stenographer *FakeStenographer) AnnounceSuccesfulMeasurement(example *types.ExampleSummary, succinct bool) { + stenographer.registerCall("AnnounceSuccesfulMeasurement", example, succinct) +} + +func (stenographer *FakeStenographer) AnnouncePendingExample(example *types.ExampleSummary, noisy bool, succinct bool) { + stenographer.registerCall("AnnouncePendingExample", example, noisy, succinct) +} + +func (stenographer *FakeStenographer) AnnounceSkippedExample(example *types.ExampleSummary) { + stenographer.registerCall("AnnounceSkippedExample", example) +} + +func (stenographer *FakeStenographer) AnnounceExampleTimedOut(example *types.ExampleSummary, succinct bool) { + stenographer.registerCall("AnnounceExampleTimedOut", example, succinct) +} + +func (stenographer *FakeStenographer) AnnounceExamplePanicked(example *types.ExampleSummary, succinct bool) { + stenographer.registerCall("AnnounceExamplePanicked", example, succinct) +} + +func (stenographer *FakeStenographer) AnnounceExampleFailed(example *types.ExampleSummary, succinct bool) { + stenographer.registerCall("AnnounceExampleFailed", example, succinct) +} diff --git a/stenographer/stenographer.go b/stenographer/stenographer.go index a38af9be3..5b7cd443e 100644 --- a/stenographer/stenographer.go +++ b/stenographer/stenographer.go @@ -1,3 +1,9 @@ +/* +The stenographer is used by Ginkgo's reporters to generate output. + +Move along, nothing to see here. +*/ + package stenographer import ( @@ -24,21 +30,44 @@ const ( cursorStateEndBlock ) -type Stenographer struct { - color bool - cursorState cursorStateType +type Stenographer interface { + AnnounceSuite(description string, randomSeed int64, randomizingAll bool) + AnnounceAggregatedParallelRun(nodes int) + AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int) + AnnounceNumberOfSpecs(specsToRun int, total int) + AnnounceSpecRunCompletion(summary *types.SuiteSummary) + + AnnounceExampleWillRun(example *types.ExampleSummary) + + AnnounceCapturedOutput(example *types.ExampleSummary) + + AnnounceSuccesfulExample(example *types.ExampleSummary) + AnnounceSuccesfulSlowExample(example *types.ExampleSummary, succinct bool) + AnnounceSuccesfulMeasurement(example *types.ExampleSummary, succinct bool) + + AnnouncePendingExample(example *types.ExampleSummary, noisy bool, succinct bool) + AnnounceSkippedExample(example *types.ExampleSummary) + + AnnounceExampleTimedOut(example *types.ExampleSummary, succinct bool) + AnnounceExamplePanicked(example *types.ExampleSummary, succinct bool) + AnnounceExampleFailed(example *types.ExampleSummary, succinct bool) } -func New(color bool) *Stenographer { - return &Stenographer{ +func New(color bool) Stenographer { + return &consoleStenographer{ color: color, cursorState: cursorStateTop, } } +type consoleStenographer struct { + color bool + cursorState cursorStateType +} + var alternatingColors = []string{defaultStyle, grayColor} -func (s *Stenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool) { +func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool) { s.printNewLine() s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=") s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed)) @@ -48,7 +77,7 @@ func (s *Stenographer) AnnounceSuite(description string, randomSeed int64, rando s.printNewLine() } -func (s *Stenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int) { +func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, totalSpecs int) { s.println(0, "Parallel test node %s/%s. Assigned %s of %s specs.", s.colorize(boldStyle, "%d", node), @@ -59,7 +88,15 @@ func (s *Stenographer) AnnounceParallelRun(node int, nodes int, specsToRun int, s.printNewLine() } -func (s *Stenographer) AnnounceNumberOfSpecs(specsToRun int, total int) { +func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int) { + s.println(0, + "Running in parallel across %s nodes", + s.colorize(boldStyle, "%d", nodes), + ) + s.printNewLine() +} + +func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int) { s.println(0, "Will run %s of %s specs", s.colorize(boldStyle, "%d", specsToRun), @@ -69,7 +106,7 @@ func (s *Stenographer) AnnounceNumberOfSpecs(specsToRun int, total int) { s.printNewLine() } -func (s *Stenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary) { +func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary) { s.printNewLine() color := greenColor if !summary.SuiteSucceeded { @@ -95,7 +132,7 @@ func (s *Stenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary) { s.printNewLine() } -func (s *Stenographer) AnnounceExampleWillRun(example *types.ExampleSummary) { +func (s *consoleStenographer) AnnounceExampleWillRun(example *types.ExampleSummary) { if s.cursorState == cursorStateStreaming { s.printNewLine() s.printDelimiter() @@ -117,12 +154,27 @@ func (s *Stenographer) AnnounceExampleWillRun(example *types.ExampleSummary) { s.cursorState = cursorStateMidBlock } -func (s *Stenographer) AnnounceSuccesfulExample(example *types.ExampleSummary) { +func (s *consoleStenographer) AnnounceCapturedOutput(example *types.ExampleSummary) { + if example.CapturedOutput == "" { + return + } + + if s.cursorState == cursorStateStreaming { + s.printNewLine() + s.printDelimiter() + } else if s.cursorState == cursorStateMidBlock { + s.printNewLine() + } + s.println(0, example.CapturedOutput) + s.cursorState = cursorStateMidBlock +} + +func (s *consoleStenographer) AnnounceSuccesfulExample(example *types.ExampleSummary) { s.print(0, s.colorize(greenColor, "•")) s.cursorState = cursorStateStreaming } -func (s *Stenographer) AnnounceSuccesfulSlowExample(example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) AnnounceSuccesfulSlowExample(example *types.ExampleSummary, succinct bool) { s.printBlockWithMessage( s.colorize(greenColor, "• [SLOW TEST:%.3f seconds]", example.RunTime.Seconds()), "", @@ -131,7 +183,7 @@ func (s *Stenographer) AnnounceSuccesfulSlowExample(example *types.ExampleSummar ) } -func (s *Stenographer) AnnounceSuccesfulMeasurement(example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) AnnounceSuccesfulMeasurement(example *types.ExampleSummary, succinct bool) { s.printBlockWithMessage( s.colorize(greenColor, "• [MEASUREMENT]"), s.measurementReport(example), @@ -140,7 +192,7 @@ func (s *Stenographer) AnnounceSuccesfulMeasurement(example *types.ExampleSummar ) } -func (s *Stenographer) AnnouncePendingExample(example *types.ExampleSummary, noisy bool, succinct bool) { +func (s *consoleStenographer) AnnouncePendingExample(example *types.ExampleSummary, noisy bool, succinct bool) { if noisy { s.printBlockWithMessage( s.colorize(yellowColor, "P [PENDING]"), @@ -154,24 +206,24 @@ func (s *Stenographer) AnnouncePendingExample(example *types.ExampleSummary, noi } } -func (s *Stenographer) AnnounceSkippedExample(example *types.ExampleSummary) { +func (s *consoleStenographer) AnnounceSkippedExample(example *types.ExampleSummary) { s.print(0, s.colorize(cyanColor, "S")) s.cursorState = cursorStateStreaming } -func (s *Stenographer) AnnounceExampleTimedOut(example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) AnnounceExampleTimedOut(example *types.ExampleSummary, succinct bool) { s.printFailure("•... Timeout", example, succinct) } -func (s *Stenographer) AnnounceExamplePanicked(example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) AnnounceExamplePanicked(example *types.ExampleSummary, succinct bool) { s.printFailure("•! Panic", example, succinct) } -func (s *Stenographer) AnnounceExampleFailed(example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) AnnounceExampleFailed(example *types.ExampleSummary, succinct bool) { s.printFailure("• Failure", example, succinct) } -func (s *Stenographer) printBlockWithMessage(header string, message string, example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) printBlockWithMessage(header string, message string, example *types.ExampleSummary, succinct bool) { if s.cursorState == cursorStateStreaming { s.printNewLine() s.printDelimiter() @@ -192,7 +244,7 @@ func (s *Stenographer) printBlockWithMessage(header string, message string, exam s.cursorState = cursorStateEndBlock } -func (s *Stenographer) printFailure(message string, example *types.ExampleSummary, succinct bool) { +func (s *consoleStenographer) printFailure(message string, example *types.ExampleSummary, succinct bool) { if s.cursorState == cursorStateStreaming { s.printNewLine() s.printDelimiter() @@ -222,7 +274,7 @@ func (s *Stenographer) printFailure(message string, example *types.ExampleSummar s.cursorState = cursorStateEndBlock } -func (s *Stenographer) printCodeLocationBlock(example *types.ExampleSummary, failure bool, succinct bool) int { +func (s *consoleStenographer) printCodeLocationBlock(example *types.ExampleSummary, failure bool, succinct bool) int { indentation := 0 startIndex := 1 @@ -277,7 +329,7 @@ func (s *Stenographer) printCodeLocationBlock(example *types.ExampleSummary, fai return indentation } -func (s *Stenographer) measurementReport(example *types.ExampleSummary) string { +func (s *consoleStenographer) measurementReport(example *types.ExampleSummary) string { if len(example.Measurements) == 0 { return "Found no measurements" } diff --git a/suite_test.go b/suite_test.go index 57dd1e959..d3d536b5c 100644 --- a/suite_test.go +++ b/suite_test.go @@ -2,6 +2,7 @@ package ginkgo import ( "github.com/onsi/ginkgo/config" + "github.com/onsi/ginkgo/reporters" "github.com/onsi/ginkgo/types" . "github.com/onsi/gomega" "math/rand" @@ -13,12 +14,12 @@ func init() { var ( specSuite *suite fakeT *fakeTestingT - fakeR *fakeReporter + fakeR *reporters.FakeReporter ) BeforeEach(func() { fakeT = &fakeTestingT{} - fakeR = &fakeReporter{} + fakeR = reporters.NewFakeReporter() specSuite = newSuite() }) @@ -81,9 +82,9 @@ func init() { }) It("provides the config and suite description to the reporter", func() { - Ω(fakeR.config.RandomSeed).Should(Equal(int64(randomSeed))) - Ω(fakeR.config.RandomizeAllSpecs).Should(Equal(randomizeAllSpecs)) - Ω(fakeR.beginSummary.SuiteDescription).Should(Equal("suite description")) + Ω(fakeR.Config.RandomSeed).Should(Equal(int64(randomSeed))) + Ω(fakeR.Config.RandomizeAllSpecs).Should(Equal(randomizeAllSpecs)) + Ω(fakeR.BeginSummary.SuiteDescription).Should(Equal("suite description")) }) Measure("should run measurements", func(b Benchmarker) { @@ -93,7 +94,7 @@ func init() { sleepTime := time.Duration(r.Float64() * 0.01 * float64(time.Second)) time.Sleep(sleepTime) }) - Ω(runtime.Seconds()).Should(BeNumerically("<=", 0.011)) + Ω(runtime.Seconds()).Should(BeNumerically("<=", 0.012)) Ω(runtime.Seconds()).Should(BeNumerically(">=", 0)) randomValue := r.Float64() * 10.0 @@ -200,9 +201,9 @@ func init() { }) It("generates the correct failure data", func() { - Ω(fakeR.exampleSummaries[0].Failure.Message).Should(Equal("oops!")) - Ω(fakeR.exampleSummaries[0].Failure.Location.FileName).Should(Equal(location.FileName)) - Ω(fakeR.exampleSummaries[0].Failure.Location.LineNumber).Should(Equal(location.LineNumber + 1)) + Ω(fakeR.ExampleSummaries[0].Failure.Message).Should(Equal("oops!")) + Ω(fakeR.ExampleSummaries[0].Failure.Location.FileName).Should(Equal(location.FileName)) + Ω(fakeR.ExampleSummaries[0].Failure.Location.LineNumber).Should(Equal(location.LineNumber + 1)) }) }) }) diff --git a/types/example_types.go b/types/example_types.go index 8fe6cfd5f..b1b83e923 100644 --- a/types/example_types.go +++ b/types/example_types.go @@ -7,6 +7,7 @@ import ( type SuiteSummary struct { SuiteDescription string SuiteSucceeded bool + SuiteID string NumberOfExamplesBeforeParallelization int NumberOfTotalExamples int @@ -28,6 +29,10 @@ type ExampleSummary struct { IsMeasurement bool NumberOfSamples int Measurements map[string]*ExampleMeasurement + + CapturedOutput string + SuiteID string + ExampleIndex int } type ExampleFailure struct { diff --git a/types/random_id_generator.go b/types/random_id_generator.go new file mode 100644 index 000000000..7bdb015e2 --- /dev/null +++ b/types/random_id_generator.go @@ -0,0 +1,15 @@ +package types + +import ( + "crypto/rand" + "fmt" +) + +func GenerateRandomID() string { + b := make([]byte, 8) + _, err := rand.Read(b) + if err != nil { + return "" + } + return fmt.Sprintf("%x-%x-%x-%x", b[0:2], b[2:4], b[4:6], b[6:8]) +} diff --git a/types/random_id_generator_test.go b/types/random_id_generator_test.go new file mode 100644 index 000000000..ff318324e --- /dev/null +++ b/types/random_id_generator_test.go @@ -0,0 +1,21 @@ +package types_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/types" + . "github.com/onsi/gomega" +) + +var _ = Describe("GuidGenerator", func() { + It("should generate a random guid", func() { + a := GenerateRandomID() + b := GenerateRandomID() + Ω(a).ShouldNot(BeEmpty()) + Ω(b).ShouldNot(BeEmpty()) + Ω(a).ShouldNot(Equal(b)) + + IDRegexp := "[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}" + Ω(a).Should(MatchRegexp(IDRegexp)) + Ω(b).Should(MatchRegexp(IDRegexp)) + }) +})