Skip to content

Commit

Permalink
Eventually and Consistently that are passed a SpecContext can provide…
Browse files Browse the repository at this point in the history
… reports when an interrupt occurs
  • Loading branch information
onsi committed Oct 6, 2022
1 parent 2ba5763 commit 0d063c9
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 7 deletions.
2 changes: 2 additions & 0 deletions docs/index.md
Expand Up @@ -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**.
Expand Down
44 changes: 37 additions & 7 deletions internal/async_assertion.go
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"reflect"
"runtime"
"sync"
"time"

"github.com/onsi/gomega/types"
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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")
Expand All @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions internal/async_assertion_test.go
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 <string>: no match\nto equal\n <string>: match"))
Ω(fakeSpecContext.Cancelled).Should(BeTrue())
})
})
})

Describe("Basic Consistently support", func() {
Expand Down

0 comments on commit 0d063c9

Please sign in to comment.