From 0d063c92419a649ab0af4ea734d511470702877d Mon Sep 17 00:00:00 2001 From: Onsi Fakhouri Date: Thu, 6 Oct 2022 14:43:30 -0600 Subject: [PATCH] Eventually and Consistently that are passed a SpecContext can provide reports when an interrupt occurs --- docs/index.md | 2 ++ internal/async_assertion.go | 44 +++++++++++++++++++++++++++----- internal/async_assertion_test.go | 27 ++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5cf36b8d3..84db222ad 100644 --- a/docs/index.md +++ b/docs/index.md @@ -369,6 +369,8 @@ It("adds a few books and checks the count", func(ctx SpecContext) { }, SpecTimeout(time.Second * 5)) ``` +In addition, Gingko's `SpecContext` allows Goemga to tell Ginkgo about the status of a currently running `Eventually` whenever a Progress Report is generated. So, if a spec times out while running an `Eventually` Ginkgo will not only show you which `Eventually` was running when the timeout occured, but will also include the failure the `Eventually` was hitting when the timeout occurred. + #### Category 3: Making assertions _in_ the function passed into `Eventually` When testing complex systems it can be valuable to assert that a *set* of assertions passes `Eventually`. `Eventually` supports this by accepting functions that take **a single `Gomega` argument** and **return zero or more values**. diff --git a/internal/async_assertion.go b/internal/async_assertion.go index b84f27191..defa80512 100644 --- a/internal/async_assertion.go +++ b/internal/async_assertion.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "runtime" + "sync" "time" "github.com/onsi/gomega/types" @@ -172,13 +173,19 @@ func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, v return types.MatchMayChangeInTheFuture(matcher, value) } +type contextWithAttachProgressReporter interface { + AttachProgressReporter(func() string) func() +} + func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool { timer := time.Now() timeout := time.After(assertion.timeoutInterval) + lock := sync.Mutex{} var matches bool var err error mayChange := true + value, err := assertion.pollActual() if err == nil { mayChange = assertion.matcherMayChange(matcher, value) @@ -187,7 +194,10 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch assertion.g.THelper() - fail := func(preamble string) { + messageGenerator := func() string { + // can be called out of band by Ginkgo if the user requests a progress report + lock.Lock() + defer lock.Unlock() errMsg := "" message := "" if err != nil { @@ -199,14 +209,22 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch message = matcher.NegatedFailureMessage(value) } } - assertion.g.THelper() description := assertion.buildDescription(optionalDescription...) - assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset) + return fmt.Sprintf("%s%s%s", description, message, errMsg) + } + + fail := func(preamble string) { + assertion.g.THelper() + assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s", preamble, time.Since(timer).Seconds(), messageGenerator()), 3+assertion.offset) } var contextDone <-chan struct{} if assertion.ctx != nil { contextDone = assertion.ctx.Done() + if v, ok := assertion.ctx.Value("GINKGO_SPEC_CONTEXT").(contextWithAttachProgressReporter); ok { + detach := v.AttachProgressReporter(messageGenerator) + defer detach() + } } if assertion.asyncType == AsyncAssertionTypeEventually { @@ -222,10 +240,16 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch select { case <-time.After(assertion.pollingInterval): - value, err = assertion.pollActual() + v, e := assertion.pollActual() + lock.Lock() + value, err = v, e + lock.Unlock() if err == nil { mayChange = assertion.matcherMayChange(matcher, value) - matches, err = matcher.Match(value) + matches, e = matcher.Match(value) + lock.Lock() + err = e + lock.Unlock() } case <-contextDone: fail("Context was cancelled") @@ -248,10 +272,16 @@ func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch select { case <-time.After(assertion.pollingInterval): - value, err = assertion.pollActual() + v, e := assertion.pollActual() + lock.Lock() + value, err = v, e + lock.Unlock() if err == nil { mayChange = assertion.matcherMayChange(matcher, value) - matches, err = matcher.Match(value) + matches, e = matcher.Match(value) + lock.Lock() + err = e + lock.Unlock() } case <-contextDone: fail("Context was cancelled") diff --git a/internal/async_assertion_test.go b/internal/async_assertion_test.go index 173453736..806fd40ca 100644 --- a/internal/async_assertion_test.go +++ b/internal/async_assertion_test.go @@ -11,6 +11,16 @@ import ( "golang.org/x/net/context" ) +type FakeGinkgoSpecContext struct { + Attached func() string + Cancelled bool +} + +func (f *FakeGinkgoSpecContext) AttachProgressReporter(v func() string) func() { + f.Attached = v + return func() { f.Cancelled = true } +} + var _ = Describe("Asynchronous Assertions", func() { var ig *InstrumentedGomega BeforeEach(func() { @@ -207,6 +217,23 @@ var _ = Describe("Asynchronous Assertions", func() { Ω(ig.FailureMessage).Should(ContainSubstring("positive: match")) }) }) + + Context("when the passed-in context is a Ginkgo SpecContext that can take a progress reporter attachment", func() { + It("attaches a progress reporter context that allows it to report on demand", func() { + fakeSpecContext := &FakeGinkgoSpecContext{} + var message string + ctx := context.WithValue(context.Background(), "GINKGO_SPEC_CONTEXT", fakeSpecContext) + ig.G.Eventually(func() string { + if fakeSpecContext.Attached != nil { + message = fakeSpecContext.Attached() + } + return NO_MATCH + }).WithTimeout(time.Millisecond * 20).WithContext(ctx).Should(Equal(MATCH)) + + Ω(message).Should(Equal("Expected\n : no match\nto equal\n : match")) + Ω(fakeSpecContext.Cancelled).Should(BeTrue()) + }) + }) }) Describe("Basic Consistently support", func() {