diff --git a/test-utils/src/main/kotlin/com/r3corda/testing/Expect.kt b/test-utils/src/main/kotlin/com/r3corda/testing/Expect.kt index 7441ea5246..1221ed603d 100644 --- a/test-utils/src/main/kotlin/com/r3corda/testing/Expect.kt +++ b/test-utils/src/main/kotlin/com/r3corda/testing/Expect.kt @@ -91,13 +91,13 @@ inline fun replicate(number: Int, expectation: (Int) -> ExpectCompose): E * @param expectCompose The DSL we expect to match against the stream of events. */ fun Observable.expectEvents(isStrict: Boolean = true, expectCompose: () -> ExpectCompose) = - genericExpectEvents( + serialize().genericExpectEvents( isStrict = isStrict, - stream = { function: (E) -> Unit -> + stream = { action: (E) -> Unit -> val lock = object {} subscribe { event -> synchronized(lock) { - function(event) + action(event) } } }, @@ -113,8 +113,8 @@ fun Observable.expectEvents(isStrict: Boolean = true, expectCompose fun Iterable.expectEvents(isStrict: Boolean = true, expectCompose: () -> ExpectCompose) = genericExpectEvents( isStrict = isStrict, - stream = { function: (E) -> Unit -> - forEach(function) + stream = { action: (E) -> Unit -> + forEach(action) }, expectCompose = expectCompose ) @@ -132,6 +132,16 @@ fun S.genericExpectEvents( expectCompose: () -> ExpectCompose ) { val finishFuture = SettableFuture() + /** + * Internally we create a "lazy" state automaton. The outgoing edges are state.getExpectedEvents() modulo additional + * matching logic. When an event comes we extract the first edge that matches using state.nextState(event), which + * returns the next state and the piece of dsl to be run on the event. If nextState() returns null it means the event + * didn't match at all, in this case we either fail (if isStrict=true) or carry on with the same state (if isStrict=false) + * + * TODO Think about pre-compiling the state automaton, possibly introducing regexp constructs. This requires some + * thinking, as the [parallel] construct blows up the state space factorially, so we need some clever lazy expansion + * of states. + */ var state = ExpectComposeState.fromExpectCompose(expectCompose()) stream { event -> if (state is ExpectComposeState.Finished) { @@ -153,8 +163,10 @@ fun S.genericExpectEvents( } } else { state = next.second + val expectClosure = next.first + // Now run the matching piece of dsl try { - next.first() + expectClosure() } catch (exception: Exception) { finishFuture.setException(exception) } @@ -167,31 +179,36 @@ fun S.genericExpectEvents( } sealed class ExpectCompose { - internal class Single(val expect: Expect) : ExpectCompose() - internal class Sequential(val sequence: List>) : ExpectCompose() - internal class Parallel(val parallel: List>) : ExpectCompose() + internal class Single(val expect: Expect) : ExpectCompose() + internal class Sequential(val sequence: List>) : ExpectCompose() + internal class Parallel(val parallel: List>) : ExpectCompose() } -internal data class Expect( - val clazz: Class, - val match: (E) -> Boolean, - val expectClosure: (E) -> Unit +internal data class Expect( + val clazz: Class, + val match: (T) -> Boolean, + val expectClosure: (T) -> Unit ) private sealed class ExpectComposeState { abstract fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? - abstract fun getExpectedEvents(): List> + abstract fun getExpectedEvents(): List> class Finished : ExpectComposeState() { override fun nextState(event: E) = null - override fun getExpectedEvents(): List> = listOf() + override fun getExpectedEvents(): List> = listOf() } - class Single(val single: ExpectCompose.Single) : ExpectComposeState() { + class Single(val single: ExpectCompose.Single) : ExpectComposeState() { override fun nextState(event: E): Pair<() -> Unit, ExpectComposeState>? = - if (single.expect.clazz.isAssignableFrom(event.javaClass) && single.expect.match(event)) { + if (single.expect.clazz.isAssignableFrom(event.javaClass)) { @Suppress("UNCHECKED_CAST") - Pair({ single.expect.expectClosure(event) }, Finished()) + val coercedEvent = event as T + if (single.expect.match(event)) { + Pair({ single.expect.expectClosure(coercedEvent) }, Finished()) + } else { + null + } } else { null } @@ -252,7 +269,12 @@ private sealed class ExpectComposeState { companion object { fun fromExpectCompose(expectCompose: ExpectCompose): ExpectComposeState { return when (expectCompose) { - is ExpectCompose.Single -> Single(expectCompose) + is ExpectCompose.Single -> { + // This coercion should not be needed but kotlin can't reason about existential type variables(T) + // so here we're coercing T into E (even though T is invariant). + @Suppress("UNCHECKED_CAST") + Single(expectCompose as ExpectCompose.Single) + } is ExpectCompose.Sequential -> { if (expectCompose.sequence.size > 0) { Sequential(expectCompose, 0, fromExpectCompose(expectCompose.sequence[0]))