From bef1003d1376ab49beca3b2ff8f0f5b0f0a36b53 Mon Sep 17 00:00:00 2001 From: Gray-Wind Date: Mon, 7 Mar 2022 18:26:27 +0200 Subject: [PATCH 1/3] Support multiple emits --- Swift/Sources/StateMachine/StateMachine.swift | 18 +++++++-------- .../StateMachineTests/StateMachineTests.swift | 10 ++++----- .../StateMachine_Matter_Tests.swift | 22 ++++++++++--------- .../StateMachine_Turnstile_Tests.swift | 14 ++++++------ 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Swift/Sources/StateMachine/StateMachine.swift b/Swift/Sources/StateMachine/StateMachine.swift index eb2faca..8e52812 100644 --- a/Swift/Sources/StateMachine/StateMachine.swift +++ b/Swift/Sources/StateMachine/StateMachine.swift @@ -13,15 +13,13 @@ open class StateMachine Action { - Action(toState: state, sideEffect: sideEffect) + Action(toState: state, sideEffects: sideEffect) } public static func dontTransition( - emit sideEffect: SideEffect? = nil + emit sideEffect: SideEffect... ) -> Action { - Action(toState: nil, sideEffect: sideEffect) + Action(toState: nil, sideEffects: sideEffect) } public static func onTransition( @@ -288,7 +286,7 @@ public enum StateMachineTypes { fileprivate typealias Factory = (State, Event) throws -> Self fileprivate let toState: State? - fileprivate let sideEffect: SideEffect? + fileprivate let sideEffects: [SideEffect] } public struct IncorrectTypeError: Error, CustomDebugStringConvertible { diff --git a/Swift/Tests/StateMachineTests/StateMachineTests.swift b/Swift/Tests/StateMachineTests/StateMachineTests.swift index 9813b34..6923a2e 100644 --- a/Swift/Tests/StateMachineTests/StateMachineTests.swift +++ b/Swift/Tests/StateMachineTests/StateMachineTests.swift @@ -66,7 +66,7 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .stateOne, event: .eventOne, toState: .stateOne, - sideEffect: .commandOne))) + sideEffects: [.commandOne]))) } func testTransition() throws { @@ -82,7 +82,7 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .stateOne, event: .eventTwo, toState: .stateTwo, - sideEffect: .commandTwo))) + sideEffects: [.commandTwo]))) } func testInvalidTransition() throws { @@ -131,16 +131,16 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { .success(ValidTransition(fromState: .stateOne, event: .eventOne, toState: .stateOne, - sideEffect: .commandOne)), + sideEffects: [.commandOne])), .success(ValidTransition(fromState: .stateOne, event: .eventTwo, toState: .stateTwo, - sideEffect: .commandTwo)), + sideEffects: [.commandTwo])), .failure(InvalidTransition()), .success(ValidTransition(fromState: .stateTwo, event: .eventTwo, toState: .stateTwo, - sideEffect: .commandThree)) + sideEffects: [.commandThree])) ])) } diff --git a/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift b/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift index d2d926e..b3c0bdd 100644 --- a/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift +++ b/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift @@ -58,12 +58,14 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { } } onTransition { - guard case let .success(transition) = $0, let sideEffect = transition.sideEffect else { return } - switch sideEffect { - case .logMelted: logger.log(Message.melted) - case .logFrozen: logger.log(Message.frozen) - case .logVaporized: logger.log(Message.vaporized) - case .logCondensed: logger.log(Message.condensed) + guard case let .success(transition) = $0 else { return } + transition.sideEffects.forEach { sideEffect in + switch sideEffect { + case .logMelted: logger.log(Message.melted) + case .logFrozen: logger.log(Message.frozen) + case .logVaporized: logger.log(Message.vaporized) + case .logCondensed: logger.log(Message.condensed) + } } } } @@ -100,7 +102,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .solid, event: .melt, toState: .liquid, - sideEffect: .logMelted))) + sideEffects: [.logMelted]))) expect(self.logger).to(log(Message.melted)) } @@ -133,7 +135,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .liquid, event: .freeze, toState: .solid, - sideEffect: .logFrozen))) + sideEffects: [.logFrozen]))) expect(self.logger).to(log(Message.frozen)) } @@ -150,7 +152,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .liquid, event: .vaporize, toState: .gas, - sideEffect: .logVaporized))) + sideEffects: [.logVaporized]))) expect(self.logger).to(log(Message.vaporized)) } @@ -167,7 +169,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .gas, event: .condense, toState: .liquid, - sideEffect: .logCondensed))) + sideEffects: [.logCondensed]))) expect(self.logger).to(log(Message.condensed)) } } diff --git a/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift b/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift index 38fbf33..cb0f62c 100644 --- a/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift +++ b/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift @@ -95,7 +95,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .locked(credit: 0), event: .insertCoin(10), toState: .locked(credit: 10), - sideEffect: nil))) + sideEffects: []))) } func test_givenStateIsLocked_whenInsertCoin_andCreditEqualsFarePrice_shouldTransitionToUnlockedStateAndOpenDoors() throws { @@ -111,7 +111,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .locked(credit: 35), event: .insertCoin(15), toState: .unlocked, - sideEffect: .openDoors))) + sideEffects: [.openDoors]))) } func test_givenStateIsLocked_whenInsertCoin_andCreditMoreThanFarePrice_shouldTransitionToUnlockedStateAndOpenDoors() throws { @@ -127,7 +127,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .locked(credit: 35), event: .insertCoin(20), toState: .unlocked, - sideEffect: .openDoors))) + sideEffects: [.openDoors]))) } func test_givenStateIsLocked_whenAdmitPerson_shouldTransitionToLockedStateAndSoundAlarm() throws { @@ -143,7 +143,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .locked(credit: 35), event: .admitPerson, toState: .locked(credit: 35), - sideEffect: .soundAlarm))) + sideEffects: [.soundAlarm]))) } func test_givenStateIsLocked_whenMachineDidFail_shouldTransitionToBrokenStateAndOrderRepair() throws { @@ -159,7 +159,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .locked(credit: 15), event: .machineDidFail, toState: .broken(oldState: .locked(credit: 15)), - sideEffect: .orderRepair))) + sideEffects: [.orderRepair]))) } func test_givenStateIsUnlocked_whenAdmitPerson_shouldTransitionToLockedStateAndCloseDoors() throws { @@ -175,7 +175,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .unlocked, event: .admitPerson, toState: .locked(credit: 0), - sideEffect: .closeDoors))) + sideEffects: [.closeDoors]))) } func test_givenStateIsBroken_whenMachineRepairDidComplete_shouldTransitionToLockedState() throws { @@ -191,7 +191,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .broken(oldState: .locked(credit: 15)), event: .machineRepairDidComplete, toState: .locked(credit: 15), - sideEffect: nil))) + sideEffects: []))) } } From d8c86b93dc9f70c102e986b7188a28ac5fadfba6 Mon Sep 17 00:00:00 2001 From: Gray-Wind Date: Tue, 8 Mar 2022 11:52:42 +0200 Subject: [PATCH 2/3] Add test to make 100% coverage --- Swift/Sources/StateMachine/StateMachine.swift | 2 +- .../StateMachineTests/StateMachineTests.swift | 26 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Swift/Sources/StateMachine/StateMachine.swift b/Swift/Sources/StateMachine/StateMachine.swift index 8e52812..eb359db 100644 --- a/Swift/Sources/StateMachine/StateMachine.swift +++ b/Swift/Sources/StateMachine/StateMachine.swift @@ -13,7 +13,7 @@ open class StateMachine @@ -33,7 +33,7 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { initialState(_state) state(.stateOne) { on(.eventOne) { - dontTransition(emit: .commandOne) + dontTransition(emit: .commandOne, .commandTwo) } on(.eventTwo) { transition(to: .stateTwo, emit: .commandTwo) @@ -43,7 +43,11 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { on(.eventTwo) { dontTransition(emit: .commandThree) } + on(.eventThree) { _, event in + transition(to: .stateThree, emit: .commandFour(try event.string())) + } } + state(.stateThree) } } @@ -66,7 +70,7 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { expect(transition).to(equal(ValidTransition(fromState: .stateOne, event: .eventOne, toState: .stateOne, - sideEffects: [.commandOne]))) + sideEffects: [.commandOne, .commandTwo]))) } func testTransition() throws { @@ -131,7 +135,7 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { .success(ValidTransition(fromState: .stateOne, event: .eventOne, toState: .stateOne, - sideEffects: [.commandOne])), + sideEffects: [.commandOne, .commandTwo])), .success(ValidTransition(fromState: .stateOne, event: .eventTwo, toState: .stateTwo, @@ -191,6 +195,14 @@ final class StateMachineTests: XCTestCase, StateMachineBuilder { // Then expect(error).to(equal(.recursionDetected)) } + + func testGettingNonExistingValue() throws { + // Given + let stateMachine: TestStateMachine = givenState(is: .stateTwo) + + // Then + XCTAssertThrowsError(try stateMachine.transition(.eventThree)) + } } final class Logger { From 578b1c9223d8a69dba95cb71d7d294e7be0ce30e Mon Sep 17 00:00:00 2001 From: Gray-Wind Date: Sat, 26 Feb 2022 10:20:02 +0200 Subject: [PATCH 3/3] Add onEnter and onExit events to states --- Swift/Sources/StateMachine/StateMachine.swift | 65 ++++++++++++++++--- .../StateMachineTests/StateMachineTests.swift | 10 +++ .../StateMachine_Matter_Tests.swift | 52 +++++++++++---- .../StateMachine_Turnstile_Tests.swift | 34 ++++++++++ 4 files changed, 139 insertions(+), 22 deletions(-) diff --git a/Swift/Sources/StateMachine/StateMachine.swift b/Swift/Sources/StateMachine/StateMachine.swift index eb359db..598eb18 100644 --- a/Swift/Sources/StateMachine/StateMachine.swift +++ b/Swift/Sources/StateMachine/StateMachine.swift @@ -54,16 +54,33 @@ open class StateMachine Void + + private var onEnterActions: [State.HashableIdentifier: EnterExitAction] + private var onExitActions: [State.HashableIdentifier: EnterExitAction] + private var isNotifying: Bool = false public init(@DefinitionBuilder build: () -> Definition) { let definition: Definition = build() state = definition.initialState.state - states = definition.states.reduce(into: States()) { - $0[$1.state] = $1.events.reduce(into: Events()) { - $0[$1.event] = $1.action + var enterActions: [State.HashableIdentifier: EnterExitAction] = [:] + var exitActions: [State.HashableIdentifier: EnterExitAction] = [:] + states = definition.states.reduce(into: States()) { result, tuple in + let (state, events) = tuple + result[state] = events.reduce(into: Events()) { + switch $1.eventType { + case .onEnter(let action): + enterActions[state] = action + case .onExit(let action): + exitActions[state] = action + case .normal(let event, let action): + $0[event] = action + } } } + onEnterActions = enterActions + onExitActions = exitActions observers = definition.callbacks.map { Observer(object: self, callback: $0) } @@ -104,10 +121,18 @@ open class StateMachine Void) -> [EventHandler] { + [EventHandler(eventType: .onEnter(perform))] + } + + public static func onExit(_ perform: @escaping (State) throws -> Void) -> [EventHandler] { + [EventHandler(eventType: .onExit(perform))] + } + + public static func onEnter(_ perform: @escaping () throws -> Void) -> [EventHandler] { + [EventHandler(eventType: .onEnter({ _ in try perform() }))] + } + + public static func onExit(_ perform: @escaping () throws -> Void) -> [EventHandler] { + [EventHandler(eventType: .onExit({ _ in try perform() }))] + } + public static func on( _ event: Event.HashableIdentifier, perform: @escaping (State, Event) throws -> Action ) -> [EventHandler] { - [EventHandler(event: event, action: perform)] + [EventHandler(eventType: .normal(event, perform))] } public static func on( _ event: Event.HashableIdentifier, perform: @escaping (State) throws -> Action ) -> [EventHandler] { - [EventHandler(event: event) { state, _ in try perform(state) }] + [EventHandler(eventType: .normal(event, { state, _ in try perform(state) }))] } public static func on( _ event: Event.HashableIdentifier, perform: @escaping () throws -> Action ) -> [EventHandler] { - [EventHandler(event: event) { _, _ in try perform() }] + [EventHandler(eventType: .normal(event, { _, _ in try perform() }))] } public static func transition( @@ -277,8 +318,16 @@ public enum StateMachineTypes { public struct EventHandler { - fileprivate let event: Event.HashableIdentifier - fileprivate let action: Action.Factory + fileprivate var eventType: EventType + + fileprivate enum EventType { + + fileprivate typealias EnterExitAction = (State) throws -> Void + + case normal(Event.HashableIdentifier, Action.Factory) + case onEnter(EnterExitAction) + case onExit(EnterExitAction) + } } public struct Action { diff --git a/Swift/Tests/StateMachineTests/StateMachineTests.swift b/Swift/Tests/StateMachineTests/StateMachineTests.swift index aa71b36..5c3aade 100644 --- a/Swift/Tests/StateMachineTests/StateMachineTests.swift +++ b/Swift/Tests/StateMachineTests/StateMachineTests.swift @@ -224,3 +224,13 @@ func log(_ expectedMessages: String...) -> Predicate { return PredicateResult(bool: actualMessages == expectedMessages, message: message) } } + +func noLog() -> Predicate { + return Predicate { + let actualMessages: [String]? = try $0.evaluate()?.messages + let actualString: String = stringify(actualMessages?.joined(separator: "\\n")) + let message: ExpectationMessage = .expectedCustomValueTo("no logs", + actual: "<\(actualString)>") + return PredicateResult(bool: actualString.count == 0, message: message) + } +} diff --git a/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift b/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift index b3c0bdd..a64e0a2 100644 --- a/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift +++ b/Swift/Tests/StateMachineTests/StateMachine_Matter_Tests.swift @@ -28,23 +28,41 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { typealias ValidTransition = MatterStateMachine.Transition.Valid typealias InvalidTransition = MatterStateMachine.Transition.Invalid - enum Message { - - static let melted: String = "I melted" - static let frozen: String = "I froze" - static let vaporized: String = "I vaporized" - static let condensed: String = "I condensed" + enum Message: String { + + case melted = "I melted" + case frozen = "I froze" + case vaporized = "I vaporized" + case condensed = "I condensed" + case enteredSolid + case exitedSolid + case enteredLiquid + case exitedLiquid + case enteredGas + case exitedGas } static func matterStateMachine(withInitialState _state: State, logger: Logger) -> MatterStateMachine { MatterStateMachine { initialState(_state) state(.solid) { + onEnter { _ in + logger.log(Message.enteredSolid.rawValue) + } + onExit { _ in + logger.log(Message.exitedSolid.rawValue) + } on(.melt) { transition(to: .liquid, emit: .logMelted) } } state(.liquid) { + onEnter { _ in + logger.log(Message.enteredLiquid.rawValue) + } + onExit { _ in + logger.log(Message.exitedLiquid.rawValue) + } on(.freeze) { transition(to: .solid, emit: .logFrozen) } @@ -53,6 +71,12 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { } } state(.gas) { + onEnter { _ in + logger.log(Message.enteredGas.rawValue) + } + onExit { _ in + logger.log(Message.exitedGas.rawValue) + } on(.condense) { transition(to: .liquid, emit: .logCondensed) } @@ -61,10 +85,10 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { guard case let .success(transition) = $0 else { return } transition.sideEffects.forEach { sideEffect in switch sideEffect { - case .logMelted: logger.log(Message.melted) - case .logFrozen: logger.log(Message.frozen) - case .logVaporized: logger.log(Message.vaporized) - case .logCondensed: logger.log(Message.condensed) + case .logMelted: logger.log(Message.melted.rawValue) + case .logFrozen: logger.log(Message.frozen.rawValue) + case .logVaporized: logger.log(Message.vaporized.rawValue) + case .logCondensed: logger.log(Message.condensed.rawValue) } } } @@ -103,7 +127,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { event: .melt, toState: .liquid, sideEffects: [.logMelted]))) - expect(self.logger).to(log(Message.melted)) + expect(self.logger).to(log(Message.exitedSolid.rawValue, Message.enteredLiquid.rawValue, Message.melted.rawValue)) } func test_givenStateIsSolid_whenFrozen_shouldThrowInvalidTransitionError() throws { @@ -136,7 +160,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { event: .freeze, toState: .solid, sideEffects: [.logFrozen]))) - expect(self.logger).to(log(Message.frozen)) + expect(self.logger).to(log(Message.exitedLiquid.rawValue, Message.enteredSolid.rawValue, Message.frozen.rawValue)) } func test_givenStateIsLiquid_whenVaporized_shouldTransitionToGasState() throws { @@ -153,7 +177,7 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { event: .vaporize, toState: .gas, sideEffects: [.logVaporized]))) - expect(self.logger).to(log(Message.vaporized)) + expect(self.logger).to(log(Message.exitedLiquid.rawValue, Message.enteredGas.rawValue, Message.vaporized.rawValue)) } func test_givenStateIsGas_whenCondensed_shouldTransitionToLiquidState() throws { @@ -170,6 +194,6 @@ final class StateMachine_Matter_Tests: XCTestCase, StateMachineBuilder { event: .condense, toState: .liquid, sideEffects: [.logCondensed]))) - expect(self.logger).to(log(Message.condensed)) + expect(self.logger).to(log(Message.exitedGas.rawValue, Message.enteredLiquid.rawValue, Message.condensed.rawValue)) } } diff --git a/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift b/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift index cb0f62c..4b8f4a9 100644 --- a/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift +++ b/Swift/Tests/StateMachineTests/StateMachine_Turnstile_Tests.swift @@ -32,10 +32,25 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { typealias TurnstileStateMachine = StateMachine typealias ValidTransition = TurnstileStateMachine.Transition.Valid + enum Message: String { + case enteredLocked + case exitedLocked + case enteredUnlocked + case exitedUnlocked + case enteredBroken + case exitedBroken + } + static func turnstileStateMachine(withInitialState _state: State, logger: Logger) -> TurnstileStateMachine { TurnstileStateMachine { initialState(_state) state(.locked) { + onEnter { state in + logger.log("\(Message.enteredLocked.rawValue) \(try state.credit() as Int)") + } + onExit { + logger.log(Message.exitedLocked.rawValue) + } on(.insertCoin) { locked, insertCoin in let newCredit: Int = try locked.credit() + insertCoin.value() if newCredit >= Constant.farePrice { @@ -52,11 +67,23 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { } } state(.unlocked) { + onEnter { + logger.log(Message.enteredUnlocked.rawValue) + } + onExit { + logger.log(Message.exitedUnlocked.rawValue) + } on(.admitPerson) { transition(to: .locked(credit: 0), emit: .closeDoors) } } state(.broken) { + onEnter { + logger.log(Message.enteredBroken.rawValue) + } + onExit { + logger.log(Message.exitedBroken.rawValue) + } on(.machineRepairDidComplete) { broken in transition(to: try broken.oldState()) } @@ -96,6 +123,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .insertCoin(10), toState: .locked(credit: 10), sideEffects: []))) + expect(self.logger).to(log(Message.exitedLocked.rawValue, "\(Message.enteredLocked.rawValue) 10")) } func test_givenStateIsLocked_whenInsertCoin_andCreditEqualsFarePrice_shouldTransitionToUnlockedStateAndOpenDoors() throws { @@ -112,6 +140,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .insertCoin(15), toState: .unlocked, sideEffects: [.openDoors]))) + expect(self.logger).to(log(Message.exitedLocked.rawValue, Message.enteredUnlocked.rawValue)) } func test_givenStateIsLocked_whenInsertCoin_andCreditMoreThanFarePrice_shouldTransitionToUnlockedStateAndOpenDoors() throws { @@ -128,6 +157,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .insertCoin(20), toState: .unlocked, sideEffects: [.openDoors]))) + expect(self.logger).to(log(Message.exitedLocked.rawValue, Message.enteredUnlocked.rawValue)) } func test_givenStateIsLocked_whenAdmitPerson_shouldTransitionToLockedStateAndSoundAlarm() throws { @@ -144,6 +174,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .admitPerson, toState: .locked(credit: 35), sideEffects: [.soundAlarm]))) + expect(self.logger).to(noLog()) } func test_givenStateIsLocked_whenMachineDidFail_shouldTransitionToBrokenStateAndOrderRepair() throws { @@ -160,6 +191,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .machineDidFail, toState: .broken(oldState: .locked(credit: 15)), sideEffects: [.orderRepair]))) + expect(self.logger).to(log(Message.exitedLocked.rawValue, Message.enteredBroken.rawValue)) } func test_givenStateIsUnlocked_whenAdmitPerson_shouldTransitionToLockedStateAndCloseDoors() throws { @@ -176,6 +208,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .admitPerson, toState: .locked(credit: 0), sideEffects: [.closeDoors]))) + expect(self.logger).to(log(Message.exitedUnlocked.rawValue, "\(Message.enteredLocked.rawValue) 0")) } func test_givenStateIsBroken_whenMachineRepairDidComplete_shouldTransitionToLockedState() throws { @@ -192,6 +225,7 @@ final class StateMachine_Turnstile_Tests: XCTestCase, StateMachineBuilder { event: .machineRepairDidComplete, toState: .locked(credit: 15), sideEffects: []))) + expect(self.logger).to(log(Message.exitedBroken.rawValue, "\(Message.enteredLocked.rawValue) 15")) } }