-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
InterceptEventRecorder()
option.
- Loading branch information
Showing
6 changed files
with
328 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
package testkit | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
|
||
"github.com/dogmatiq/dogma" | ||
"github.com/dogmatiq/testkit/engine" | ||
) | ||
|
||
// EventRecorder is an implementation of dogma.EventRecorder that records events | ||
// within the context of a Test. | ||
// | ||
// Each instance is bound to a particular Test. Use Test.EventRecorder() to | ||
// obtain an instance. | ||
type EventRecorder struct { | ||
m sync.RWMutex | ||
next engine.EventRecorder | ||
interceptor EventRecorderInterceptor | ||
} | ||
|
||
// RecordEvent records the event message m. | ||
// | ||
// It panics unless it is called during an Action, such as when calling | ||
// Test.Prepare() or Test.Expect(). | ||
func (r *EventRecorder) RecordEvent(ctx context.Context, m dogma.Message) error { | ||
r.m.RLock() | ||
defer r.m.RUnlock() | ||
|
||
if r.next.Engine == nil { | ||
panic("RecordEvent(): can not be called outside of a test") | ||
} | ||
|
||
if r.interceptor != nil { | ||
return r.interceptor(ctx, m, r.next) | ||
} | ||
|
||
return r.next.RecordEvent(ctx, m) | ||
} | ||
|
||
// Bind sets the engine and options used to record events. | ||
// | ||
// It is intended for use within Action implementations that support recording | ||
// events outside of a Dogma handler, such as Call(). | ||
// | ||
// It must be called before RecordEvent(), otherwise RecordEvent() panics. | ||
// | ||
// It must be accompanied by a call to Unbind() upon completion of the Action. | ||
func (r *EventRecorder) Bind(eng *engine.Engine, options []engine.OperationOption) { | ||
r.m.Lock() | ||
defer r.m.Unlock() | ||
|
||
r.next.Engine = eng | ||
r.next.Options = options | ||
} | ||
|
||
// Unbind removes the engine and options configured by a prior call to Bind(). | ||
// | ||
// Calls to RecordEvent() on an unbound recorder will cause a panic. | ||
func (r *EventRecorder) Unbind() { | ||
r.m.Lock() | ||
defer r.m.Unlock() | ||
|
||
r.next.Engine = nil | ||
r.next.Options = nil | ||
} | ||
|
||
// Intercept installs an interceptor function that is invoked whenever | ||
// RecordEvent() is called. | ||
// | ||
// If fn is nil the interceptor is removed. | ||
// | ||
// It returns the previous interceptor, if any. | ||
func (r *EventRecorder) Intercept(fn EventRecorderInterceptor) EventRecorderInterceptor { | ||
r.m.Lock() | ||
defer r.m.Unlock() | ||
|
||
prev := r.interceptor | ||
r.interceptor = fn | ||
|
||
return prev | ||
} | ||
|
||
// EventRecorderInterceptor is used by the InterceptEventRecorder() option to | ||
// specify custom behavior for the dogma.EventRecorder returned by | ||
// Test.EventRecorder(). | ||
// | ||
// m is the event being recorded. | ||
// | ||
// e can be used to record the event as it would be recorded without this | ||
// interceptor installed. | ||
type EventRecorderInterceptor func( | ||
ctx context.Context, | ||
m dogma.Message, | ||
r dogma.EventRecorder, | ||
) error | ||
|
||
// InterceptEventRecorder returns an option that causes fn to be called | ||
// whenever an event is recorded via the dogma.EventRecorder returned by | ||
// Test.EventRecorder(). | ||
// | ||
// Intercepting calls to the event recorder allows the user to simulate | ||
// failures (or any other behavior) in the event recorder. | ||
func InterceptEventRecorder(fn EventRecorderInterceptor) interface { | ||
TestOption | ||
CallOption | ||
} { | ||
if fn == nil { | ||
panic("InterceptEventRecorder(<nil>): function must not be nil") | ||
} | ||
|
||
return interceptEventRecorderOption{fn} | ||
} | ||
|
||
// interceptEventRecorderOption is an implementation of both TestOption and | ||
// CallOption that allows the InterceptEventRecorder() option to be used with | ||
// both Test.Begin() and Call(). | ||
type interceptEventRecorderOption struct { | ||
fn EventRecorderInterceptor | ||
} | ||
|
||
func (o interceptEventRecorderOption) applyTestOption(t *Test) { | ||
t.recorder.Intercept(o.fn) | ||
} | ||
|
||
func (o interceptEventRecorderOption) applyCallOption(a *callAction) { | ||
a.onRecord = o.fn | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
package testkit_test | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
|
||
"github.com/dogmatiq/dogma" | ||
. "github.com/dogmatiq/dogma/fixtures" | ||
. "github.com/dogmatiq/testkit" | ||
"github.com/dogmatiq/testkit/internal/testingmock" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("func InterceptEventRecorder()", func() { | ||
var ( | ||
testingT *testingmock.T | ||
app dogma.Application | ||
doNothing EventRecorderInterceptor | ||
recordEventAndReturnError EventRecorderInterceptor | ||
) | ||
|
||
BeforeEach(func() { | ||
testingT = &testingmock.T{} | ||
|
||
app = &Application{ | ||
ConfigureFunc: func(c dogma.ApplicationConfigurer) { | ||
c.Identity("<app>", "<app-key>") | ||
|
||
c.RegisterProcess(&ProcessMessageHandler{ | ||
ConfigureFunc: func(c dogma.ProcessConfigurer) { | ||
c.Identity("<handler-name>", "<handler-key>") | ||
c.ConsumesEventType(MessageE{}) | ||
c.ProducesCommandType(MessageC{}) | ||
}, | ||
RouteEventToInstanceFunc: func( | ||
context.Context, | ||
dogma.Message, | ||
) (string, bool, error) { | ||
return "<instance>", true, nil | ||
}, | ||
HandleEventFunc: func( | ||
_ context.Context, | ||
_ dogma.ProcessRoot, | ||
s dogma.ProcessEventScope, | ||
_ dogma.Message, | ||
) error { | ||
s.ExecuteCommand(MessageC1) | ||
return nil | ||
}, | ||
}) | ||
}, | ||
} | ||
|
||
doNothing = func( | ||
context.Context, | ||
dogma.Message, | ||
dogma.EventRecorder, | ||
) error { | ||
return nil | ||
} | ||
|
||
recordEventAndReturnError = func( | ||
ctx context.Context, | ||
m dogma.Message, | ||
e dogma.EventRecorder, | ||
) error { | ||
Expect(m).To(Equal(MessageE1)) | ||
|
||
err := e.RecordEvent(ctx, m) | ||
Expect(err).ShouldNot(HaveOccurred()) | ||
|
||
return errors.New("<error>") | ||
} | ||
}) | ||
|
||
It("panics if the interceptor function is nil", func() { | ||
Expect(func() { | ||
InterceptEventRecorder(nil) | ||
}).To(PanicWith("InterceptEventRecorder(<nil>): function must not be nil")) | ||
}) | ||
|
||
When("used as a TestOption", func() { | ||
It("intercepts calls to RecordEvent()", func() { | ||
test := Begin( | ||
testingT, | ||
app, | ||
InterceptEventRecorder(recordEventAndReturnError), | ||
) | ||
|
||
test.EnableHandlers("<handler-name>") | ||
|
||
test.Expect( | ||
Call(func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).To(MatchError("<error>")) | ||
}), | ||
ToExecuteCommand(MessageC1), | ||
) | ||
}) | ||
}) | ||
|
||
When("used as a CallOption", func() { | ||
It("intercepts calls to RecordEvent()", func() { | ||
test := Begin( | ||
&testingmock.T{}, | ||
app, | ||
) | ||
|
||
test.EnableHandlers("<handler-name>") | ||
|
||
test.Expect( | ||
Call( | ||
func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).To(MatchError("<error>")) | ||
}, | ||
InterceptEventRecorder(recordEventAndReturnError), | ||
), | ||
ToExecuteCommand(MessageC1), | ||
) | ||
}) | ||
|
||
It("uninstalls the interceptor upon completion of the Call() action", func() { | ||
test := Begin( | ||
&testingmock.T{}, | ||
app, | ||
) | ||
|
||
test.Prepare( | ||
Call( | ||
func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).To(MatchError("<error>")) | ||
}, | ||
InterceptEventRecorder(recordEventAndReturnError), | ||
), | ||
Call( | ||
func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).ShouldNot(HaveOccurred()) | ||
}, | ||
), | ||
) | ||
}) | ||
|
||
It("re-installs the test-level interceptor upon completion of the Call() action", func() { | ||
test := Begin( | ||
&testingmock.T{}, | ||
app, | ||
InterceptEventRecorder(recordEventAndReturnError), | ||
) | ||
|
||
test.Prepare( | ||
Call( | ||
func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).ShouldNot(HaveOccurred()) | ||
}, | ||
InterceptEventRecorder(doNothing), | ||
), | ||
Call( | ||
func() { | ||
err := test.EventRecorder().RecordEvent( | ||
context.Background(), | ||
MessageE1, | ||
) | ||
Expect(err).To(MatchError("<error>")) | ||
}, | ||
), | ||
) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters