diff --git a/Code/TKStateMachine.h b/Code/TKStateMachine.h index a734ac9..9319658 100644 --- a/Code/TKStateMachine.h +++ b/Code/TKStateMachine.h @@ -76,6 +76,13 @@ */ @property (nonatomic, strong, readonly) TKState *currentState; +/** + The set of states which are terminal, and will cause the state machine to terminate + + Once an event transitions the machine into one of the states in this set, the machine will return YES from -terminated and will refuse to accept any further events. + */ +@property (nonatomic, copy) NSSet *terminalStates; + /** Adds a state to the receiver. @@ -176,6 +183,15 @@ */ - (BOOL)isActive; +///------------------------------------ +/// @name Terminal State of the Machine +///------------------------------------ + +/** + Returns a Boolean value that indicates if the receiver has transitioned into one of the states in `terminalStates`. + */ +@property (nonatomic, readonly) BOOL terminated; + ///-------------------- /// @name Firing Events ///-------------------- @@ -236,10 +252,17 @@ extern NSString *const TKStateMachineDidChangeStateEventUserInfoKey; */ extern NSString *const TKStateMachineIsImmutableException; +/** + A Notification posted when the `terminated` state of a `TKStateMachine` changes to YES + */ +extern NSString *const TKStateMachineDidTerminateNotification; + + /** Error Codes */ typedef enum { TKInvalidTransitionError = 1000, // An invalid transition was attempted. TKTransitionDeclinedError = 1001, // The transition was declined by the `shouldFireEvent` guard block. + TKStateMachineTerminatedError = 1002, // The transition failed because the state machine has reached a terminal state. } TKErrorCode; diff --git a/Code/TKStateMachine.m b/Code/TKStateMachine.m index 94f836c..b9ca3eb 100644 --- a/Code/TKStateMachine.m +++ b/Code/TKStateMachine.m @@ -41,6 +41,7 @@ @interface TKState () NSString *const TKStateMachineDidChangeStateOldStateUserInfoKey = @"old"; NSString *const TKStateMachineDidChangeStateNewStateUserInfoKey = @"new"; NSString *const TKStateMachineDidChangeStateEventUserInfoKey = @"event"; +NSString *const TKStateMachineDidTerminateNotification = @"TKStateMachineDidTerminateNotification"; NSString *const TKStateMachineIsImmutableException = @"TKStateMachineIsImmutableException"; @@ -56,6 +57,7 @@ @interface TKStateMachine () @property (nonatomic, strong) NSMutableSet *mutableStates; @property (nonatomic, strong) NSMutableSet *mutableEvents; @property (nonatomic, assign, getter = isActive) BOOL active; +@property (nonatomic) BOOL terminated; @property (nonatomic, strong, readwrite) TKState *currentState; @end @@ -73,6 +75,10 @@ + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key NSSet *affectingKey = [NSSet setWithObject:@"mutableEvents"]; keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKey]; return keyPaths; + } else if ([key isEqualToString:@"terminated"]) { + NSSet *affectingKey = [NSSet setWithObject:@"currentState"]; + keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKey]; + return keyPaths; } return keyPaths; @@ -82,8 +88,9 @@ - (id)init { self = [super init]; if (self) { - self.mutableStates = [NSMutableSet set]; - self.mutableEvents = [NSMutableSet set]; + _mutableStates = [NSMutableSet set]; + _mutableEvents = [NSMutableSet set]; + _terminalStates = [NSSet set]; } return self; } @@ -98,9 +105,35 @@ - (NSString *)description - (void)setInitialState:(TKState *)initialState { TKRaiseIfActive(); + if (! [initialState isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object, instead got a `%@` (%@)", [initialState class], initialState]; _initialState = initialState; } +- (void)setTerminalStates:(NSSet*)terminalStates +{ + TKRaiseIfActive(); + + if (terminalStates && ! [terminalStates isKindOfClass: [NSSet class]]) [NSException raise:NSInvalidArgumentException format:@"Expected an `NSSet` object specifying the terminal states, instead got a `%@` (%@)", [terminalStates class], terminalStates]; + + for (TKState* state in terminalStates) + { + if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected an `NSSet` of `TKState` objects, but the set contains a `%@` (%@)", [state class], state]; + } + + [self.mutableStates unionSet: terminalStates]; + + _terminalStates = terminalStates ? [NSSet setWithSet: terminalStates] : [NSSet set]; +} + +- (void)setCurrentState: (TKState*)newCurrentState +{ + _currentState = newCurrentState; + if ([self.terminalStates containsObject: newCurrentState]) + { + self.terminated = YES; + } +} + - (NSSet *)states { return [NSSet setWithSet:self.mutableStates]; @@ -109,7 +142,7 @@ - (NSSet *)states - (void)addState:(TKState *)state { TKRaiseIfActive(); - if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object, instead got a `%@` (%@)", [state class], state]; + if (! [state isKindOfClass:[TKState class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKState` object or `NSString` object specifying the name of a state, instead got a `%@` (%@)", [state class], state]; if (self.initialState == nil) self.initialState = state; [self.mutableStates addObject:state]; } @@ -190,7 +223,7 @@ - (BOOL)canFireEvent:(id)eventOrEventName if (! [eventOrEventName isKindOfClass:[TKEvent class]] && ![eventOrEventName isKindOfClass:[NSString class]]) [NSException raise:NSInvalidArgumentException format:@"Expected a `TKEvent` object or `NSString` object specifying the name of an event, instead got a `%@` (%@)", [eventOrEventName class], eventOrEventName]; TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName]; if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName]; - return [event.sourceStates containsObject:self.currentState]; + return !self.terminated && (event.sourceStates == nil || [event.sourceStates containsObject:self.currentState]); } - (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:(NSError *__autoreleasing *)error @@ -200,6 +233,17 @@ - (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:( TKEvent *event = [eventOrEventName isKindOfClass:[TKEvent class]] ? eventOrEventName : [self eventNamed:eventOrEventName]; if (! event) [NSException raise:NSInvalidArgumentException format:@"Cannot find an Event named '%@'", eventOrEventName]; + if (self.terminated) + { + if (error) + { + NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event after the state machine has terminated.", event.name]; + NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"The event cannot be fired because the state machine has reached a terminal state", NSLocalizedFailureReasonErrorKey: failureReason }; + *error = [NSError errorWithDomain:TKErrorDomain code:TKStateMachineTerminatedError userInfo:userInfo]; + } + return NO; + } + // Check that this transition is permitted if (event.sourceStates != nil && ![event.sourceStates containsObject:self.currentState]) { NSString *failureReason = [NSString stringWithFormat:@"An attempt was made to fire the '%@' event while in the '%@' state, but the event can only be fired from the following states: %@", event.name, self.currentState.name, [[event.sourceStates valueForKey:@"name"] componentsJoinedByString:@", "]]; @@ -237,6 +281,11 @@ - (BOOL)fireEvent:(id)eventOrEventName userInfo:(NSDictionary *)userInfo error:( TKStateMachineDidChangeStateEventUserInfoKey: event }]; [[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidChangeStateNotification object:self userInfo:notificationInfo]; + if(self.terminated) + { + [[NSNotificationCenter defaultCenter] postNotificationName:TKStateMachineDidTerminateNotification object:self userInfo:userInfo]; + } + return YES; } @@ -254,6 +303,8 @@ - (id)initWithCoder:(NSCoder *)aDecoder self.mutableStates = [[aDecoder decodeObjectForKey:@"states"] mutableCopy]; self.mutableEvents = [[aDecoder decodeObjectForKey:@"events"] mutableCopy]; self.active = [aDecoder decodeBoolForKey:@"isActive"]; + self.terminalStates = [aDecoder decodeObjectForKey: @"terminalStates"]; + self.terminated = [aDecoder decodeBoolForKey: @"terminated"]; return self; } @@ -264,6 +315,8 @@ - (void)encodeWithCoder:(NSCoder *)aCoder [aCoder encodeObject:self.states forKey:@"states"]; [aCoder encodeObject:self.events forKey:@"events"]; [aCoder encodeBool:self.isActive forKey:@"isActive"]; + [aCoder encodeObject:self.terminalStates forKey: @"terminalStates"]; + [aCoder encodeBool:self.terminated forKey:@"terminated"]; } #pragma mark - NSCopying @@ -272,6 +325,7 @@ - (id)copyWithZone:(NSZone *)zone { TKStateMachine *copiedStateMachine = [[[self class] allocWithZone:zone] init]; copiedStateMachine.active = NO; + copiedStateMachine.terminated = NO; copiedStateMachine.currentState = nil; copiedStateMachine.initialState = self.initialState; @@ -279,6 +333,12 @@ - (id)copyWithZone:(NSZone *)zone [copiedStateMachine addState:[state copy]]; } + NSMutableSet* terminalStates = [NSMutableSet setWithCapacity: self.terminalStates.count]; + for (TKState *state in self.terminalStates) { + [terminalStates addObject: [copiedStateMachine stateNamed: state.name]]; + } + copiedStateMachine.terminalStates = terminalStates; + for (TKEvent *event in self.events) { NSMutableArray *sourceStates = [NSMutableArray arrayWithCapacity:[event.sourceStates count]]; for (TKState *sourceState in event.sourceStates) { @@ -288,6 +348,7 @@ - (id)copyWithZone:(NSZone *)zone TKEvent *copiedEvent = [TKEvent eventWithName:event.name transitioningFromStates:sourceStates toState:destinationState]; [copiedStateMachine addEvent:copiedEvent]; } + return copiedStateMachine; } diff --git a/Specs/TKStateMachineSpec.m b/Specs/TKStateMachineSpec.m index eed3552..f1e912c 100644 --- a/Specs/TKStateMachineSpec.m +++ b/Specs/TKStateMachineSpec.m @@ -25,6 +25,7 @@ @interface TKSpecPerson : NSObject @property (nonatomic, assign, getter = isHappy) BOOL happy; @property (nonatomic, assign, getter = isLookingForLove) BOOL lookingForLove; @property (nonatomic, assign, getter = isDepressed) BOOL depressed; +@property (nonatomic, assign, getter = isDead) BOOL dead; @property (nonatomic, assign, getter = isConsultingLawyer) BOOL consultingLawyer; @property (nonatomic, assign, getter = wasPreviouslyMarried) BOOL previouslyMarried; @property (nonatomic, assign, getter = isWillingToGiveUpHalfOfEverything) BOOL willingToGiveUpHalfOfEverything; @@ -52,9 +53,21 @@ - (void)startTryingToPickUpCollegeGirls {} it(@"has no states", ^{ [[stateMachine.states should] haveCountOf:0]; }); + + it(@"is not active", ^{ + [[@(stateMachine.isActive) should] beNo]; + }); + it(@"is not terminated", ^{ + [[@(stateMachine.terminated) should] beNo]; + }); + + it(@"has no terminal states", ^{ + [[stateMachine.terminalStates should] haveCountOf:0]; + }); + it(@"has a nil initial state", ^{ - [stateMachine.initialState shouldBeNil]; + [[stateMachine.initialState should] beNil]; }); it(@"has no events", ^{ @@ -87,6 +100,31 @@ - (void)startTryingToPickUpCollegeGirls {} }); }); + context(@"and a terminal state is added", ^{ + __block TKState *state = nil; + + beforeEach(^{ + state = [TKState stateWithName:@"Dead"]; + [stateMachine setTerminalStates: [NSSet setWithObject: state]]; + }); + + it(@"has a terminalStates count of 1", ^{ + [[stateMachine.terminalStates should] haveCountOf:1]; + }); + + it(@"contains the terminalState that was added", ^{ + [[stateMachine.terminalStates should] contain:state]; + }); + + it(@"adds the terminalState to the main state list too", ^{ + [[stateMachine.states should] contain:state]; + }); + + it(@"does NOT set the initial state to the newly added terminalState", ^{ + [[state shouldNot] equal: stateMachine.initialState]; + }); + }); + context(@"when an event is added", ^{ __block TKEvent *event = nil; __block TKState *singleState = nil; @@ -129,13 +167,16 @@ - (void)startTryingToPickUpCollegeGirls {} context(@"when a state machine is copied", ^{ __block TKState *firstState; __block TKState *secondState; + __block TKState *lastState; __block TKEvent *event; __block TKStateMachine *copiedStateMachine; beforeEach(^{ firstState = [TKState stateWithName:@"First"]; secondState = [TKState stateWithName:@"Second"]; + lastState = [TKState stateWithName:@"Last"]; [stateMachine addStates:@[ firstState, secondState ]]; + stateMachine.terminalStates = [NSSet setWithObject: lastState]; event = [TKEvent eventWithName:@"Event" transitioningFromStates:@[ firstState ] toState:secondState]; [stateMachine addEvent:event]; @@ -150,11 +191,18 @@ - (void)startTryingToPickUpCollegeGirls {} }); it(@"copies all states", ^{ - [[copiedStateMachine.states should] haveCountOf:2]; + [[copiedStateMachine.states should] haveCountOf:3]; [[copiedStateMachine.states shouldNot] contain:firstState]; [[copiedStateMachine.states shouldNot] contain:secondState]; + [[copiedStateMachine.states shouldNot] contain:lastState]; }); + it(@"copies terminal states", ^{ + [[copiedStateMachine.terminalStates should] haveCountOf:1]; + [[copiedStateMachine.terminalStates shouldNot] contain:lastState]; + [[copiedStateMachine.terminalStates should] contain: [copiedStateMachine stateNamed: lastState.name]]; + }); + it(@"copies all events", ^{ [[copiedStateMachine.events should] haveCountOf:1]; [[copiedStateMachine.events shouldNot] contain:event]; @@ -165,8 +213,13 @@ - (void)startTryingToPickUpCollegeGirls {} }); it(@"has a `nil` current state", ^{ - [copiedStateMachine.currentState shouldBeNil]; + [[copiedStateMachine.currentState should] beNil]; }); + + it(@"is not terminated", ^{ + [[@(copiedStateMachine.terminated) should] beNo]; + }); + }); context(@"when a state machine is serialized", ^{ @@ -191,7 +244,7 @@ - (void)startTryingToPickUpCollegeGirls {} }); it(@"does not have a current state", ^{ - [stateMachine.currentState shouldBeNil]; + [[stateMachine.currentState should] beNil]; }); context(@"and then is started", ^{ @@ -223,6 +276,51 @@ - (void)startTryingToPickUpCollegeGirls {} }); }); +describe(@"setInitialState:", ^{ + context(@"when given an object that is not a TKState", ^{ + it(@"raises an NSInvalidArgumentException", ^{ + [[theBlock(^{ + [stateMachine setInitialState:(TKState *)@1234]; + }) should] raiseWithName:NSInvalidArgumentException reason:@"Expected a `TKState` object, instead got a `__NSCFNumber` (1234)"]; + }); + }); +}); + +describe(@"setTerminalStates:", ^{ + context(@"when given an object that is not an NSSet", ^{ + it(@"raises an NSInvalidArgumentException", ^{ + [[theBlock(^{ + [stateMachine setTerminalStates:(NSSet *)@1234]; + }) should] raiseWithName:NSInvalidArgumentException reason:@"Expected an `NSSet` object specifying the terminal states, instead got a `__NSCFNumber` (1234)"]; + }); + }); +}); + +describe(@"setTerminalStates:", ^{ + context(@"when given a set containing an object that is not a TKState", ^{ + it(@"raises an NSInvalidArgumentException", ^{ + [[theBlock(^{ + [stateMachine setTerminalStates: [NSSet setWithObject: @1234]]; + }) should] raiseWithName:NSInvalidArgumentException reason:@"Expected an `NSSet` of `TKState` objects, but the set contains a `__NSCFNumber` (1234)"]; + }); + }); +}); + +describe(@"setTerminalStates:", ^{ + context(@"when given nil", ^{ + stateMachine.terminalStates = nil; + [[stateMachine.terminalStates should] beNonNil]; + [[stateMachine.terminalStates should] haveCountOf: 0]; + [[stateMachine.terminalStates should] beMemberOfClass: [NSSet class]]; + it(@"raises an NSInvalidArgumentException", ^{ + [[theBlock(^{ + [stateMachine setTerminalStates: [NSSet setWithObject: @1234]]; + }) should] raiseWithName:NSInvalidArgumentException reason:@"Expected an `NSSet` of `TKState` objects, but the set contains a `__NSCFNumber` (1234)"]; + }); + }); +}); + + describe(@"isInState:", ^{ context(@"when given an object that is not a TKState or an NSString", ^{ it(@"raises an NSInvalidArgumentException", ^{ @@ -244,12 +342,17 @@ - (void)startTryingToPickUpCollegeGirls {} describe(@"fireEvent:userInfo:error", ^{ __block TKState *singleState; __block TKState *datingState; + __block TKState *deadState; beforeEach(^{ singleState = [TKState stateWithName:@"Single"]; datingState = [TKState stateWithName:@"Dating"]; + deadState = [TKState stateWithName:@"Dead"]; [stateMachine addStates:@[ singleState, datingState ]]; + stateMachine.terminalStates = [NSSet setWithObject:deadState]; [stateMachine addEvent:[TKEvent eventWithName:@"Break Up" transitioningFromStates:@[ datingState ] toState:singleState]]; + [stateMachine addEvent:[TKEvent eventWithName:@"Death" transitioningFromStates:nil toState:deadState]]; + [stateMachine addEvent:[TKEvent eventWithName:@"Resurrection" transitioningFromStates: @[ deadState ] toState: singleState]]; stateMachine.initialState = [stateMachine stateNamed:@"Dating"]; [stateMachine activate]; }); @@ -270,6 +373,60 @@ - (void)startTryingToPickUpCollegeGirls {} [[blockTransition.event.name should] equal:@"Break Up"]; }); + it(@"does not transition into a terminated state after entering a non-terminal state", ^{ + NSError *error = nil; + BOOL success = [stateMachine fireEvent:@"Break Up" userInfo:nil error:&error]; + [[theValue(success) should] beTrue]; + + BOOL terminated = stateMachine.terminated; + [[theValue(terminated) should] beFalse]; + }); + + it(@"transitions into a terminated state after entering a terminal state", ^{ + NSError *error = nil; + BOOL success = [stateMachine fireEvent:@"Death" userInfo:nil error:&error]; + [[theValue(success) should] beTrue]; + [[error should] beNil]; + + BOOL terminated = stateMachine.terminated; + [[theValue(terminated) should] beTrue]; + }); + + it(@"should refuse further events after being terminated", ^{ + NSError *error = nil; + BOOL success = [stateMachine fireEvent:@"Death" userInfo:nil error:&error]; + [[theValue(success) should] beTrue]; + [[error should] beNil]; + + BOOL terminated = stateMachine.terminated; + [[theValue(terminated) should] beTrue]; + + BOOL canFire = [stateMachine canFireEvent: @"Resurrection"]; + [[theValue(canFire) should] beFalse]; + + success = [stateMachine fireEvent:@"Resurrection" userInfo:nil error: &error]; + [[theValue(success) should] beFalse]; + [[error should] beNonNil]; + [[error.domain should] equal: TKErrorDomain]; + [[@(error.code) should] equal: @(TKStateMachineTerminatedError)]; + }); + + it(@"should send a notification when entering a terminal state", ^{ + __block NSNotification *notification = nil; + id observer = [[NSNotificationCenter defaultCenter] addObserverForName:TKStateMachineDidChangeStateNotification object:stateMachine queue:nil usingBlock:^(NSNotification *note) { + notification = note; + }]; + + NSError *error = nil; + BOOL success = [stateMachine fireEvent:@"Death" userInfo:nil error:&error]; + + [[theValue(success) should] beTrue]; + [[error should] beNil]; + [[expectFutureValue(notification) shouldEventually] beNonNil]; + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + }); + + context(@"with userInfo", ^{ it(@"includes the userInfo in the transition", ^{ __block TKTransition *blockTransition; @@ -289,6 +446,20 @@ - (void)startTryingToPickUpCollegeGirls {} [[NSNotificationCenter defaultCenter] removeObserver:observer]; [[notification.userInfo[@"songPlayingOnRepeat"] should] equal:@"What is love, when you don't hurt me?"]; }); + + it(@"merges the userInfo when posting the notification for termination", ^{ + __block NSNotification *notification = nil; + id observer = [[NSNotificationCenter defaultCenter] addObserverForName:TKStateMachineDidChangeStateNotification object:stateMachine queue:nil usingBlock:^(NSNotification *note) { + notification = note; + }]; + + [stateMachine fireEvent:@"Death" userInfo:@{ @"reason for death" : @"heartbreak" } error: NULL]; + [[expectFutureValue(notification) shouldEventually] beNonNil]; + [[notification.userInfo[@"reason for death"] should] equal:@"heartbreak"]; + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + }); + + }); }); @@ -297,10 +468,12 @@ - (void)startTryingToPickUpCollegeGirls {} __block TKState *singleState; __block TKState *datingState; __block TKState *marriedState; + __block TKState *deadState; __block TKEvent *startDating; __block TKEvent *breakup; __block TKEvent *getMarried; __block TKEvent *divorce; + __block TKEvent *die; beforeEach(^{ person = [TKSpecPerson new]; @@ -321,7 +494,10 @@ - (void)startTryingToPickUpCollegeGirls {} person.happy = NO; }]; marriedState = [TKState stateWithName:@"Married"]; + deadState = [TKState stateWithName: @"Dead"]; + [stateMachine addStates:@[ singleState, datingState, marriedState ]]; + stateMachine.terminalStates = [NSSet setWithObject: deadState]; startDating = [TKEvent eventWithName:@"Start Dating" transitioningFromStates:@[ singleState ] toState:datingState]; [startDating setDidFireEventBlock:^(TKEvent *event, TKTransition *transition) { @@ -345,8 +521,11 @@ - (void)startTryingToPickUpCollegeGirls {} [person startDrinkingHeavily]; [person startTryingToPickUpCollegeGirls]; }]; - - [stateMachine addEvents:@[ startDating, breakup, getMarried, divorce ]]; + die = [TKEvent eventWithName:@"Die" transitioningFromStates: nil toState: deadState]; + [die setDidFireEventBlock:^(TKEvent *event, TKTransition *transition) { + person.dead = YES; + }]; + [stateMachine addEvents:@[ startDating, breakup, getMarried, divorce, die ]]; }); context(@"when a Single Person Starts Dating", ^{ @@ -368,7 +547,7 @@ - (void)startTryingToPickUpCollegeGirls {} it(@"returns a nil error", ^{ NSError *error = nil; [stateMachine fireEvent:@"Start Dating" userInfo:nil error:&error]; - [error shouldBeNil]; + [[error should] beNil]; }); it(@"is no longer looking for love", ^{ @@ -391,7 +570,7 @@ - (void)startTryingToPickUpCollegeGirls {} [stateMachine fireEvent:@"Start Dating" userInfo:nil error:nil]; [[expectFutureValue(notification) shouldEventually] beNonNil]; [[NSNotificationCenter defaultCenter] removeObserver:observer]; - [notification.userInfo shouldNotBeNil]; + [[notification.userInfo should] beNonNil]; [[[[notification.userInfo objectForKey:TKStateMachineDidChangeStateOldStateUserInfoKey] name] should] equal:@"Single"]; [[[[notification.userInfo objectForKey:TKStateMachineDidChangeStateNewStateUserInfoKey] name] should] equal:@"Dating"]; [[[[notification.userInfo objectForKey:TKStateMachineDidChangeStateEventUserInfoKey] name] should] equal:@"Start Dating"]; @@ -540,6 +719,22 @@ - (void)startTryingToPickUpCollegeGirls {} }); }); + context(@"when a Dead Person tries to do anything", ^{ + beforeEach(^{ + stateMachine.initialState = [stateMachine stateNamed:@"Dead"]; + }); + + it(@"cannot be fired", ^{ + [[@([stateMachine canFireEvent:@"Get Married"]) should] beNo]; + }); + + it(@"is terminated", ^{ + [stateMachine activate]; + [[@(stateMachine.terminated) should] beYes]; + }); + + }); + context(@"when a Single Person tries to Get Married", ^{ beforeEach(^{ stateMachine.initialState = [stateMachine stateNamed:@"Single"];