From e60e9151d39c4845da6400d00fbc48c3cafc3d98 Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Thu, 5 Aug 2021 06:43:15 -0600 Subject: [PATCH] Fix support for assertions in functions passed to Eventually and Consistently (fixes #457) The previous version (1.14.0) introduced a change to allow `Eventually` and `Consistently` to support functions that make assertions. This was accomplished by overriding the global fail handler when running the callbacks passed to `Eventually/Consistently` in order to capture any resulting errors. Issue #457 uncovered a flaw with this approach: when multiple `Eventually`s are running concurrently they race when overriding the singleton global fail handler. 1.15.0 resolves this by requiring users who want to make assertions in `Eventually/Consistently` call backs to explicitly pass in a function that takes a `Gomega` as an argument. The passed-in `Gomega` instance can be used to make assertions. Any failures will cause `Eventually` to retry the callback. This cleaner interface avoids the issue of swapping out globals but comes at the cost of changing the contract introduced in v1.14.0. As such 1.15.0 introduces a breaking change with respect to 1.14.0 - however we expect that adoption of this feature in 1.14.0 remains limited. In addition, 1.15.0 cleans up some of Gomega's internals. Most users shouldn't notice any differences stemming from the refactoring that was made. --- CHANGELOG.md | 9 + env.go | 40 - ghttp/test_server_test.go | 4 +- gomega_dsl.go | 342 +++------ internal/{assertion => }/assertion.go | 28 +- internal/assertion/assertion_suite_test.go | 13 - internal/assertion/assertion_test.go | 277 ------- internal/assertion_test.go | 147 ++++ .../{asyncassertion => }/async_assertion.go | 171 ++--- internal/async_assertion_test.go | 717 ++++++++++++++++++ .../async_assertion_suite_test.go | 13 - .../asyncassertion/async_assertion_test.go | 489 ------------ internal/defaults/defaults_suite_test.go | 13 - internal/defaults/env.go | 22 - internal/defaults/env_test.go | 81 -- internal/dsl_test.go | 188 +++++ internal/duration_bundle.go | 71 ++ internal/duration_bundle_test.go | 154 ++++ internal/fakematcher/fake_matcher.go | 23 - internal/gomega.go | 102 +++ internal/gomega_test.go | 90 +++ internal/internal_suite_test.go | 77 ++ internal/oraclematcher/oracle_matcher.go | 25 - internal/testingtsupport/testing_t_support.go | 60 -- .../testingtsupport/testing_t_support_test.go | 81 +- matchers/and.go | 5 +- matchers/not.go | 3 +- matchers/or.go | 5 +- matchers/with_transform.go | 3 +- types/types.go | 69 +- 30 files changed, 1820 insertions(+), 1502 deletions(-) delete mode 100644 env.go rename internal/{assertion => }/assertion.go (80%) delete mode 100644 internal/assertion/assertion_suite_test.go delete mode 100644 internal/assertion/assertion_test.go create mode 100644 internal/assertion_test.go rename internal/{asyncassertion => }/async_assertion.go (51%) create mode 100644 internal/async_assertion_test.go delete mode 100644 internal/asyncassertion/async_assertion_suite_test.go delete mode 100644 internal/asyncassertion/async_assertion_test.go delete mode 100644 internal/defaults/defaults_suite_test.go delete mode 100644 internal/defaults/env.go delete mode 100644 internal/defaults/env_test.go create mode 100644 internal/duration_bundle.go create mode 100644 internal/duration_bundle_test.go delete mode 100644 internal/fakematcher/fake_matcher.go create mode 100644 internal/gomega.go create mode 100644 internal/gomega_test.go delete mode 100644 internal/oraclematcher/oracle_matcher.go delete mode 100644 internal/testingtsupport/testing_t_support.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f2181a8cc..3486f3582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.15.0 + +### Fixes +The previous version (1.14.0) introduced a change to allow `Eventually` and `Consistently` to support functions that make assertions. This was accomplished by overriding the global fail handler when running the callbacks passed to `Eventually/Consistently` in order to capture any resulting errors. Issue #457 uncovered a flaw with this approach: when multiple `Eventually`s are running concurrently they race when overriding the singleton global fail handler. + +1.15.0 resolves this by requiring users who want to make assertions in `Eventually/Consistently` call backs to explicitly pass in a function that takes a `Gomega` as an argument. The passed-in `Gomega` instance can be used to make assertions. Any failures will cause `Eventually` to retry the callback. This cleaner interface avoids the issue of swapping out globals but comes at the cost of changing the contract introduced in v1.14.0. As such 1.15.0 introduces a breaking change with respect to 1.14.0 - however we expect that adoption of this feature in 1.14.0 remains limited. + +In addition, 1.15.0 cleans up some of Gomega's internals. Most users shouldn't notice any differences stemming from the refactoring that was made. + ## 1.14.0 ### Features diff --git a/env.go b/env.go deleted file mode 100644 index 62fd885a9..000000000 --- a/env.go +++ /dev/null @@ -1,40 +0,0 @@ -package gomega - -import ( - "os" - - "github.com/onsi/gomega/internal/defaults" -) - -const ( - ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION" - ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL" - EventuallyTimeoutEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT" - EventuallyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL" -) - -func init() { - defaults.SetDurationFromEnv( - os.Getenv, - SetDefaultConsistentlyDuration, - ConsistentlyDurationEnvVarName, - ) - - defaults.SetDurationFromEnv( - os.Getenv, - SetDefaultConsistentlyPollingInterval, - ConsistentlyPollingIntervalEnvVarName, - ) - - defaults.SetDurationFromEnv( - os.Getenv, - SetDefaultEventuallyTimeout, - EventuallyTimeoutEnvVarName, - ) - - defaults.SetDurationFromEnv( - os.Getenv, - SetDefaultEventuallyPollingInterval, - EventuallyPollingIntervalEnvVarName, - ) -} diff --git a/ghttp/test_server_test.go b/ghttp/test_server_test.go index bcd32c660..ea9d1bf7b 100644 --- a/ghttp/test_server_test.go +++ b/ghttp/test_server_test.go @@ -259,8 +259,8 @@ var _ = Describe("TestServer", func() { s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) { // Expect(true).Should(BeFalse()) <-- would be nice to do it this way, but the test just can't be written this way - By("We're cheating a bit here -- we're throwing a GINKGO_PANIC which simulates a failed assertion") - panic(GINKGO_PANIC) + By("We're cheating a bit here -- we're pretending to throw a Ginkgo panic which simulates a failed assertion") + panic("defer GinkgoRecover()") }) }) diff --git a/gomega_dsl.go b/gomega_dsl.go index 67f6e45c1..6a7c98eba 100644 --- a/gomega_dsl.go +++ b/gomega_dsl.go @@ -16,78 +16,92 @@ package gomega import ( "errors" "fmt" - "reflect" "time" - "github.com/onsi/gomega/internal/assertion" - "github.com/onsi/gomega/internal/asyncassertion" - "github.com/onsi/gomega/internal/testingtsupport" + "github.com/onsi/gomega/internal" "github.com/onsi/gomega/types" ) -const GOMEGA_VERSION = "1.14.0" +const GOMEGA_VERSION = "1.15.0" -const nilFailHandlerPanic = `You are trying to make an assertion, but Gomega's fail handler is nil. +const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler. If you're using Ginkgo then you probably forgot to put your assertion in an It(). Alternatively, you may have forgotten to register a fail handler with RegisterFailHandler() or RegisterTestingT(). Depending on your vendoring solution you may be inadvertently importing gomega and subpackages (e.g. ghhtp, gexec,...) from different locations. ` -var globalFailWrapper *types.GomegaFailWrapper - -var defaultEventuallyTimeout = time.Second -var defaultEventuallyPollingInterval = 10 * time.Millisecond -var defaultConsistentlyDuration = 100 * time.Millisecond -var defaultConsistentlyPollingInterval = 10 * time.Millisecond - -// RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails -// the fail handler passed into RegisterFailHandler is called. -func RegisterFailHandler(handler types.GomegaFailHandler) { - RegisterFailHandlerWithT(testingtsupport.EmptyTWithHelper{}, handler) -} +// Gomega describes the essential Gomega DSL. This interface allows libraries +// to abstract between the standard package-level function implementations +// and alternatives like *WithT. +// +// The types in the top-level DSL have gotten a bit messy due to earlier depracations that avoid stuttering +// and due to an accidental use of a concrete type (*WithT) in an earlier release. +// +// As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object +// however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant) +// is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure +// that declarations of *WithT in existing code are not broken by the upgrade to 1.15. +type Gomega = types.Gomega -// RegisterFailHandlerWithT ensures that the given types.TWithHelper and fail handler -// are used globally. -func RegisterFailHandlerWithT(t types.TWithHelper, handler types.GomegaFailHandler) { - if handler == nil { - globalFailWrapper = nil - return - } +// DefaultGomega supplies the standard package-level implementation +var Default = Gomega(internal.NewGomega(internal.FetchDefaultDurationBundle())) - globalFailWrapper = &types.GomegaFailWrapper{ - Fail: handler, - TWithHelper: t, - } +// NewGomega returns an instance of Gomega wired into the passed-in fail handler. +// You generally don't need to use this when using Ginkgo - RegisterFailHandler will wire up the global gomega +// However creating a NewGomega with a custom fail handler can be useful in contexts where you want to use Gomega's +// rich ecosystem of matchers without causing a test to fail. For example, to aggregate a series of potential failures +// or for use in a non-test setting. +func NewGomega(fail types.GomegaFailHandler) Gomega { + return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithFailHandler(fail) } -// RegisterTestingT connects Gomega to Golang's XUnit style -// Testing.T tests. It is now deprecated and you should use NewWithT() instead. +// WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage +// Gomega's rich ecosystem of matchers in standard `testing` test suites. // -// Legacy Documentation: +// Use `NewWithT` to instantiate a `WithT` // -// You'll need to call this at the top of each XUnit style test: +// As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object +// however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant) +// is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure +// that declarations of *WithT in existing code are not broken by the upgrade to 1.15. +type WithT = internal.Gomega + +// GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter. +type GomegaWithT = WithT + +// NewWithT takes a *testing.T and returngs a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with +// Gomega's rich ecosystem of matchers in standard `testing` test suits. // // func TestFarmHasCow(t *testing.T) { -// RegisterTestingT(t) +// g := gomega.NewWithT(t) // // f := farm.New([]string{"Cow", "Horse"}) -// Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") -// } -// -// Note that this *testing.T is registered *globally* by Gomega (this is why you don't have to -// pass `t` down to the matcher itself). This means that you cannot run the XUnit style tests -// in parallel as the global fail handler cannot point to more than one testing.T at a time. -// -// NewWithT() does not have this limitation -// -// (As an aside: Ginkgo gets around this limitation by running parallel tests in different *processes*). +// g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") +// } +func NewWithT(t types.GomegaTestingT) *WithT { + return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithT(t) +} + +// NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter. +var NewGomegaWithT = NewWithT + +// RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails +// the fail handler passed into RegisterFailHandler is called. +func RegisterFailHandler(fail types.GomegaFailHandler) { + Default.(*internal.Gomega).ConfigureWithFailHandler(fail) +} + +// RegisterFailHandlerWithT is deprecated and will be removed in a future release. +// users should use RegisterFailHandler, or RegisterTestingT +func RegisterFailHandlerWithT(_ types.GomegaTestingT, fail types.GomegaFailHandler) { + fmt.Println("RegisterFailHandlerWithT is deprecated. Please use RegisterFailHandler or RegisterTestingT instead.") + Default.(*internal.Gomega).ConfigureWithFailHandler(fail) +} + +// RegisterTestingT connects Gomega to Golang's XUnit style +// Testing.T tests. It is now deprecated and you should use NewWithT() instead to get a fresh instance of Gomega for each test. func RegisterTestingT(t types.GomegaTestingT) { - tWithHelper, hasHelper := t.(types.TWithHelper) - if !hasHelper { - RegisterFailHandler(testingtsupport.BuildTestingTGomegaFailWrapper(t).Fail) - return - } - RegisterFailHandlerWithT(tWithHelper, testingtsupport.BuildTestingTGomegaFailWrapper(t).Fail) + Default.(*internal.Gomega).ConfigureWithT(t) } // InterceptGomegaFailures runs a given callback and returns an array of @@ -98,13 +112,13 @@ func RegisterTestingT(t types.GomegaTestingT) { // This is most useful when testing custom matchers, but can also be used to check // on a value using a Gomega assertion without causing a test failure. func InterceptGomegaFailures(f func()) []string { - originalHandler := globalFailWrapper.Fail + originalHandler := Default.(*internal.Gomega).Fail failures := []string{} - RegisterFailHandler(func(message string, callerSkip ...int) { + Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) { failures = append(failures, message) - }) + } defer func() { - RegisterFailHandler(originalHandler) + Default.(*internal.Gomega).Fail = originalHandler }() f() return failures @@ -117,14 +131,14 @@ func InterceptGomegaFailures(f func()) []string { // does not register a failure with the FailHandler registered via RegisterFailHandler - it is up // to the user to decide what to do with the returned error func InterceptGomegaFailure(f func()) (err error) { - originalHandler := globalFailWrapper.Fail - RegisterFailHandler(func(message string, callerSkip ...int) { + originalHandler := Default.(*internal.Gomega).Fail + Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) { err = errors.New(message) panic("stop execution") - }) + } defer func() { - RegisterFailHandler(originalHandler) + Default.(*internal.Gomega).Fail = originalHandler if e := recover(); e != nil { if err == nil { panic(e) @@ -136,6 +150,12 @@ func InterceptGomegaFailure(f func()) (err error) { return err } +func ensureDefaultGomegaIsConfigured() { + if !Default.(*internal.Gomega).IsConfigured() { + panic(nilGomegaPanic) + } +} + // Ω wraps an actual value allowing assertions to be made on it: // Ω("foo").Should(Equal("foo")) // @@ -154,7 +174,8 @@ func InterceptGomegaFailure(f func()) (err error) { // // Ω and Expect are identical func Ω(actual interface{}, extra ...interface{}) Assertion { - return ExpectWithOffset(0, actual, extra...) + ensureDefaultGomegaIsConfigured() + return Default.Ω(actual, extra...) } // Expect wraps an actual value allowing assertions to be made on it: @@ -175,7 +196,8 @@ func Ω(actual interface{}, extra ...interface{}) Assertion { // // Expect and Ω are identical func Expect(actual interface{}, extra ...interface{}) Assertion { - return ExpectWithOffset(0, actual, extra...) + ensureDefaultGomegaIsConfigured() + return Default.Expect(actual, extra...) } // ExpectWithOffset wraps an actual value allowing assertions to be made on it: @@ -188,10 +210,8 @@ func Expect(actual interface{}, extra ...interface{}) Assertion { // error message to refer to the calling line in the test (as opposed to the line in the helper function) // set the first argument of `ExpectWithOffset` appropriately. func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion { - if globalFailWrapper == nil { - panic(nilFailHandlerPanic) - } - return assertion.New(actual, globalFailWrapper, offset, extra...) + ensureDefaultGomegaIsConfigured() + return Default.ExpectWithOffset(offset, actual, extra...) } // Eventually wraps an actual value allowing assertions to be made on it. @@ -259,25 +279,16 @@ func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Asse // // Eventually's default timeout is 1 second, and its default polling interval is 10ms func Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion { - return EventuallyWithOffset(0, actual, intervals...) + ensureDefaultGomegaIsConfigured() + return Default.Eventually(actual, intervals...) } // EventuallyWithOffset operates like Eventually but takes an additional // initial argument to indicate an offset in the call stack. This is useful when building helper // functions that contain matchers. To learn more, read about `ExpectWithOffset`. func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { - if globalFailWrapper == nil { - panic(nilFailHandlerPanic) - } - timeoutInterval := defaultEventuallyTimeout - pollingInterval := defaultEventuallyPollingInterval - if len(intervals) > 0 { - timeoutInterval = toDuration(intervals[0]) - } - if len(intervals) > 1 { - pollingInterval = toDuration(intervals[1]) - } - return asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, actual, globalFailWrapper, timeoutInterval, pollingInterval, offset) + ensureDefaultGomegaIsConfigured() + return Default.EventuallyWithOffset(offset, actual, intervals...) } // Consistently wraps an actual value allowing assertions to be made on it. @@ -309,45 +320,36 @@ func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface // // Consistently's default duration is 100ms, and its default polling interval is 10ms func Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion { - return ConsistentlyWithOffset(0, actual, intervals...) + ensureDefaultGomegaIsConfigured() + return Default.Consistently(actual, intervals...) } // ConsistentlyWithOffset operates like Consistently but takes an additional // initial argument to indicate an offset in the call stack. This is useful when building helper // functions that contain matchers. To learn more, read about `ExpectWithOffset`. func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { - if globalFailWrapper == nil { - panic(nilFailHandlerPanic) - } - timeoutInterval := defaultConsistentlyDuration - pollingInterval := defaultConsistentlyPollingInterval - if len(intervals) > 0 { - timeoutInterval = toDuration(intervals[0]) - } - if len(intervals) > 1 { - pollingInterval = toDuration(intervals[1]) - } - return asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, actual, globalFailWrapper, timeoutInterval, pollingInterval, offset) + ensureDefaultGomegaIsConfigured() + return Default.ConsistentlyWithOffset(offset, actual, intervals...) } // SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses. func SetDefaultEventuallyTimeout(t time.Duration) { - defaultEventuallyTimeout = t + Default.SetDefaultEventuallyTimeout(t) } // SetDefaultEventuallyPollingInterval sets the default polling interval for Eventually. func SetDefaultEventuallyPollingInterval(t time.Duration) { - defaultEventuallyPollingInterval = t + Default.SetDefaultEventuallyPollingInterval(t) } // SetDefaultConsistentlyDuration sets the default duration for Consistently. Consistently will verify that your condition is satisfied for this long. func SetDefaultConsistentlyDuration(t time.Duration) { - defaultConsistentlyDuration = t + Default.SetDefaultConsistentlyDuration(t) } // SetDefaultConsistentlyPollingInterval sets the default polling interval for Consistently. func SetDefaultConsistentlyPollingInterval(t time.Duration) { - defaultConsistentlyPollingInterval = t + Default.SetDefaultConsistentlyPollingInterval(t) } // AsyncAssertion is returned by Eventually and Consistently and polls the actual value passed into Eventually against @@ -365,13 +367,10 @@ func SetDefaultConsistentlyPollingInterval(t time.Duration) { // // Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.") // Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." }) -type AsyncAssertion interface { - Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool - ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool -} +type AsyncAssertion = types.AsyncAssertion // GomegaAsyncAssertion is deprecated in favor of AsyncAssertion, which does not stutter. -type GomegaAsyncAssertion = AsyncAssertion +type GomegaAsyncAssertion = types.AsyncAssertion // Assertion is returned by Ω and Expect and compares the actual value to the matcher // passed to the Should/ShouldNot and To/ToNot/NotTo methods. @@ -390,149 +389,10 @@ type GomegaAsyncAssertion = AsyncAssertion // Example: // // Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm) -type Assertion interface { - Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool - ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool - - To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool - ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool - NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool -} +type Assertion = types.Assertion // GomegaAssertion is deprecated in favor of Assertion, which does not stutter. -type GomegaAssertion = Assertion +type GomegaAssertion = types.Assertion // OmegaMatcher is deprecated in favor of the better-named and better-organized types.GomegaMatcher but sticks around to support existing code that uses it -type OmegaMatcher types.GomegaMatcher - -// WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage -// Gomega's rich ecosystem of matchers in standard `testing` test suites. -// -// Use `NewWithT` to instantiate a `WithT` -type WithT struct { - failWrapper *types.GomegaFailWrapper -} - -// GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter. -type GomegaWithT = WithT - -// NewWithT takes a *testing.T and returngs a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with -// Gomega's rich ecosystem of matchers in standard `testing` test suits. -// -// func TestFarmHasCow(t *testing.T) { -// g := gomega.NewWithT(t) -// -// f := farm.New([]string{"Cow", "Horse"}) -// g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") -// } -func NewWithT(t types.GomegaTestingT) *WithT { - return &WithT{ - failWrapper: testingtsupport.BuildTestingTGomegaFailWrapper(t), - } -} - -// NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter. -func NewGomegaWithT(t types.GomegaTestingT) *GomegaWithT { - return NewWithT(t) -} - -// ExpectWithOffset is used to make assertions. See documentation for ExpectWithOffset. -func (g *WithT) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion { - return assertion.New(actual, g.failWrapper, offset, extra...) -} - -// EventuallyWithOffset is used to make asynchronous assertions. See documentation for EventuallyWithOffset. -func (g *WithT) EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { - timeoutInterval := defaultEventuallyTimeout - pollingInterval := defaultEventuallyPollingInterval - if len(intervals) > 0 { - timeoutInterval = toDuration(intervals[0]) - } - if len(intervals) > 1 { - pollingInterval = toDuration(intervals[1]) - } - return asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, actual, g.failWrapper, timeoutInterval, pollingInterval, offset) -} - -// ConsistentlyWithOffset is used to make asynchronous assertions. See documentation for ConsistentlyWithOffset. -func (g *WithT) ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion { - timeoutInterval := defaultConsistentlyDuration - pollingInterval := defaultConsistentlyPollingInterval - if len(intervals) > 0 { - timeoutInterval = toDuration(intervals[0]) - } - if len(intervals) > 1 { - pollingInterval = toDuration(intervals[1]) - } - return asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, actual, g.failWrapper, timeoutInterval, pollingInterval, offset) -} - -// Expect is used to make assertions. See documentation for Expect. -func (g *WithT) Expect(actual interface{}, extra ...interface{}) Assertion { - return g.ExpectWithOffset(0, actual, extra...) -} - -// Eventually is used to make asynchronous assertions. See documentation for Eventually. -func (g *WithT) Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion { - return g.EventuallyWithOffset(0, actual, intervals...) -} - -// Consistently is used to make asynchronous assertions. See documentation for Consistently. -func (g *WithT) Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion { - return g.ConsistentlyWithOffset(0, actual, intervals...) -} - -func toDuration(input interface{}) time.Duration { - duration, ok := input.(time.Duration) - if ok { - return duration - } - - value := reflect.ValueOf(input) - kind := reflect.TypeOf(input).Kind() - - if reflect.Int <= kind && kind <= reflect.Int64 { - return time.Duration(value.Int()) * time.Second - } else if reflect.Uint <= kind && kind <= reflect.Uint64 { - return time.Duration(value.Uint()) * time.Second - } else if reflect.Float32 <= kind && kind <= reflect.Float64 { - return time.Duration(value.Float() * float64(time.Second)) - } else if reflect.String == kind { - duration, err := time.ParseDuration(value.String()) - if err != nil { - panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input)) - } - return duration - } - - panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input)) -} - -// Gomega describes the essential Gomega DSL. This interface allows libraries -// to abstract between the standard package-level function implementations -// and alternatives like *WithT. -type Gomega interface { - Expect(actual interface{}, extra ...interface{}) Assertion - Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion - Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion -} - -type globalFailHandlerGomega struct{} - -// DefaultGomega supplies the standard package-level implementation -var Default Gomega = globalFailHandlerGomega{} - -// Expect is used to make assertions. See documentation for Expect. -func (globalFailHandlerGomega) Expect(actual interface{}, extra ...interface{}) Assertion { - return Expect(actual, extra...) -} - -// Eventually is used to make asynchronous assertions. See documentation for Eventually. -func (globalFailHandlerGomega) Eventually(actual interface{}, extra ...interface{}) AsyncAssertion { - return Eventually(actual, extra...) -} - -// Consistently is used to make asynchronous assertions. See documentation for Consistently. -func (globalFailHandlerGomega) Consistently(actual interface{}, extra ...interface{}) AsyncAssertion { - return Consistently(actual, extra...) -} +type OmegaMatcher = types.GomegaMatcher diff --git a/internal/assertion/assertion.go b/internal/assertion.go similarity index 80% rename from internal/assertion/assertion.go rename to internal/assertion.go index a248298f4..36b0e8345 100644 --- a/internal/assertion/assertion.go +++ b/internal/assertion.go @@ -1,4 +1,4 @@ -package assertion +package internal import ( "fmt" @@ -9,42 +9,42 @@ import ( type Assertion struct { actualInput interface{} - failWrapper *types.GomegaFailWrapper offset int extra []interface{} + g *Gomega } -func New(actualInput interface{}, failWrapper *types.GomegaFailWrapper, offset int, extra ...interface{}) *Assertion { +func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion { return &Assertion{ actualInput: actualInput, - failWrapper: failWrapper, offset: offset, extra: extra, + g: g, } } func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) } func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) } func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...) } func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) } func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...) } @@ -62,10 +62,10 @@ func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { matches, err := matcher.Match(assertion.actualInput) - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() if err != nil { description := assertion.buildDescription(optionalDescription...) - assertion.failWrapper.Fail(description+err.Error(), 2+assertion.offset) + assertion.g.Fail(description+err.Error(), 2+assertion.offset) return false } if matches != desiredMatch { @@ -76,7 +76,7 @@ func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool message = matcher.NegatedFailureMessage(assertion.actualInput) } description := assertion.buildDescription(optionalDescription...) - assertion.failWrapper.Fail(description+message, 2+assertion.offset) + assertion.g.Fail(description+message, 2+assertion.offset) return false } @@ -90,8 +90,8 @@ func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool { } description := assertion.buildDescription(optionalDescription...) - assertion.failWrapper.TWithHelper.Helper() - assertion.failWrapper.Fail(description+message, 2+assertion.offset) + assertion.g.THelper() + assertion.g.Fail(description+message, 2+assertion.offset) return false } diff --git a/internal/assertion/assertion_suite_test.go b/internal/assertion/assertion_suite_test.go deleted file mode 100644 index dae47a48b..000000000 --- a/internal/assertion/assertion_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package assertion_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestAssertion(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Assertion Suite") -} diff --git a/internal/assertion/assertion_test.go b/internal/assertion/assertion_test.go deleted file mode 100644 index 146b11e9b..000000000 --- a/internal/assertion/assertion_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package assertion_test - -import ( - "errors" - - "github.com/onsi/gomega/internal/testingtsupport" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/internal/assertion" - "github.com/onsi/gomega/internal/fakematcher" - "github.com/onsi/gomega/types" -) - -var _ = Describe("Assertion", func() { - var ( - a *assertion.Assertion - failureMessage string - failureCallerSkip int - matcher *fakematcher.FakeMatcher - ) - - input := "The thing I'm testing" - - var fakeFailWrapper = &types.GomegaFailWrapper{ - Fail: func(message string, callerSkip ...int) { - failureMessage = message - if len(callerSkip) == 1 { - failureCallerSkip = callerSkip[0] - } - }, - TWithHelper: testingtsupport.EmptyTWithHelper{}, - } - - BeforeEach(func() { - matcher = &fakematcher.FakeMatcher{} - failureMessage = "" - failureCallerSkip = 0 - a = assertion.New(input, fakeFailWrapper, 1) - }) - - When("called", func() { - It("should pass the provided input value to the matcher", func() { - a.Should(matcher) - - Expect(matcher.ReceivedActual).Should(Equal(input)) - matcher.ReceivedActual = "" - - a.ShouldNot(matcher) - - Expect(matcher.ReceivedActual).Should(Equal(input)) - matcher.ReceivedActual = "" - - a.To(matcher) - - Expect(matcher.ReceivedActual).Should(Equal(input)) - matcher.ReceivedActual = "" - - a.ToNot(matcher) - - Expect(matcher.ReceivedActual).Should(Equal(input)) - matcher.ReceivedActual = "" - - a.NotTo(matcher) - - Expect(matcher.ReceivedActual).Should(Equal(input)) - }) - }) - - When("the matcher succeeds", func() { - BeforeEach(func() { - matcher.MatchesToReturn = true - matcher.ErrToReturn = nil - }) - - Context("and a positive assertion is being made", func() { - It("should not call the failure callback", func() { - a.Should(matcher) - Expect(failureMessage).Should(Equal("")) - }) - - It("should be true", func() { - Expect(a.Should(matcher)).Should(BeTrue()) - }) - }) - - Context("and a negative assertion is being made", func() { - It("should call the failure callback", func() { - a.ShouldNot(matcher) - Expect(failureMessage).Should(Equal("negative: The thing I'm testing")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - - It("should be false", func() { - Expect(a.ShouldNot(matcher)).Should(BeFalse()) - }) - }) - - Context("and the optional description is a function", func() { - It("should not evaluate that function", func() { - evaluated := false - a.Should(matcher, func() string { - evaluated = true - return "A description" - }) - Expect(evaluated).Should(BeFalse()) - }) - }) - }) - - When("the matcher fails", func() { - BeforeEach(func() { - matcher.MatchesToReturn = false - matcher.ErrToReturn = nil - }) - - Context("and a positive assertion is being made", func() { - It("should call the failure callback", func() { - a.Should(matcher) - Expect(failureMessage).Should(Equal("positive: The thing I'm testing")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - - It("should be false", func() { - Expect(a.Should(matcher)).Should(BeFalse()) - }) - }) - - Context("and a negative assertion is being made", func() { - It("should not call the failure callback", func() { - a.ShouldNot(matcher) - Expect(failureMessage).Should(Equal("")) - }) - - It("should be true", func() { - Expect(a.ShouldNot(matcher)).Should(BeTrue()) - }) - }) - }) - - Context("When reporting a failure", func() { - BeforeEach(func() { - matcher.MatchesToReturn = false - matcher.ErrToReturn = nil - }) - - Context("and there is an optional description", func() { - It("should append the description to the failure message", func() { - a.Should(matcher, "A description") - Expect(failureMessage).Should(Equal("A description\npositive: The thing I'm testing")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - - Context("and there are multiple arguments to the optional description", func() { - It("should append the formatted description to the failure message", func() { - a.Should(matcher, "A description of [%d]", 3) - Expect(failureMessage).Should(Equal("A description of [3]\npositive: The thing I'm testing")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - - Context("and the optional description is a function", func() { - It("should append the description to the failure message", func() { - a.Should(matcher, func() string { return "A description" }) - Expect(failureMessage).Should(Equal("A description\npositive: The thing I'm testing")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - }) - - Context("When the matcher returns an error", func() { - BeforeEach(func() { - matcher.ErrToReturn = errors.New("Kaboom!") - }) - - Context("and a positive assertion is being made", func() { - It("should call the failure callback", func() { - matcher.MatchesToReturn = true - a.Should(matcher) - Expect(failureMessage).Should(Equal("Kaboom!")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - - Context("and a negative assertion is being made", func() { - It("should call the failure callback", func() { - matcher.MatchesToReturn = false - a.ShouldNot(matcher) - Expect(failureMessage).Should(Equal("Kaboom!")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - - It("should always be false", func() { - Expect(a.Should(matcher)).Should(BeFalse()) - Expect(a.ShouldNot(matcher)).Should(BeFalse()) - }) - }) - - When("there are extra parameters", func() { - It("(a simple example)", func() { - Expect(func() (string, int, error) { - return "foo", 0, nil - }()).Should(Equal("foo")) - }) - - When("the parameters are all nil or zero", func() { - It("should invoke the matcher", func() { - matcher.MatchesToReturn = true - matcher.ErrToReturn = nil - - var typedNil []string - a = assertion.New(input, fakeFailWrapper, 1, 0, nil, typedNil) - - result := a.Should(matcher) - Expect(result).Should(BeTrue()) - Expect(matcher.ReceivedActual).Should(Equal(input)) - - Expect(failureMessage).Should(BeZero()) - }) - }) - - When("any of the parameters are not nil or zero", func() { - It("should call the failure callback", func() { - matcher.MatchesToReturn = false - matcher.ErrToReturn = nil - - a = assertion.New(input, fakeFailWrapper, 1, errors.New("foo")) - result := a.Should(matcher) - Expect(result).Should(BeFalse()) - Expect(matcher.ReceivedActual).Should(BeZero(), "The matcher doesn't even get called") - Expect(failureMessage).Should(ContainSubstring("foo")) - failureMessage = "" - - a = assertion.New(input, fakeFailWrapper, 1, nil, 1) - result = a.ShouldNot(matcher) - Expect(result).Should(BeFalse()) - Expect(failureMessage).Should(ContainSubstring("1")) - failureMessage = "" - - a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"}) - result = a.To(matcher) - Expect(result).Should(BeFalse()) - Expect(failureMessage).Should(ContainSubstring("foo")) - failureMessage = "" - - a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"}) - result = a.ToNot(matcher) - Expect(result).Should(BeFalse()) - Expect(failureMessage).Should(ContainSubstring("foo")) - failureMessage = "" - - a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"}) - result = a.NotTo(matcher) - Expect(result).Should(BeFalse()) - Expect(failureMessage).Should(ContainSubstring("foo")) - Expect(failureCallerSkip).Should(Equal(3)) - }) - }) - }) - - Context("Making an assertion without a registered fail handler", func() { - It("should panic", func() { - defer func() { - e := recover() - RegisterFailHandler(Fail) - if e == nil { - Fail("expected a panic to have occurred") - } - }() - - RegisterFailHandler(nil) - Expect(true).Should(BeTrue()) - }) - }) -}) diff --git a/internal/assertion_test.go b/internal/assertion_test.go new file mode 100644 index 000000000..a02aab0d4 --- /dev/null +++ b/internal/assertion_test.go @@ -0,0 +1,147 @@ +package internal_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Making Synchronous Assertions", func() { + var SHOULD_MATCH = true + var SHOULD_NOT_MATCH = false + var IT_PASSES = true + var IT_FAILS = false + + Extras := func(extras ...interface{}) []interface{} { + return extras + } + + OptionalDescription := func(optionalDescription ...interface{}) []interface{} { + return optionalDescription + } + + DescribeTable( + "the various cases", + func(actual interface{}, extras []interface{}, optionalDescription []interface{}, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) { + if isPositiveAssertion { + ig := NewInstrumentedGomega() + returnValue := ig.G.Expect(actual, extras...).To(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{2})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To")) + + ig = NewInstrumentedGomega() + returnValue = ig.G.ExpectWithOffset(3, actual, extras...).To(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{5})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To")) + + ig = NewInstrumentedGomega() + returnValue = ig.G.Ω(actual, extras...).Should(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{2})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).Should")) + } else { + ig := NewInstrumentedGomega() + returnValue := ig.G.Expect(actual, extras...).ToNot(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{2})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ToNot")) + + ig = NewInstrumentedGomega() + returnValue = ig.G.Expect(actual, extras...).NotTo(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{2})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo")) + + ig = NewInstrumentedGomega() + returnValue = ig.G.ExpectWithOffset(3, actual, extras...).NotTo(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{5})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo")) + + ig = NewInstrumentedGomega() + returnValue = ig.G.Ω(actual, extras...).ShouldNot(SpecMatch(), optionalDescription...) + Expect(returnValue).To(Equal(expectedReturnValue)) + Expect(ig.FailureMessage).To(Equal(expectedFailureMessage)) + if expectedFailureMessage != "" { + Expect(ig.FailureSkip).To(Equal([]int{2})) + } + Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ShouldNot")) + } + }, + Entry( + "when the matcher matches and a positive assertion is being made", + MATCH, Extras(), OptionalDescription(), + SHOULD_MATCH, "", IT_PASSES, + ), + Entry( + "when the matcher matches and a negative assertion is being made", + MATCH, Extras(), OptionalDescription(), + SHOULD_NOT_MATCH, "negative: match", IT_FAILS, + ), + Entry( + "when the matcher does not match and a positive assertion is being made", + NO_MATCH, Extras(), OptionalDescription(), + SHOULD_MATCH, "positive: no match", IT_FAILS, + ), + Entry( + "when the matcher does not match and a negative assertion is being made", + NO_MATCH, Extras(), OptionalDescription(), + SHOULD_NOT_MATCH, "", IT_PASSES, + ), + Entry( + "when the matcher returns an error and a positive assertion is being made", + ERR_MATCH, Extras(), OptionalDescription(), + SHOULD_MATCH, "spec matcher error", IT_FAILS, + ), + Entry( + "when the matcher returns an error and a negative assertion is being made", + ERR_MATCH, Extras(), OptionalDescription(), + SHOULD_NOT_MATCH, "spec matcher error", IT_FAILS, + ), + Entry( + "when a failure occurs and there is a single optional description", + NO_MATCH, Extras(), OptionalDescription("a description"), + SHOULD_MATCH, "a description\npositive: no match", IT_FAILS, + ), + Entry( + "when a failure occurs and there are multiple optional descriptions", + NO_MATCH, Extras(), OptionalDescription("a description of [%d]", 3), + SHOULD_MATCH, "a description of [3]\npositive: no match", IT_FAILS, + ), + Entry( + "when a failure occurs and the optional description is a function", + NO_MATCH, Extras(), OptionalDescription(func() string { return "a description" }), + SHOULD_MATCH, "a description\npositive: no match", IT_FAILS, + ), + Entry( + "when the matcher matches and zero-valued extra parameters are included, it passes", + MATCH, Extras(0, "", struct{ Foo string }{}, nil), OptionalDescription(), + SHOULD_MATCH, "", IT_PASSES, + ), + Entry( + "when the matcher matches but a non-zero-valued extra parameter is included, it fails", + MATCH, Extras(1, "bam", struct{ Foo string }{Foo: "foo"}, nil), OptionalDescription(), + SHOULD_MATCH, "Unexpected non-nil/non-zero extra argument at index 1:\n\t: 1", IT_FAILS, + ), + ) +}) diff --git a/internal/asyncassertion/async_assertion.go b/internal/async_assertion.go similarity index 51% rename from internal/asyncassertion/async_assertion.go rename to internal/async_assertion.go index 6aa02bc5d..ae20c14b8 100644 --- a/internal/asyncassertion/async_assertion.go +++ b/internal/async_assertion.go @@ -1,6 +1,4 @@ -// untested sections: 2 - -package asyncassertion +package internal import ( "errors" @@ -9,7 +7,6 @@ import ( "runtime" "time" - "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -21,39 +18,82 @@ const ( ) type AsyncAssertion struct { - asyncType AsyncAssertionType - actualInput interface{} + asyncType AsyncAssertionType + + actualIsFunc bool + actualValue interface{} + actualFunc func() ([]reflect.Value, error) + timeoutInterval time.Duration pollingInterval time.Duration - failWrapper *types.GomegaFailWrapper offset int + g *Gomega } -func New(asyncType AsyncAssertionType, actualInput interface{}, failWrapper *types.GomegaFailWrapper, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion { - actualType := reflect.TypeOf(actualInput) - if actualType.Kind() == reflect.Func { - if actualType.NumIn() != 0 { - panic("Expected a function with no arguments and zero or more return values.") - } - } - - return &AsyncAssertion{ +func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion { + out := &AsyncAssertion{ asyncType: asyncType, - actualInput: actualInput, - failWrapper: failWrapper, timeoutInterval: timeoutInterval, pollingInterval: pollingInterval, offset: offset, + g: g, + } + + switch actualType := reflect.TypeOf(actualInput); { + case actualType.Kind() != reflect.Func: + out.actualValue = actualInput + case actualType.NumIn() == 0 && actualType.NumOut() > 0: + out.actualIsFunc = true + out.actualFunc = func() ([]reflect.Value, error) { + return reflect.ValueOf(actualInput).Call([]reflect.Value{}), nil + } + case actualType.NumIn() == 1 && actualType.In(0).Implements(reflect.TypeOf((*types.Gomega)(nil)).Elem()): + out.actualIsFunc = true + out.actualFunc = func() (values []reflect.Value, err error) { + var assertionFailure error + assertionCapturingGomega := NewGomega(g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) { + skip := 0 + if len(callerSkip) > 0 { + skip = callerSkip[0] + } + _, file, line, _ := runtime.Caller(skip + 1) + assertionFailure = fmt.Errorf("Assertion in callback at %s:%d failed:\n%s", file, line, message) + panic("stop execution") + }) + + defer func() { + if actualType.NumOut() == 0 { + if assertionFailure == nil { + values = []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())} + } else { + values = []reflect.Value{reflect.ValueOf(assertionFailure)} + } + } else { + err = assertionFailure + } + if e := recover(); e != nil && assertionFailure == nil { + panic(e) + } + }() + + values = reflect.ValueOf(actualInput).Call([]reflect.Value{reflect.ValueOf(assertionCapturingGomega)}) + return + } + default: + msg := fmt.Sprintf("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes %d arguments and returns %d values.", actualType.NumIn(), actualType.NumOut()) + g.Fail(msg, offset+4) } + + return out } func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.match(matcher, true, optionalDescription...) } func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool { - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() return assertion.match(matcher, false, optionalDescription...) } @@ -69,74 +109,32 @@ func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interfa return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n" } -func (assertion *AsyncAssertion) actualInputIsAFunction() bool { - actualType := reflect.TypeOf(assertion.actualInput) - return actualType.Kind() == reflect.Func && actualType.NumIn() == 0 -} - func (assertion *AsyncAssertion) pollActual() (interface{}, error) { - if !assertion.actualInputIsAFunction() { - return assertion.actualInput, nil + if !assertion.actualIsFunc { + return assertion.actualValue, nil } - var capturedAssertionFailure string - var values []reflect.Value - - numOut := reflect.TypeOf(assertion.actualInput).NumOut() - func() { - originalHandler := assertion.failWrapper.Fail - assertion.failWrapper.Fail = func(message string, callerSkip ...int) { - skip := 0 - if len(callerSkip) > 0 { - skip = callerSkip[0] - } - _, file, line, _ := runtime.Caller(skip + 1) - capturedAssertionFailure = fmt.Sprintf("Assertion in callback at %s:%d failed:\n%s", file, line, message) - panic("stop execution") - } - - defer func() { - assertion.failWrapper.Fail = originalHandler - if e := recover(); e != nil && capturedAssertionFailure == "" { - panic(e) - } - }() - - values = reflect.ValueOf(assertion.actualInput).Call([]reflect.Value{}) - }() - - if capturedAssertionFailure != "" { - if numOut == 0 { - return errors.New(capturedAssertionFailure), nil - } else { - return nil, errors.New(capturedAssertionFailure) - } + values, err := assertion.actualFunc() + if err != nil { + return nil, err } - - if numOut > 0 { - extras := []interface{}{} - for _, value := range values[1:] { - extras = append(extras, value.Interface()) - } - - success, message := vetExtras(extras) - - if !success { - return nil, errors.New(message) - } - - return values[0].Interface(), nil + extras := []interface{}{} + for _, value := range values[1:] { + extras = append(extras, value.Interface()) + } + success, message := vetExtras(extras) + if !success { + return nil, errors.New(message) } - return nil, nil + return values[0].Interface(), nil } func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool { - if assertion.actualInputIsAFunction() { + if assertion.actualIsFunc { return true } - - return oraclematcher.MatchMayChangeInTheFuture(matcher, value) + return types.MatchMayChangeInTheFuture(matcher, value) } func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { @@ -152,7 +150,7 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch matches, err = matcher.Match(value) } - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() fail := func(preamble string) { errMsg := "" @@ -166,9 +164,9 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch message = matcher.NegatedFailureMessage(value) } } - assertion.failWrapper.TWithHelper.Helper() + assertion.g.THelper() description := assertion.buildDescription(optionalDescription...) - assertion.failWrapper.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset) + assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset) } if assertion.asyncType == AsyncAssertionTypeEventually { @@ -220,16 +218,3 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch return false } - -func vetExtras(extras []interface{}) (bool, string) { - for i, extra := range extras { - if extra != nil { - zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface() - if !reflect.DeepEqual(zeroValue, extra) { - message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra) - return false, message - } - } - } - return true, "" -} diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go new file mode 100644 index 000000000..c63751cf3 --- /dev/null +++ b/internal/async_assertion_test.go @@ -0,0 +1,717 @@ +package internal_test + +import ( + "errors" + "runtime" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Asynchronous Assertions", func() { + var ig *InstrumentedGomega + BeforeEach(func() { + ig = NewInstrumentedGomega() + }) + + Describe("Basic Eventually support", func() { + Context("the positive case", func() { + It("polls the function and matcher until a match occurs", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 5 { + return MATCH + } + return NO_MATCH + }).Should(SpecMatch()) + Ω(counter).Should(Equal(6)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("continues polling even if the matcher errors", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 5 { + return MATCH + } + return ERR_MATCH + }).Should(SpecMatch()) + Ω(counter).Should(Equal(6)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("times out eventually if the assertion doesn't match in time", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 100 { + return MATCH + } + return NO_MATCH + }, "200ms", "20ms").Should(SpecMatch()) + Ω(counter).Should(BeNumerically(">", 2)) + Ω(counter).Should(BeNumerically("<", 20)) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match")) + Ω(ig.FailureSkip).Should(Equal([]int{3})) + }) + }) + + Context("the negative case", func() { + It("polls the function and matcher until a match does not occur", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 5 { + return NO_MATCH + } + return MATCH + }).ShouldNot(SpecMatch()) + Ω(counter).Should(Equal(6)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("continues polling when the matcher errors - an error does not count as a successful non-match", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 5 { + return NO_MATCH + } + return ERR_MATCH + }).ShouldNot(SpecMatch()) + Ω(counter).Should(Equal(6)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("times out eventually if the assertion doesn't match in time", func() { + counter := 0 + ig.G.Eventually(func() string { + counter++ + if counter > 100 { + return NO_MATCH + } + return MATCH + }, "200ms", "20ms").ShouldNot(SpecMatch()) + Ω(counter).Should(BeNumerically(">", 2)) + Ω(counter).Should(BeNumerically("<", 20)) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ig.FailureMessage).Should(ContainSubstring("negative: match")) + Ω(ig.FailureSkip).Should(Equal([]int{3})) + }) + }) + + Context("when a failure occurs", func() { + It("registers the appropriate helper functions", func() { + ig.G.Eventually(NO_MATCH, "50ms", "10ms").Should(SpecMatch()) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match")) + Ω(ig.FailureSkip).Should(Equal([]int{3})) + Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).Should")) + Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).match")) + }) + + It("renders the matcher's error if an error occured", func() { + ig.G.Eventually(ERR_MATCH, "50ms", "10ms").Should(SpecMatch()) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + }) + + It("renders the optional description", func() { + ig.G.Eventually(NO_MATCH, "50ms", "10ms").Should(SpecMatch(), "boop") + Ω(ig.FailureMessage).Should(ContainSubstring("boop")) + }) + + It("formats and renders the optional description when there are multiple arguments", func() { + ig.G.Eventually(NO_MATCH, "50ms", "10ms").Should(SpecMatch(), "boop %d", 17) + Ω(ig.FailureMessage).Should(ContainSubstring("boop 17")) + }) + + It("calls the optional description if it is a function", func() { + ig.G.Eventually(NO_MATCH, "50ms", "10ms").Should(SpecMatch(), func() string { return "boop" }) + Ω(ig.FailureMessage).Should(ContainSubstring("boop")) + }) + }) + }) + + Describe("Basic Consistently support", func() { + Context("the positive case", func() { + It("polls the function and matcher ensuring a match occurs consistently", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + return MATCH + }, "50ms", "10ms").Should(SpecMatch()) + Ω(counter).Should(BeNumerically(">", 1)) + Ω(counter).Should(BeNumerically("<", 7)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("fails if the matcher ever errors", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + if counter == 3 { + return ERR_MATCH + } + return MATCH + }, "50ms", "10ms").Should(SpecMatch()) + Ω(counter).Should(Equal(3)) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + }) + + It("fails if the matcher doesn't match at any point", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + if counter == 3 { + return NO_MATCH + } + return MATCH + }, "50ms", "10ms").Should(SpecMatch()) + Ω(counter).Should(Equal(3)) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match")) + }) + }) + + Context("the negative case", func() { + It("polls the function and matcher ensuring a match never occurs", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + return NO_MATCH + }, "50ms", "10ms").ShouldNot(SpecMatch()) + Ω(counter).Should(BeNumerically(">", 1)) + Ω(counter).Should(BeNumerically("<", 7)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("fails if the matcher ever errors", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + if counter == 3 { + return ERR_MATCH + } + return NO_MATCH + }, "50ms", "10ms").ShouldNot(SpecMatch()) + Ω(counter).Should(Equal(3)) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + }) + + It("fails if the matcher matches at any point", func() { + counter := 0 + ig.G.Consistently(func() string { + counter++ + if counter == 3 { + return MATCH + } + return NO_MATCH + }, "50ms", "10ms").ShouldNot(SpecMatch()) + Ω(counter).Should(Equal(3)) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("negative: match")) + }) + }) + + Context("when a failure occurs", func() { + It("registers the appropriate helper functions", func() { + ig.G.Consistently(NO_MATCH).Should(SpecMatch()) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match")) + Ω(ig.FailureSkip).Should(Equal([]int{3})) + Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).Should")) + Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).match")) + }) + + It("renders the matcher's error if an error occured", func() { + ig.G.Consistently(ERR_MATCH).Should(SpecMatch()) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error")) + }) + + It("renders the optional description", func() { + ig.G.Consistently(NO_MATCH).Should(SpecMatch(), "boop") + Ω(ig.FailureMessage).Should(ContainSubstring("boop")) + }) + + It("formats and renders the optional description when there are multiple arguments", func() { + ig.G.Consistently(NO_MATCH).Should(SpecMatch(), "boop %d", 17) + Ω(ig.FailureMessage).Should(ContainSubstring("boop 17")) + }) + + It("calls the optional description if it is a function", func() { + ig.G.Consistently(NO_MATCH).Should(SpecMatch(), func() string { return "boop" }) + Ω(ig.FailureMessage).Should(ContainSubstring("boop")) + }) + }) + }) + + Describe("the passed-in actual", func() { + type Foo struct{ Bar string } + + Context("when passed a value", func() { + It("(eventually) continuously checks on the value until a match occurs", func() { + c := make(chan bool) + go func() { + time.Sleep(100 * time.Millisecond) + close(c) + }() + ig.G.Eventually(c, "1s", "10ms").Should(BeClosed()) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("(consistently) continuously checks on the value ensuring a match always occurs", func() { + c := make(chan bool) + close(c) + ig.G.Consistently(c, "50ms", "10ms").Should(BeClosed()) + Ω(ig.FailureMessage).Should(BeZero()) + }) + }) + + Context("when passed a function that takes no arguments and returns one value", func() { + It("(eventually) polls the function until the returned value satisfies the matcher", func() { + counter := 0 + ig.G.Eventually(func() int { + counter += 1 + return counter + }, "1s", "10ms").Should(BeNumerically(">", 5)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("(consistently) polls the function ensuring the returned value satisfies the matcher", func() { + counter := 0 + ig.G.Consistently(func() int { + counter += 1 + return counter + }, "50ms", "10ms").Should(BeNumerically("<", 20)) + Ω(counter).Should(BeNumerically(">", 2)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("works when the function returns nil", func() { + counter := 0 + ig.G.Eventually(func() error { + counter += 1 + if counter > 5 { + return nil + } + return errors.New("oops") + }, "1s", "10ms").Should(BeNil()) + Ω(ig.FailureMessage).Should(BeZero()) + }) + }) + + Context("when passed a function that takes no arguments and returns mutliple values", func() { + Context("with Eventually", func() { + It("polls the function until the first returned value satisfies the matcher _and_ all additional values are zero", func() { + counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi") + ig.G.Eventually(func() (int, string, Foo, error) { + switch counter += 1; counter { + case 2: + s = "" + case 3: + f = Foo{} + case 4: + err = nil + } + return counter, s, f, err + }, "1s", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(Equal(4)) + }) + + It("reports on the non-zero value if it times out", func() { + ig.G.Eventually(func() (int, string, Foo, error) { + return 1, "", Foo{Bar: "hi"}, nil + }, "30ms", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero extra argument at index 2:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"hi"}`)) + }) + + Context("when making a ShouldNot assertion", func() { + It("doesn't succeed until the matcher is (not) satisfied with the first returned value _and_ all additional values are zero", func() { + counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi") + ig.G.Eventually(func() (int, string, Foo, error) { + switch counter += 1; counter { + case 2: + s = "" + case 3: + f = Foo{} + case 4: + err = nil + } + return counter, s, f, err + }, "1s", "10ms").ShouldNot(BeNumerically("<", 0)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(Equal(4)) + }) + }) + }) + + Context("with Consistently", func() { + It("polls the function and succeeds if all the values are zero and the matcher is consistently satisfied", func() { + var err error + counter, s, f := 0, "", Foo{} + ig.G.Consistently(func() (int, string, Foo, error) { + counter += 1 + return counter, s, f, err + }, "50ms", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(BeNumerically(">", 2)) + }) + + It("polls the function and fails any of the values are non-zero", func() { + var err error + counter, s, f := 0, "", Foo{} + ig.G.Consistently(func() (int, string, Foo, error) { + counter += 1 + if counter == 3 { + f = Foo{Bar: "welp"} + } + return counter, s, f, err + }, "50ms", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero extra argument at index 2:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"welp"}`)) + Ω(counter).Should(Equal(3)) + }) + + Context("when making a ShouldNot assertion", func() { + It("succeeds if all additional values are zero", func() { + var err error + counter, s, f := 0, "", Foo{} + ig.G.Consistently(func() (int, string, Foo, error) { + counter += 1 + return counter, s, f, err + }, "50ms", "10ms").ShouldNot(BeNumerically(">", 100)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(BeNumerically(">", 2)) + }) + + It("fails if any additional values are ever non-zero", func() { + var err error + counter, s, f := 0, "", Foo{} + ig.G.Consistently(func() (int, string, Foo, error) { + counter += 1 + if counter == 3 { + s = "welp" + } + return counter, s, f, err + }, "50ms", "10ms").ShouldNot(BeNumerically(">", 100)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero extra argument at index 1:")) + Ω(ig.FailureMessage).Should(ContainSubstring(`: "welp"`)) + Ω(counter).Should(Equal(3)) + }) + }) + }) + }) + + Context("when passed a function that takes a Gomega argument and returns values", func() { + Context("with Eventually", func() { + It("passes in a Gomega and passes if the matcher matches, all extra values are zero, and there are no failed assertions", func() { + counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi") + ig.G.Eventually(func(g Gomega) (int, string, Foo, error) { + switch counter += 1; counter { + case 2: + s = "" + case 3: + f = Foo{} + case 4: + err = nil + } + if counter == 5 { + g.Expect(true).To(BeTrue()) + } else { + g.Expect(false).To(BeTrue()) + panic("boom") //never see since the expectation stops execution + } + return counter, s, f, err + }, "1s", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(Equal(5)) + }) + + It("times out if assertions in the function never succeed and reports on the error", func() { + _, file, line, _ := runtime.Caller(0) + ig.G.Eventually(func(g Gomega) int { + g.Expect(false).To(BeTrue()) + return 10 + }, "30ms", "10ms").Should(Equal(10)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2)) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) + }) + + It("forwards panics", func() { + Ω(func() { + ig.G.Eventually(func(g Gomega) int { + g.Expect(true).To(BeTrue()) + panic("boom") + return 10 + }, "30ms", "10ms").Should(Equal(10)) + }).Should(PanicWith("boom")) + Ω(ig.FailureMessage).Should(BeEmpty()) + }) + + Context("when making a ShouldNot assertion", func() { + It("doesn't succeed until all extra values are zero, there are no failed assertions, and the matcher is (not) satisfied", func() { + counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi") + ig.G.Eventually(func(g Gomega) (int, string, Foo, error) { + switch counter += 1; counter { + case 2: + s = "" + case 3: + f = Foo{} + case 4: + err = nil + } + if counter == 5 { + g.Expect(true).To(BeTrue()) + } else { + g.Expect(false).To(BeTrue()) + panic("boom") //never see since the expectation stops execution + } + return counter, s, f, err + }, "1s", "10ms").ShouldNot(BeNumerically("<", 0)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(Equal(5)) + }) + }) + + It("fails if an assertion is never satisfied", func() { + _, file, line, _ := runtime.Caller(0) + ig.G.Eventually(func(g Gomega) int { + g.Expect(false).To(BeTrue()) + return 9 + }, "30ms", "10ms").ShouldNot(Equal(10)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2)) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) + }) + }) + + Context("with Consistently", func() { + It("passes in a Gomega and passes if the matcher matches, all extra values are zero, and there are no failed assertions", func() { + var err error + counter, s, f := 0, "", Foo{} + ig.G.Consistently(func(g Gomega) (int, string, Foo, error) { + counter += 1 + g.Expect(true).To(BeTrue()) + return counter, s, f, err + }, "50ms", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(BeZero()) + Ω(counter).Should(BeNumerically(">", 2)) + }) + + It("fails if the passed-in gomega ever hits a failure", func() { + var err error + counter, s, f := 0, "", Foo{} + _, file, line, _ := runtime.Caller(0) + ig.G.Consistently(func(g Gomega) (int, string, Foo, error) { + counter += 1 + g.Expect(true).To(BeTrue()) + if counter == 3 { + g.Expect(false).To(BeTrue()) + panic("boom") //never see this + } + return counter, s, f, err + }, "50ms", "10ms").Should(BeNumerically("<", 100)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5)) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) + Ω(counter).Should(Equal(3)) + }) + + It("forwards panics", func() { + Ω(func() { + ig.G.Consistently(func(g Gomega) int { + g.Expect(true).To(BeTrue()) + panic("boom") + return 10 + }, "50ms", "10ms").Should(Equal(10)) + }).Should(PanicWith("boom")) + Ω(ig.FailureMessage).Should(BeEmpty()) + }) + + Context("when making a ShouldNot assertion", func() { + It("succeeds if any interior assertions always pass", func() { + ig.G.Consistently(func(g Gomega) int { + g.Expect(true).To(BeTrue()) + return 9 + }, "50ms", "10ms").ShouldNot(Equal(10)) + Ω(ig.FailureMessage).Should(BeEmpty()) + }) + + It("fails if any interior assertions ever fail", func() { + counter := 0 + _, file, line, _ := runtime.Caller(0) + ig.G.Consistently(func(g Gomega) int { + g.Expect(true).To(BeTrue()) + counter += 1 + if counter == 3 { + g.Expect(false).To(BeTrue()) + panic("boom") //never see this + } + return 9 + }, "50ms", "10ms").ShouldNot(Equal(10)) + Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5)) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n : false\nto be true")) + }) + }) + }) + }) + + Context("when passed a function that takes a Gomega argument and returns nothing", func() { + Context("with Eventually", func() { + It("returns the first failed assertion as an error and so should Succeed() if the callback ever runs without issue", func() { + counter := 0 + ig.G.Eventually(func(g Gomega) { + counter += 1 + if counter < 5 { + g.Expect(false).To(BeTrue()) + g.Expect("bloop").To(Equal("blarp")) + } + }, "1s", "10ms").Should(Succeed()) + Ω(counter).Should(Equal(5)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("returns the first failed assertion as an error and so should timeout if the callback always fails", func() { + counter := 0 + ig.G.Eventually(func(g Gomega) { + counter += 1 + if counter < 5000 { + g.Expect(false).To(BeTrue()) + g.Expect("bloop").To(Equal("blarp")) + } + }, "100ms", "10ms").Should(Succeed()) + Ω(counter).Should(BeNumerically(">", 1)) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error")) + Ω(ig.FailureMessage).Should(ContainSubstring(": false")) + Ω(ig.FailureMessage).Should(ContainSubstring("to be true")) + Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop")) + }) + + It("returns the first failed assertion as an error and should satisy ShouldNot(Succeed) eventually", func() { + counter := 0 + ig.G.Eventually(func(g Gomega) { + counter += 1 + if counter > 5 { + g.Expect(false).To(BeTrue()) + g.Expect("bloop").To(Equal("blarp")) + } + }, "100ms", "10ms").ShouldNot(Succeed()) + Ω(counter).Should(Equal(6)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("should fail to ShouldNot(Succeed) eventually if an error never occurs", func() { + ig.G.Eventually(func(g Gomega) { + g.Expect(true).To(BeTrue()) + }, "50ms", "10ms").ShouldNot(Succeed()) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected failure, but got no error.")) + }) + }) + + Context("with Consistently", func() { + It("returns the first failed assertion as an error and so should Succeed() if the callback always runs without issue", func() { + counter := 0 + ig.G.Consistently(func(g Gomega) { + counter += 1 + g.Expect(true).To(BeTrue()) + }, "50ms", "10ms").Should(Succeed()) + Ω(counter).Should(BeNumerically(">", 2)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("returns the first failed assertion as an error and so should fail if the callback ever fails", func() { + counter := 0 + ig.G.Consistently(func(g Gomega) { + counter += 1 + g.Expect(true).To(BeTrue()) + if counter == 3 { + g.Expect(false).To(BeTrue()) + g.Expect("bloop").To(Equal("blarp")) + } + }, "50ms", "10ms").Should(Succeed()) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error")) + Ω(ig.FailureMessage).Should(ContainSubstring(": false")) + Ω(ig.FailureMessage).Should(ContainSubstring("to be true")) + Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop")) + Ω(counter).Should(Equal(3)) + }) + + It("returns the first failed assertion as an error and should satisy ShouldNot(Succeed) consistently if an error always occur", func() { + counter := 0 + ig.G.Consistently(func(g Gomega) { + counter += 1 + g.Expect(true).To(BeFalse()) + }, "50ms", "10ms").ShouldNot(Succeed()) + Ω(counter).Should(BeNumerically(">", 2)) + Ω(ig.FailureMessage).Should(BeZero()) + }) + + It("should fail to satisfy ShouldNot(Succeed) consistently if an error ever does not occur", func() { + counter := 0 + ig.G.Consistently(func(g Gomega) { + counter += 1 + if counter == 3 { + g.Expect(true).To(BeTrue()) + } else { + g.Expect(false).To(BeTrue()) + } + }, "50ms", "10ms").ShouldNot(Succeed()) + Ω(ig.FailureMessage).Should(ContainSubstring("Failed after")) + Ω(ig.FailureMessage).Should(ContainSubstring("Expected failure, but got no error.")) + Ω(counter).Should(Equal(3)) + }) + }) + }) + + Describe("when passed an invalid function", func() { + It("errors immediately", func() { + ig.G.Eventually(func() {}) + Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 0 arguments and returns 0 values.")) + Ω(ig.FailureSkip).Should(Equal([]int{4})) + + ig = NewInstrumentedGomega() + ig.G.Eventually(func(g Gomega, foo string) {}) + Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 2 arguments and returns 0 values.")) + Ω(ig.FailureSkip).Should(Equal([]int{4})) + + ig = NewInstrumentedGomega() + ig.G.Eventually(func(foo string) {}) + Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 1 arguments and returns 0 values.")) + Ω(ig.FailureSkip).Should(Equal([]int{4})) + }) + }) + }) + + Describe("when using OracleMatchers", func() { + It("stops and gives up with an appropriate failure message if the OracleMatcher says things can't change", func() { + c := make(chan bool) + close(c) + + t := time.Now() + ig.G.Eventually(c, "100ms", "10ms").Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") + Ω(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond)) + Ω(ig.FailureMessage).Should(ContainSubstring("No future change is possible.")) + Ω(ig.FailureMessage).Should(ContainSubstring("The channel is closed.")) + }) + + It("never gives up if actual is a function", func() { + c := make(chan bool) + close(c) + + t := time.Now() + ig.G.Eventually(func() chan bool { return c }, "100ms", "10ms").Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed") + Ω(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond)) + Ω(ig.FailureMessage).ShouldNot(ContainSubstring("No future change is possible.")) + Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after")) + }) + }) +}) diff --git a/internal/asyncassertion/async_assertion_suite_test.go b/internal/asyncassertion/async_assertion_suite_test.go deleted file mode 100644 index bdb0c3d22..000000000 --- a/internal/asyncassertion/async_assertion_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package asyncassertion_test - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "testing" -) - -func TestAsyncAssertion(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "AsyncAssertion Suite") -} diff --git a/internal/asyncassertion/async_assertion_test.go b/internal/asyncassertion/async_assertion_test.go deleted file mode 100644 index 8c6c83a11..000000000 --- a/internal/asyncassertion/async_assertion_test.go +++ /dev/null @@ -1,489 +0,0 @@ -package asyncassertion_test - -import ( - "errors" - "runtime" - "time" - - "github.com/onsi/gomega/internal/testingtsupport" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/internal/asyncassertion" - "github.com/onsi/gomega/types" -) - -var _ = Describe("Async Assertion", func() { - var ( - failureMessage string - callerSkip int - ) - - var fakeFailWrapper = &types.GomegaFailWrapper{ - Fail: func(message string, skip ...int) { - failureMessage = message - callerSkip = skip[0] - }, - TWithHelper: testingtsupport.EmptyTWithHelper{}, - } - - BeforeEach(func() { - failureMessage = "" - callerSkip = 0 - }) - - Describe("Eventually", func() { - Context("the positive case", func() { - It("should poll the function and matcher", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { - counter++ - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(BeNumerically("==", 5)) - Expect(failureMessage).Should(BeZero()) - }) - - It("should continue when the matcher errors", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} { - counter++ - if counter == 5 { - return "not-a-number" //this should cause the matcher to error - } - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(BeNumerically("==", 5), "My description %d", 2) - - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(ContainSubstring("My description 2")) - Expect(callerSkip).Should(Equal(4)) - }) - - It("should be able to timeout", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { - counter++ - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(BeNumerically(">", 100), "My description %d", 2) - - Expect(counter).Should(BeNumerically(">", 8)) - Expect(counter).Should(BeNumerically("<=", 10)) - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(MatchRegexp(`\: \d`), "Should pass the correct value to the matcher message formatter.") - Expect(failureMessage).Should(ContainSubstring("My description 2")) - Expect(callerSkip).Should(Equal(4)) - }) - - When("the optional description is a function", func() { - It("should append the description to the failure message", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} { - counter++ - if counter == 5 { - return "not-a-number" //this should cause the matcher to error - } - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(BeNumerically("==", 5), func() string { return "My description" }) - - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(ContainSubstring("My description")) - Expect(callerSkip).Should(Equal(4)) - }) - - Context("and there is no failure", func() { - It("should not evaluate that function", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { - counter++ - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - evaluated := false - a.Should(BeNumerically("==", 5), func() string { - evaluated = true - return "A description" - }) - - Expect(failureMessage).Should(BeZero()) - Expect(evaluated).Should(BeFalse()) - }) - }) - }) - }) - - Context("the negative case", func() { - It("should poll the function and matcher", func() { - counter := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { - counter += 1 - return counter - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(BeNumerically("<", 3)) - - Expect(counter).Should(Equal(3)) - Expect(failureMessage).Should(BeZero()) - }) - - It("should timeout when the matcher errors", func() { - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} { - return 0 //this should cause the matcher to error - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(HaveLen(0), "My description %d", 2) - - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(ContainSubstring("Error:")) - Expect(failureMessage).Should(ContainSubstring("My description 2")) - Expect(callerSkip).Should(Equal(4)) - }) - - It("should be able to timeout", func() { - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { - return 0 - }, fakeFailWrapper, time.Duration(0.1*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(Equal(0), "My description %d", 2) - - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(ContainSubstring(": 0"), "Should pass the correct value to the matcher message formatter.") - Expect(failureMessage).Should(ContainSubstring("My description 2")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - - Context("with a function that returns multiple values", func() { - It("should eventually succeed if the additional arguments are nil", func() { - i := 0 - Eventually(func() (int, error) { - i++ - return i, nil - }).Should(Equal(10)) - }) - - It("should eventually timeout if the additional arguments are not nil", func() { - i := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) { - i++ - return i, errors.New("bam") - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.Should(Equal(2)) - - Expect(failureMessage).Should(ContainSubstring("Timed out after")) - Expect(failureMessage).Should(ContainSubstring("Error:")) - Expect(failureMessage).Should(ContainSubstring("bam")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - - Context("when the polled function makes assertions", func() { - It("fails if those assertions never succeed", func() { - var file string - var line int - err := InterceptGomegaFailure(func() { - i := 0 - Eventually(func() int { - _, file, line, _ = runtime.Caller(0) - Expect(i).To(BeNumerically(">", 5)) - return 2 - }, 200*time.Millisecond, 20*time.Millisecond).Should(Equal(2)) - }) - Ω(err.Error()).Should(ContainSubstring("Timed out after")) - Ω(err.Error()).Should(ContainSubstring("Assertion in callback at %s:%d failed:", file, line+1)) - Ω(err.Error()).Should(ContainSubstring("to be >")) - }) - - It("eventually succeeds if the assertions succeed", func() { - err := InterceptGomegaFailure(func() { - i := 0 - Eventually(func() int { - i++ - Expect(i).To(BeNumerically(">", 5)) - return 2 - }, 200*time.Millisecond, 20*time.Millisecond).Should(Equal(2)) - }) - Ω(err).ShouldNot(HaveOccurred()) - }) - - It("succeeds if the assertions succeed even if the function doesn't return anything", func() { - i := 0 - Eventually(func() { - i++ - Expect(i).To(BeNumerically(">", 5)) - }, 200*time.Millisecond, 20*time.Millisecond).Should(Succeed()) - }) - - It("succeeds if the function returns nothing, the assertions eventually fail and the Eventually is assertion that it ShouldNot(Succeed()) ", func() { - i := 0 - Eventually(func() { - i++ - Expect(i).To(BeNumerically("<", 5)) - }, 200*time.Millisecond, 20*time.Millisecond).ShouldNot(Succeed()) - }) - }) - - Context("Making an assertion without a registered fail handler", func() { - It("should panic", func() { - defer func() { - e := recover() - RegisterFailHandler(Fail) - if e == nil { - Fail("expected a panic to have occurred") - } - }() - - RegisterFailHandler(nil) - c := make(chan bool, 1) - c <- true - Eventually(c).Should(Receive()) - }) - }) - }) - - Describe("Consistently", func() { - Describe("The positive case", func() { - When("the matcher consistently passes for the duration", func() { - It("should pass", func() { - calls := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() string { - calls++ - return "foo" - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(Equal("foo")) - Expect(calls).Should(BeNumerically(">", 8)) - Expect(calls).Should(BeNumerically("<=", 10)) - Expect(failureMessage).Should(BeZero()) - }) - }) - - When("the matcher fails at some point", func() { - It("should fail", func() { - calls := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} { - calls++ - if calls > 5 { - return "bar" - } - return "foo" - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(Equal("foo")) - Expect(failureMessage).Should(ContainSubstring("to equal")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - - When("the matcher errors at some point", func() { - It("should fail", func() { - calls := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} { - calls++ - if calls > 5 { - return 3 - } - return []int{1, 2, 3} - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.Should(HaveLen(3)) - Expect(failureMessage).Should(ContainSubstring("HaveLen matcher expects")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - }) - - Describe("The negative case", func() { - When("the matcher consistently passes for the duration", func() { - It("should pass", func() { - c := make(chan bool) - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, c, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(Receive()) - Expect(failureMessage).Should(BeZero()) - }) - }) - - When("the matcher fails at some point", func() { - It("should fail", func() { - c := make(chan bool) - go func() { - time.Sleep(time.Duration(100 * time.Millisecond)) - c <- true - }() - - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, c, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(Receive()) - Expect(failureMessage).Should(ContainSubstring("not to receive anything")) - }) - }) - - When("the matcher errors at some point", func() { - It("should fail", func() { - calls := 0 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} { - calls++ - return calls - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - - a.ShouldNot(BeNumerically(">", 5)) - Expect(failureMessage).Should(ContainSubstring("not to be >")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - }) - - Context("with a function that returns multiple values", func() { - It("should consistently succeed if the additional arguments are nil", func() { - i := 2 - Consistently(func() (int, error) { - i++ - return i, nil - }).Should(BeNumerically(">=", 2)) - }) - - It("should eventually timeout if the additional arguments are not nil", func() { - i := 2 - a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) { - i++ - return i, errors.New("bam") - }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1) - a.Should(BeNumerically(">=", 2)) - - Expect(failureMessage).Should(ContainSubstring("Error:")) - Expect(failureMessage).Should(ContainSubstring("bam")) - Expect(callerSkip).Should(Equal(4)) - }) - }) - - Context("when the polled function makes assertions", func() { - It("fails if those assertions ever fail", func() { - var file string - var line int - - err := InterceptGomegaFailure(func() { - i := 0 - Consistently(func() int { - _, file, line, _ = runtime.Caller(0) - Expect(i).To(BeNumerically("<", 5)) - i++ - return 2 - }, 200*time.Millisecond, 20*time.Millisecond).Should(Equal(2)) - }) - Ω(err.Error()).Should(ContainSubstring("Failed after")) - Ω(err.Error()).Should(ContainSubstring("Assertion in callback at %s:%d failed:", file, line+1)) - Ω(err.Error()).Should(ContainSubstring("to be <")) - }) - - It("succeeds if the assertion consistently succeeds", func() { - err := InterceptGomegaFailure(func() { - i := 0 - Consistently(func() int { - i++ - Expect(i).To(BeNumerically("<", 1000)) - return 2 - }, 200*time.Millisecond, 20*time.Millisecond).Should(Equal(2)) - }) - Ω(err).ShouldNot(HaveOccurred()) - }) - - It("succeeds if the assertions succeed even if the function doesn't return anything", func() { - i := 0 - Consistently(func() { - i++ - Expect(i).To(BeNumerically("<", 1000)) - }, 200*time.Millisecond, 20*time.Millisecond).Should(Succeed()) - }) - - It("succeeds if the assertions fail even if the function doesn't return anything and Consistently is checking for ShouldNot(Succeed())", func() { - i := 0 - Consistently(func() { - i++ - Expect(i).To(BeNumerically(">", 1000)) - }, 200*time.Millisecond, 20*time.Millisecond).ShouldNot(Succeed()) - }) - - }) - - Context("Making an assertion without a registered fail handler", func() { - It("should panic", func() { - defer func() { - e := recover() - RegisterFailHandler(Fail) - if e == nil { - Fail("expected a panic to have occurred") - } - }() - - RegisterFailHandler(nil) - c := make(chan bool) - Consistently(c).ShouldNot(Receive()) - }) - }) - }) - - When("passed a function with the wrong # or arguments", func() { - It("should panic", func() { - Expect(func() { - asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() {}, fakeFailWrapper, 0, 0, 1) - }).ShouldNot(Panic()) - - Expect(func() { - asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func(a string) int { return 0 }, fakeFailWrapper, 0, 0, 1) - }).Should(Panic()) - - Expect(func() { - asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func(a string) {}, fakeFailWrapper, 0, 0, 1) - }).Should(Panic()) - - Expect(func() { - asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { return 0 }, fakeFailWrapper, 0, 0, 1) - }).ShouldNot(Panic()) - - Expect(func() { - asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) { return 0, nil }, fakeFailWrapper, 0, 0, 1) - }).ShouldNot(Panic()) - }) - }) - - Describe("bailing early", func() { - When("actual is a value", func() { - It("Eventually should bail out and fail early if the matcher says to", func() { - c := make(chan bool) - close(c) - - t := time.Now() - failures := InterceptGomegaFailures(func() { - Eventually(c, 0.1).Should(Receive()) - }) - Expect(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond)) - - Expect(failures).Should(HaveLen(1)) - }) - }) - - When("actual is a function", func() { - It("should never bail early", func() { - c := make(chan bool) - close(c) - - t := time.Now() - failures := InterceptGomegaFailures(func() { - Eventually(func() chan bool { - return c - }, 0.1).Should(Receive()) - }) - Expect(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond)) - - Expect(failures).Should(HaveLen(1)) - }) - }) - }) -}) diff --git a/internal/defaults/defaults_suite_test.go b/internal/defaults/defaults_suite_test.go deleted file mode 100644 index 004a02b8e..000000000 --- a/internal/defaults/defaults_suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package defaults_test - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestDefaults(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Gomega Defaults Suite") -} diff --git a/internal/defaults/env.go b/internal/defaults/env.go deleted file mode 100644 index bc29c63d5..000000000 --- a/internal/defaults/env.go +++ /dev/null @@ -1,22 +0,0 @@ -package defaults - -import ( - "fmt" - "time" -) - -func SetDurationFromEnv(getDurationFromEnv func(string) string, varSetter func(time.Duration), name string) { - durationFromEnv := getDurationFromEnv(name) - - if len(durationFromEnv) == 0 { - return - } - - duration, err := time.ParseDuration(durationFromEnv) - - if err != nil { - panic(fmt.Sprintf("Expected a duration when using %s! Parse error %v", name, err)) - } - - varSetter(duration) -} diff --git a/internal/defaults/env_test.go b/internal/defaults/env_test.go deleted file mode 100644 index 138cfe9f9..000000000 --- a/internal/defaults/env_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package defaults_test - -import ( - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - d "github.com/onsi/gomega/internal/defaults" -) - -var _ = Describe("Durations", func() { - var ( - duration *time.Duration - envVarGot string - envVarToReturn string - - getDurationFromEnv = func(name string) string { - envVarGot = name - return envVarToReturn - } - - setDuration = func(t time.Duration) { - duration = &t - } - - setDurationCalled = func() bool { - return duration != nil - } - - resetDuration = func() { - duration = nil - } - ) - - BeforeEach(func() { - resetDuration() - }) - - Context("When the environment has a duration", func() { - Context("When the duration is valid", func() { - BeforeEach(func() { - envVarToReturn = "10m" - - d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR") - }) - - It("sets the duration", func() { - Expect(envVarGot).To(Equal("MY_ENV_VAR")) - Expect(setDurationCalled()).To(Equal(true)) - Expect(*duration).To(Equal(10 * time.Minute)) - }) - }) - - Context("When the duration is not valid", func() { - BeforeEach(func() { - envVarToReturn = "10" - }) - - It("panics with a helpful error message", func() { - Expect(func() { - d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR") - }).To(PanicWith(MatchRegexp("Expected a duration when using MY_ENV_VAR"))) - }) - }) - }) - - Context("When the environment does not have a duration", func() { - BeforeEach(func() { - envVarToReturn = "" - - d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR") - }) - - It("does not set the duration", func() { - Expect(envVarGot).To(Equal("MY_ENV_VAR")) - Expect(setDurationCalled()).To(Equal(false)) - Expect(duration).To(BeNil()) - }) - }) -}) diff --git a/internal/dsl_test.go b/internal/dsl_test.go index ccb067a52..33b7069f4 100644 --- a/internal/dsl_test.go +++ b/internal/dsl_test.go @@ -2,12 +2,122 @@ package internal_test import ( "errors" + "runtime" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/internal" ) +func getGlobalDurationBundle() internal.DurationBundle { + return Default.(*internal.Gomega).DurationBundle +} + +func setGlobalDurationBundle(bundle internal.DurationBundle) { + SetDefaultEventuallyTimeout(bundle.EventuallyTimeout) + SetDefaultEventuallyPollingInterval(bundle.EventuallyPollingInterval) + SetDefaultConsistentlyDuration(bundle.ConsistentlyDuration) + SetDefaultConsistentlyPollingInterval(bundle.ConsistentlyPollingInterval) +} + var _ = Describe("Gomega DSL", func() { + var globalDurationBundle internal.DurationBundle + + BeforeEach(func() { + globalDurationBundle = getGlobalDurationBundle() + }) + + AfterEach(func() { + RegisterFailHandler(Fail) + setGlobalDurationBundle(globalDurationBundle) + }) + + Describe("The Default, global, Gomega", func() { + It("exists", func() { + Ω(Default).ShouldNot(BeNil()) + }) + + It("is wired up via the global DSL", func() { + counter := 0 + Eventually(func() int { + counter += 1 + return counter + }).Should(Equal(5)) + Ω(counter).Should(Equal(5)) + }) + }) + + Describe("NewGomega", func() { + It("creates and configures a new Gomega, using the global duration bundle", func() { + bundle := internal.DurationBundle{ + EventuallyTimeout: time.Minute, + EventuallyPollingInterval: 2 * time.Minute, + ConsistentlyDuration: 3 * time.Minute, + ConsistentlyPollingInterval: 4 * time.Minute, + } + setGlobalDurationBundle(bundle) + + var calledWith string + g := NewGomega(func(message string, skip ...int) { + calledWith = message + }) + + gAsStruct := g.(*internal.Gomega) + Ω(gAsStruct.DurationBundle).Should(Equal(bundle)) + + g.Ω(true).Should(BeFalse()) + Ω(calledWith).Should(Equal("Expected\n : true\nto be false")) + }) + }) + + Describe("NewWithT", func() { + It("creates and configure a new Gomega with the passed-in T, using the global duration bundle", func() { + bundle := internal.DurationBundle{ + EventuallyTimeout: time.Minute, + EventuallyPollingInterval: 2 * time.Minute, + ConsistentlyDuration: 3 * time.Minute, + ConsistentlyPollingInterval: 4 * time.Minute, + } + setGlobalDurationBundle(bundle) + + fakeT := &FakeGomegaTestingT{} + g := NewWithT(fakeT) + + Ω(g.DurationBundle).Should(Equal(bundle)) + + g.Ω(true).Should(BeFalse()) + Ω(fakeT.CalledFatalf).Should(Equal("\nExpected\n : true\nto be false")) + Ω(fakeT.CalledHelper).Should(BeTrue()) + }) + }) + + Describe("RegisterFailHandler", func() { + It("overrides the global fail handler", func() { + var calledWith string + RegisterFailHandler(func(message string, skip ...int) { + calledWith = message + }) + + Ω(true).Should(BeFalse()) + + RegisterFailHandler(Fail) + Ω(calledWith).Should(Equal("Expected\n : true\nto be false")) + }) + }) + + Describe("RegisterTestingT", func() { + It("overrides the global fail handler", func() { + fakeT := &FakeGomegaTestingT{} + RegisterTestingT(fakeT) + + Ω(true).Should(BeFalse()) + RegisterFailHandler(Fail) + Ω(fakeT.CalledFatalf).Should(Equal("\nExpected\n : true\nto be false")) + Ω(fakeT.CalledHelper).Should(BeTrue()) + }) + }) + Describe("InterceptGomegaFailures", func() { Context("when no failures occur", func() { It("returns an empty array", func() { @@ -62,4 +172,82 @@ var _ = Describe("Gomega DSL", func() { }) }) }) + + Context("Making an assertion without a registered fail handler", func() { + It("should panic", func() { + defer func() { + e := recover() + RegisterFailHandler(Fail) + if e == nil { + Fail("expected a panic to have occurred") + } + }() + + RegisterFailHandler(nil) + Expect(true).Should(BeTrue()) + }) + }) + + Describe("specifying default durations globally", func() { + It("should update the durations on the Default gomega", func() { + bundle := internal.DurationBundle{ + EventuallyTimeout: time.Minute, + EventuallyPollingInterval: 2 * time.Minute, + ConsistentlyDuration: 3 * time.Minute, + ConsistentlyPollingInterval: 4 * time.Minute, + } + + SetDefaultEventuallyTimeout(bundle.EventuallyTimeout) + SetDefaultEventuallyPollingInterval(bundle.EventuallyPollingInterval) + SetDefaultConsistentlyDuration(bundle.ConsistentlyDuration) + SetDefaultConsistentlyPollingInterval(bundle.ConsistentlyPollingInterval) + + Ω(Default.(*internal.Gomega).DurationBundle).Should(Equal(bundle)) + }) + }) + + Describe("Offsets", func() { + AfterEach(func() { + RegisterFailHandler(Fail) + }) + + It("computes the correct offsets", func() { + doubleNested := func(eventually bool) { + func() { + if eventually { + EventuallyWithOffset(2, true, "10ms", "5ms").Should(BeFalse()) + } else { + ExpectWithOffset(2, true).To(BeFalse()) + } + }() + } + + reportedFile, reportedLine := "", 0 + captureLocation := func(message string, skip ...int) { + _, reportedFile, reportedLine, _ = runtime.Caller(skip[0] + 1) + } + + _, thisFile, anchorLine, _ := runtime.Caller(0) // 0 + RegisterFailHandler(captureLocation) // 1 + Expect(true).To(BeFalse()) // *2* + RegisterFailHandler(Fail) // 3 + Ω(reportedFile).Should(Equal(thisFile)) // 4 + Ω(reportedLine - anchorLine).Should(Equal(2)) // 5 + RegisterFailHandler(captureLocation) // 6 + doubleNested(false) // *7* + RegisterFailHandler(Fail) // 8 + Ω(reportedFile).Should(Equal(thisFile)) // 9 + Ω(reportedLine - anchorLine).Should(Equal(7)) // 10 + RegisterFailHandler(captureLocation) // 11 + Eventually(true, "10ms", "5ms").Should(BeFalse()) // *12* + RegisterFailHandler(Fail) // 13 + Ω(reportedFile).Should(Equal(thisFile)) // 14 + Ω(reportedLine - anchorLine).Should(Equal(12)) // 15 + RegisterFailHandler(captureLocation) // 16 + doubleNested(true) // *17* + RegisterFailHandler(Fail) // 18 + Ω(reportedFile).Should(Equal(thisFile)) // 19 + Ω(reportedLine - anchorLine).Should(Equal(17)) // 20 + }) + }) }) diff --git a/internal/duration_bundle.go b/internal/duration_bundle.go new file mode 100644 index 000000000..af8d989fa --- /dev/null +++ b/internal/duration_bundle.go @@ -0,0 +1,71 @@ +package internal + +import ( + "fmt" + "os" + "reflect" + "time" +) + +type DurationBundle struct { + EventuallyTimeout time.Duration + EventuallyPollingInterval time.Duration + ConsistentlyDuration time.Duration + ConsistentlyPollingInterval time.Duration +} + +const ( + EventuallyTimeoutEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT" + EventuallyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL" + + ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION" + ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL" +) + +func FetchDefaultDurationBundle() DurationBundle { + return DurationBundle{ + EventuallyTimeout: durationFromEnv(EventuallyTimeoutEnvVarName, time.Second), + EventuallyPollingInterval: durationFromEnv(EventuallyPollingIntervalEnvVarName, 10*time.Millisecond), + + ConsistentlyDuration: durationFromEnv(ConsistentlyDurationEnvVarName, 100*time.Millisecond), + ConsistentlyPollingInterval: durationFromEnv(ConsistentlyPollingIntervalEnvVarName, 10*time.Millisecond), + } +} + +func durationFromEnv(key string, defaultDuration time.Duration) time.Duration { + value := os.Getenv(key) + if value == "" { + return defaultDuration + } + duration, err := time.ParseDuration(value) + if err != nil { + panic(fmt.Sprintf("Expected a duration when using %s! Parse error %v", key, err)) + } + return duration +} + +func toDuration(input interface{}) time.Duration { + duration, ok := input.(time.Duration) + if ok { + return duration + } + + value := reflect.ValueOf(input) + kind := reflect.TypeOf(input).Kind() + + if reflect.Int <= kind && kind <= reflect.Int64 { + return time.Duration(value.Int()) * time.Second + } else if reflect.Uint <= kind && kind <= reflect.Uint64 { + return time.Duration(value.Uint()) * time.Second + } else if reflect.Float32 <= kind && kind <= reflect.Float64 { + return time.Duration(value.Float() * float64(time.Second)) + } else if reflect.String == kind { + duration, err := time.ParseDuration(value.String()) + if err != nil { + panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input)) + } + return duration + } + + panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input)) +} diff --git a/internal/duration_bundle_test.go b/internal/duration_bundle_test.go new file mode 100644 index 000000000..0c463a931 --- /dev/null +++ b/internal/duration_bundle_test.go @@ -0,0 +1,154 @@ +package internal_test + +import ( + "os" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/onsi/gomega/internal" +) + +var _ = Describe("DurationBundle and Duration Support", func() { + Describe("fetching default durations from the environment", func() { + var envVars []string + var originalValues map[string]string + + BeforeEach(func() { + envVars = []string{internal.EventuallyTimeoutEnvVarName, internal.EventuallyPollingIntervalEnvVarName, internal.ConsistentlyDurationEnvVarName, internal.ConsistentlyPollingIntervalEnvVarName} + originalValues = map[string]string{} + + for _, envVar := range envVars { + originalValues[envVar] = os.Getenv(envVar) + } + }) + + AfterEach(func() { + for _, envVar := range envVars { + Ω(os.Setenv(envVar, originalValues[envVar])).Should(Succeed()) + } + }) + + Context("with no environment set", func() { + BeforeEach(func() { + for _, envVar := range envVars { + os.Unsetenv(envVar) + } + }) + + It("returns the default bundle", func() { + bundle := internal.FetchDefaultDurationBundle() + Ω(bundle.EventuallyTimeout).Should(Equal(time.Second)) + Ω(bundle.EventuallyPollingInterval).Should(Equal(10 * time.Millisecond)) + Ω(bundle.ConsistentlyDuration).Should(Equal(100 * time.Millisecond)) + Ω(bundle.ConsistentlyPollingInterval).Should(Equal(10 * time.Millisecond)) + }) + }) + + Context("with a valid environment set", func() { + BeforeEach(func() { + os.Setenv(internal.EventuallyTimeoutEnvVarName, "1m") + os.Setenv(internal.EventuallyPollingIntervalEnvVarName, "2s") + os.Setenv(internal.ConsistentlyDurationEnvVarName, "1h") + os.Setenv(internal.ConsistentlyPollingIntervalEnvVarName, "3ms") + }) + + It("returns an appropriate bundle", func() { + bundle := internal.FetchDefaultDurationBundle() + Ω(bundle.EventuallyTimeout).Should(Equal(time.Minute)) + Ω(bundle.EventuallyPollingInterval).Should(Equal(2 * time.Second)) + Ω(bundle.ConsistentlyDuration).Should(Equal(time.Hour)) + Ω(bundle.ConsistentlyPollingInterval).Should(Equal(3 * time.Millisecond)) + }) + }) + + Context("with an invalid environment set", func() { + BeforeEach(func() { + os.Setenv(internal.EventuallyTimeoutEnvVarName, "chicken nuggets") + }) + + It("panics", func() { + Ω(func() { + internal.FetchDefaultDurationBundle() + }).Should(PanicWith(`Expected a duration when using GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT! Parse error time: invalid duration "chicken nuggets"`)) + }) + }) + }) + + Describe("specifying default durations on a Gomega instance", func() { + It("is supported", func() { + ig := NewInstrumentedGomega() + ig.G.SetDefaultConsistentlyDuration(50 * time.Millisecond) + ig.G.SetDefaultConsistentlyPollingInterval(5 * time.Millisecond) + ig.G.SetDefaultEventuallyTimeout(200 * time.Millisecond) + ig.G.SetDefaultEventuallyPollingInterval(20 * time.Millisecond) + + counter := 0 + t := time.Now() + ig.G.Consistently(func() bool { + counter += 1 + return true + }).Should(BeTrue()) + dt := time.Since(t) + Ω(dt).Should(BeNumerically("~", 50*time.Millisecond, 25*time.Millisecond)) + Ω(counter).Should(BeNumerically("~", 10, 5)) + + t = time.Now() + counter = 0 + ig.G.Eventually(func() bool { + counter += 1 + if counter >= 6 { + return true + } + return false + }).Should(BeTrue()) + dt = time.Since(t) + Ω(dt).Should(BeNumerically("~", 120*time.Millisecond, 20*time.Millisecond)) + }) + }) + + Describe("specifying durations", func() { + It("supports passing in a duration", func() { + t := time.Now() + Consistently(true, 50*time.Millisecond).Should(BeTrue()) + Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond)) + }) + + It("supports passing in a raw integer # of seconds", func() { + t := time.Now() + Consistently(true, 1).Should(BeTrue()) + Ω(time.Since(t)).Should(BeNumerically("~", time.Second, 100*time.Millisecond)) + }) + + It("supports passing in an unsigned integer # of seconds", func() { + t := time.Now() + Consistently(true, uint(1)).Should(BeTrue()) + Ω(time.Since(t)).Should(BeNumerically("~", time.Second, 100*time.Millisecond)) + }) + + It("supports passing in a float number of seconds", func() { + t := time.Now() + Consistently(true, 0.05).Should(BeTrue()) + Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond)) + }) + + It("supports passing in a duration string", func() { + t := time.Now() + Consistently(true, "50ms").Should(BeTrue()) + Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond)) + }) + + It("panics when the duration string can't be parsed", func() { + Ω(func() { + Consistently(true, "fries").Should(BeTrue()) + }).Should(PanicWith(`"fries" is not a valid parsable duration string.`)) + }) + + It("panics if anything else is passed in", func() { + Ω(func() { + Consistently(true, true).Should(BeTrue()) + }).Should(PanicWith("true is not a valid interval. Must be time.Duration, parsable duration string or a number.")) + }) + }) +}) diff --git a/internal/fakematcher/fake_matcher.go b/internal/fakematcher/fake_matcher.go deleted file mode 100644 index 6e351a7de..000000000 --- a/internal/fakematcher/fake_matcher.go +++ /dev/null @@ -1,23 +0,0 @@ -package fakematcher - -import "fmt" - -type FakeMatcher struct { - ReceivedActual interface{} - MatchesToReturn bool - ErrToReturn error -} - -func (matcher *FakeMatcher) Match(actual interface{}) (bool, error) { - matcher.ReceivedActual = actual - - return matcher.MatchesToReturn, matcher.ErrToReturn -} - -func (matcher *FakeMatcher) FailureMessage(actual interface{}) string { - return fmt.Sprintf("positive: %v", actual) -} - -func (matcher *FakeMatcher) NegatedFailureMessage(actual interface{}) string { - return fmt.Sprintf("negative: %v", actual) -} diff --git a/internal/gomega.go b/internal/gomega.go new file mode 100644 index 000000000..f5b5c6b7a --- /dev/null +++ b/internal/gomega.go @@ -0,0 +1,102 @@ +package internal + +import ( + "time" + + "github.com/onsi/gomega/types" +) + +type Gomega struct { + Fail types.GomegaFailHandler + THelper func() + DurationBundle DurationBundle +} + +func NewGomega(bundle DurationBundle) *Gomega { + return &Gomega{ + Fail: nil, + THelper: nil, + DurationBundle: bundle, + } +} + +func (g *Gomega) IsConfigured() bool { + return g.Fail != nil && g.THelper != nil +} + +func (g *Gomega) ConfigureWithFailHandler(fail types.GomegaFailHandler) *Gomega { + g.Fail = fail + g.THelper = func() {} + return g +} + +func (g *Gomega) ConfigureWithT(t types.GomegaTestingT) *Gomega { + g.Fail = func(message string, _ ...int) { + t.Helper() + t.Fatalf("\n%s", message) + } + g.THelper = t.Helper + return g +} + +func (g *Gomega) Ω(atual interface{}, extra ...interface{}) types.Assertion { + return g.ExpectWithOffset(0, atual, extra...) +} + +func (g *Gomega) Expect(atual interface{}, extra ...interface{}) types.Assertion { + return g.ExpectWithOffset(0, atual, extra...) +} + +func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) types.Assertion { + return NewAssertion(actual, g, offset, extra...) +} + +func (g *Gomega) Eventually(actual interface{}, intervals ...interface{}) types.AsyncAssertion { + return g.EventuallyWithOffset(0, actual, intervals...) +} + +func (g *Gomega) EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) types.AsyncAssertion { + timeoutInterval := g.DurationBundle.EventuallyTimeout + pollingInterval := g.DurationBundle.EventuallyPollingInterval + if len(intervals) > 0 { + timeoutInterval = toDuration(intervals[0]) + } + if len(intervals) > 1 { + pollingInterval = toDuration(intervals[1]) + } + + return NewAsyncAssertion(AsyncAssertionTypeEventually, actual, g, timeoutInterval, pollingInterval, offset) +} + +func (g *Gomega) Consistently(actual interface{}, intervals ...interface{}) types.AsyncAssertion { + return g.ConsistentlyWithOffset(0, actual, intervals...) +} + +func (g *Gomega) ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) types.AsyncAssertion { + timeoutInterval := g.DurationBundle.ConsistentlyDuration + pollingInterval := g.DurationBundle.ConsistentlyPollingInterval + if len(intervals) > 0 { + timeoutInterval = toDuration(intervals[0]) + } + if len(intervals) > 1 { + pollingInterval = toDuration(intervals[1]) + } + + return NewAsyncAssertion(AsyncAssertionTypeConsistently, actual, g, timeoutInterval, pollingInterval, offset) +} + +func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) { + g.DurationBundle.EventuallyTimeout = t +} + +func (g *Gomega) SetDefaultEventuallyPollingInterval(t time.Duration) { + g.DurationBundle.EventuallyPollingInterval = t +} + +func (g *Gomega) SetDefaultConsistentlyDuration(t time.Duration) { + g.DurationBundle.ConsistentlyDuration = t +} + +func (g *Gomega) SetDefaultConsistentlyPollingInterval(t time.Duration) { + g.DurationBundle.ConsistentlyPollingInterval = t +} diff --git a/internal/gomega_test.go b/internal/gomega_test.go new file mode 100644 index 000000000..a9fcb1cd3 --- /dev/null +++ b/internal/gomega_test.go @@ -0,0 +1,90 @@ +package internal_test + +import ( + "runtime" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/internal" +) + +var _ = Describe("Gomega", func() { + It("is mostly tested in assertion_test and async_assertion_test", func() { + + }) + Describe("when initialized", func() { + var g *internal.Gomega + + BeforeEach(func() { + g = internal.NewGomega(internal.DurationBundle{}) + Ω(g.Fail).Should(BeNil()) + Ω(g.THelper).Should(BeNil()) + }) + + It("should be registered as unconfigured", func() { + Ω(g.IsConfigured()).Should(BeFalse()) + }) + + Context("when configured with a fail handler", func() { + It("registers the fail handler and a no-op helper", func() { + var capturedMessage string + g.ConfigureWithFailHandler(func(message string, skip ...int) { + capturedMessage = message + }) + Ω(g.IsConfigured()).Should(BeTrue()) + + g.Fail("hi bob") + Ω(capturedMessage).Should(Equal("hi bob")) + Ω(g.THelper).ShouldNot(Panic()) + }) + }) + + Context("when configured with a T", func() { + It("registers a fail handler an the T's helper", func() { + fake := &FakeGomegaTestingT{} + g.ConfigureWithT(fake) + Ω(g.IsConfigured()).Should(BeTrue()) + + g.Fail("hi bob") + Ω(fake.CalledHelper).Should(BeTrue()) + Ω(fake.CalledFatalf).Should(Equal("\nhi bob")) + + fake.CalledHelper = false + g.THelper() + Ω(fake.CalledHelper).Should(BeTrue()) + }) + }) + }) + + Describe("Offset", func() { + It("computes the correct offsets", func() { + doubleNested := func(g Gomega, eventually bool) { + func() { + if eventually { + g.EventuallyWithOffset(2, true, "10ms", "5ms").Should(BeFalse()) + } else { + g.ExpectWithOffset(2, true).To(BeFalse()) + } + }() + } + + reportedFile, reportedLine := "", 0 + _, thisFile, anchorLine, _ := runtime.Caller(0) // 0 + g := NewGomega(func(message string, skip ...int) { // 1 + _, reportedFile, reportedLine, _ = runtime.Caller(skip[0] + 1) // 2 + }) // 3 + g.Expect(true).To(BeFalse()) // *4* + Ω(reportedFile).Should(Equal(thisFile)) // 5 + Ω(reportedLine - anchorLine).Should(Equal(4)) // 6 + doubleNested(g, false) // *7* + Ω(reportedFile).Should(Equal(thisFile)) // 8 + Ω(reportedLine - anchorLine).Should(Equal(7)) // 9 + g.Eventually(true, "10ms", "5ms").Should(BeFalse()) // *10* + Ω(reportedFile).Should(Equal(thisFile)) // 11 + Ω(reportedLine - anchorLine).Should(Equal(10)) // 12 + doubleNested(g, true) // *13* + Ω(reportedFile).Should(Equal(thisFile)) // 14 + Ω(reportedLine - anchorLine).Should(Equal(13)) // 15 + }) + }) +}) diff --git a/internal/internal_suite_test.go b/internal/internal_suite_test.go index 78d70a558..0e2d508f3 100644 --- a/internal/internal_suite_test.go +++ b/internal/internal_suite_test.go @@ -1,13 +1,90 @@ package internal_test import ( + "errors" + "fmt" + "runtime" + "strings" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/onsi/gomega/internal" ) func TestInternal(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Internal Suite") } + +// InstrumentedGomega +type InstrumentedGomega struct { + G *internal.Gomega + FailureMessage string + FailureSkip []int + RegisteredHelpers []string +} + +func NewInstrumentedGomega() *InstrumentedGomega { + out := &InstrumentedGomega{} + + out.G = internal.NewGomega(internal.FetchDefaultDurationBundle()) + out.G.Fail = func(message string, skip ...int) { + out.FailureMessage = message + out.FailureSkip = skip + } + out.G.THelper = func() { + pc, _, _, _ := runtime.Caller(1) + f := runtime.FuncForPC(pc) + funcName := strings.TrimPrefix(f.Name(), "github.com/onsi/gomega/internal.") + out.RegisteredHelpers = append(out.RegisteredHelpers, funcName) + } + + return out +} + +// TestMatcher +var MATCH = "match" +var NO_MATCH = "no match" +var ERR_MATCH = "err match" +var TEST_MATCHER_ERR = errors.New("spec matcher error") + +type SpecMatcher struct{} + +func (matcher SpecMatcher) Match(actual interface{}) (bool, error) { + switch actual { + case MATCH: + return true, nil + case NO_MATCH: + return false, nil + case ERR_MATCH: + return false, TEST_MATCHER_ERR + } + return false, fmt.Errorf("unkown actual %v", actual) +} + +func (matcher SpecMatcher) FailureMessage(actual interface{}) string { + return fmt.Sprintf("positive: %s", actual) +} + +func (matcher SpecMatcher) NegatedFailureMessage(actual interface{}) string { + return fmt.Sprintf("negative: %s", actual) +} + +func SpecMatch() SpecMatcher { + return SpecMatcher{} +} + +//FakeGomegaTestingT +type FakeGomegaTestingT struct { + CalledHelper bool + CalledFatalf string +} + +func (f *FakeGomegaTestingT) Helper() { + f.CalledHelper = true +} + +func (f *FakeGomegaTestingT) Fatalf(s string, args ...interface{}) { + f.CalledFatalf = fmt.Sprintf(s, args...) +} diff --git a/internal/oraclematcher/oracle_matcher.go b/internal/oraclematcher/oracle_matcher.go deleted file mode 100644 index 66cad88a1..000000000 --- a/internal/oraclematcher/oracle_matcher.go +++ /dev/null @@ -1,25 +0,0 @@ -package oraclematcher - -import "github.com/onsi/gomega/types" - -/* -GomegaMatchers that also match the OracleMatcher interface can convey information about -whether or not their result will change upon future attempts. - -This allows `Eventually` and `Consistently` to short circuit if success becomes impossible. - -For example, a process' exit code can never change. So, gexec's Exit matcher returns `true` -for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore. -*/ -type OracleMatcher interface { - MatchMayChangeInTheFuture(actual interface{}) bool -} - -func MatchMayChangeInTheFuture(matcher types.GomegaMatcher, value interface{}) bool { - oracleMatcher, ok := matcher.(OracleMatcher) - if !ok { - return true - } - - return oracleMatcher.MatchMayChangeInTheFuture(value) -} diff --git a/internal/testingtsupport/testing_t_support.go b/internal/testingtsupport/testing_t_support.go deleted file mode 100644 index bb27032f6..000000000 --- a/internal/testingtsupport/testing_t_support.go +++ /dev/null @@ -1,60 +0,0 @@ -package testingtsupport - -import ( - "regexp" - "runtime/debug" - "strings" - - "github.com/onsi/gomega/types" -) - -var StackTracePruneRE = regexp.MustCompile(`\/gomega\/|\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`) - -type EmptyTWithHelper struct{} - -func (e EmptyTWithHelper) Helper() {} - -type gomegaTestingT interface { - Fatalf(format string, args ...interface{}) -} - -func BuildTestingTGomegaFailWrapper(t gomegaTestingT) *types.GomegaFailWrapper { - tWithHelper, hasHelper := t.(types.TWithHelper) - if !hasHelper { - tWithHelper = EmptyTWithHelper{} - } - - fail := func(message string, callerSkip ...int) { - if hasHelper { - tWithHelper.Helper() - t.Fatalf("\n%s", message) - } else { - skip := 2 - if len(callerSkip) > 0 { - skip += callerSkip[0] - } - stackTrace := pruneStack(string(debug.Stack()), skip) - t.Fatalf("\n%s\n%s\n", stackTrace, message) - } - } - - return &types.GomegaFailWrapper{ - Fail: fail, - TWithHelper: tWithHelper, - } -} - -func pruneStack(fullStackTrace string, skip int) string { - stack := strings.Split(fullStackTrace, "\n")[1:] - if len(stack) > 2*skip { - stack = stack[2*skip:] - } - prunedStack := []string{} - for i := 0; i < len(stack)/2; i++ { - if !StackTracePruneRE.Match([]byte(stack[i*2])) { - prunedStack = append(prunedStack, stack[i*2]) - prunedStack = append(prunedStack, stack[i*2+1]) - } - } - return strings.Join(prunedStack, "\n") -} diff --git a/internal/testingtsupport/testing_t_support_test.go b/internal/testingtsupport/testing_t_support_test.go index 8fd8f0a6c..fc509474c 100644 --- a/internal/testingtsupport/testing_t_support_test.go +++ b/internal/testingtsupport/testing_t_support_test.go @@ -1,14 +1,8 @@ package testingtsupport_test import ( - "regexp" - "time" - - "github.com/onsi/gomega/internal/testingtsupport" - . "github.com/onsi/gomega" - "fmt" "testing" ) @@ -17,76 +11,7 @@ func TestTestingT(t *testing.T) { Ω(true).Should(BeTrue()) } -type FakeTWithHelper struct { - LastFatal string -} - -func (f *FakeTWithHelper) Fatalf(format string, args ...interface{}) { - f.LastFatal = fmt.Sprintf(format, args...) -} - -func TestGomegaWithTWithoutHelper(t *testing.T) { - g := NewGomegaWithT(t) - - testingtsupport.StackTracePruneRE = regexp.MustCompile(`\/ginkgo\/`) - - f := &FakeTWithHelper{} - testG := NewGomegaWithT(f) - - testG.Expect("foo").To(Equal("foo")) - g.Expect(f.LastFatal).To(BeZero()) - - testG.Expect("foo").To(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo")) - g.Expect(f.LastFatal).To(ContainSubstring("testingtsupport_test"), "It should include a stacktrace") - - testG.Eventually("foo2", time.Millisecond).Should(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo2")) - - testG.Consistently("foo3", time.Millisecond).Should(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo3")) -} - -type FakeTWithoutHelper struct { - LastFatal string - HelperCount int -} - -func (f *FakeTWithoutHelper) Fatalf(format string, args ...interface{}) { - f.LastFatal = fmt.Sprintf(format, args...) -} - -func (f *FakeTWithoutHelper) Helper() { - f.HelperCount += 1 -} - -func (f *FakeTWithoutHelper) ResetHelper() { - f.HelperCount = 0 -} - -func TestGomegaWithTWithHelper(t *testing.T) { - g := NewGomegaWithT(t) - - f := &FakeTWithoutHelper{} - testG := NewGomegaWithT(f) - - testG.Expect("foo").To(Equal("foo")) - g.Expect(f.LastFatal).To(BeZero()) - g.Expect(f.HelperCount).To(BeNumerically(">", 0)) - f.ResetHelper() - - testG.Expect("foo").To(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo")) - g.Expect(f.LastFatal).NotTo(ContainSubstring("testingtsupport_test"), "It should _not_ include a stacktrace") - g.Expect(f.HelperCount).To(BeNumerically(">", 0)) - f.ResetHelper() - - testG.Eventually("foo2", time.Millisecond).Should(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo2")) - g.Expect(f.HelperCount).To(BeNumerically(">", 0)) - f.ResetHelper() - - testG.Consistently("foo3", time.Millisecond).Should(Equal("bar")) - g.Expect(f.LastFatal).To(ContainSubstring(": foo3")) - g.Expect(f.HelperCount).To(BeNumerically(">", 0)) +func TestNewGomegaWithT(t *testing.T) { + g := NewWithT(t) + g.Expect(true).To(BeTrue()) } diff --git a/matchers/and.go b/matchers/and.go index d83a29164..6bd826adc 100644 --- a/matchers/and.go +++ b/matchers/and.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/onsi/gomega/format" - "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -52,12 +51,12 @@ func (m *AndMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { if m.firstFailedMatcher == nil { // so all matchers succeeded.. Any one of them changing would change the result. for _, matcher := range m.Matchers { - if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) { + if types.MatchMayChangeInTheFuture(matcher, actual) { return true } } return false // none of were going to change } // one of the matchers failed.. it must be able to change in order to affect the result - return oraclematcher.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual) + return types.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual) } diff --git a/matchers/not.go b/matchers/not.go index 2c91670bd..78b71910d 100644 --- a/matchers/not.go +++ b/matchers/not.go @@ -1,7 +1,6 @@ package matchers import ( - "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -26,5 +25,5 @@ func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) } func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { - return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value + return types.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value } diff --git a/matchers/or.go b/matchers/or.go index 3bf799800..841ae26ab 100644 --- a/matchers/or.go +++ b/matchers/or.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/onsi/gomega/format" - "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -54,11 +53,11 @@ func (m *OrMatcher) MatchMayChangeInTheFuture(actual interface{}) bool { if m.firstSuccessfulMatcher != nil { // one of the matchers succeeded.. it must be able to change in order to affect the result - return oraclematcher.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual) + return types.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual) } else { // so all matchers failed.. Any one of them changing would change the result. for _, matcher := range m.Matchers { - if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) { + if types.MatchMayChangeInTheFuture(matcher, actual) { return true } } diff --git a/matchers/with_transform.go b/matchers/with_transform.go index f3dec9101..8a06bd384 100644 --- a/matchers/with_transform.go +++ b/matchers/with_transform.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" - "github.com/onsi/gomega/internal/oraclematcher" "github.com/onsi/gomega/types" ) @@ -77,5 +76,5 @@ func (m *WithTransformMatcher) MatchMayChangeInTheFuture(_ interface{}) bool { // Querying the next matcher is fine if the transformer always will return the same value. // But if the transformer is non-deterministic and returns a different value each time, then there // is no point in querying the next matcher, since it can only comment on the last transformed value. - return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue) + return types.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue) } diff --git a/types/types.go b/types/types.go index ac59a3a5a..c75fcb3cc 100644 --- a/types/types.go +++ b/types/types.go @@ -1,21 +1,35 @@ package types -type TWithHelper interface { - Helper() -} +import ( + "time" +) type GomegaFailHandler func(message string, callerSkip ...int) -type GomegaFailWrapper struct { - Fail GomegaFailHandler - TWithHelper TWithHelper -} - //A simple *testing.T interface wrapper type GomegaTestingT interface { + Helper() Fatalf(format string, args ...interface{}) } +// Gomega represents an object that can perform synchronous and assynchronous assertions with Gomega matchers +type Gomega interface { + Ω(actual interface{}, extra ...interface{}) Assertion + Expect(actual interface{}, extra ...interface{}) Assertion + ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion + + Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion + EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion + + Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion + ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion + + SetDefaultEventuallyTimeout(time.Duration) + SetDefaultEventuallyPollingInterval(time.Duration) + SetDefaultConsistentlyDuration(time.Duration) + SetDefaultConsistentlyPollingInterval(time.Duration) +} + //All Gomega matchers must implement the GomegaMatcher interface // //For details on writing custom matchers, check out: http://onsi.github.io/gomega/#adding-your-own-matchers @@ -24,3 +38,42 @@ type GomegaMatcher interface { FailureMessage(actual interface{}) (message string) NegatedFailureMessage(actual interface{}) (message string) } + +/* +GomegaMatchers that also match the OracleMatcher interface can convey information about +whether or not their result will change upon future attempts. + +This allows `Eventually` and `Consistently` to short circuit if success becomes impossible. + +For example, a process' exit code can never change. So, gexec's Exit matcher returns `true` +for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore. +*/ +type OracleMatcher interface { + MatchMayChangeInTheFuture(actual interface{}) bool +} + +func MatchMayChangeInTheFuture(matcher GomegaMatcher, value interface{}) bool { + oracleMatcher, ok := matcher.(OracleMatcher) + if !ok { + return true + } + + return oracleMatcher.MatchMayChangeInTheFuture(value) +} + +// AsyncAssertions are returned by Eventually and Consistently and enable matchers to be polled repeatedly to ensure +// they are eventually satisfied +type AsyncAssertion interface { + Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool +} + +// Assertions are returned by Ω and Expect and enable assertions against Gomega matchers +type Assertion interface { + Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool + + To(matcher GomegaMatcher, optionalDescription ...interface{}) bool + ToNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool + NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool +}