def test_canceled_before_event_false(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_before_calm': lambda e: False}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertRaises(Canceled, gsm.calm, obj)
def test_asynchronous_transition(self): def _func(event): event.obj.logs.append('function_callback') def on_leave_red(event): return False gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={ 'on_leave_red': on_leave_red, 'on_after_calm': _func }, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'red')) self.assertTrue(hasattr(obj, 'transition')) self.assertFalse('functioon_callback' in obj.logs) obj.transition() self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertFalse(hasattr(obj, 'transition')) self.assertTrue('function_callback' in obj.logs)
def test_transition_with_args_kwargs(self): def _func(event): self.assertTrue(hasattr(event, 'args')) self.assertTrue(hasattr(event, 'kwargs')) self.assertTrue(hasattr(event, 'msg')) event.obj.logs.append('function_callback') gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_startup': _func}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj, msg='msg') self.assertTrue('function_callback' in obj.logs)
def test_cfg_parameter(self): gsm = FysomGlobal( cfg={ 'events': [ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" { True: 'is_very_angry', 'else': 'yellow' } ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green') ] }, initial='green', final='red', state_field='state') self.assertTrue(hasattr(gsm, 'warn')) self.assertTrue(hasattr(gsm, 'panic')) self.assertTrue(hasattr(gsm, 'calm')) self.assertTrue(hasattr(gsm, 'clear'))
def test_wildcard_src(self): gsm = FysomGlobal(events=[{ 'name': 'calm', 'dst': 'yellow' }], initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow'))
def test_function_callback(self): def _func(event): event.obj.logs.append('function_callback') gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_clear': _func}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) gsm.clear(obj) self.assertTrue('function_callback' in obj.logs)
def test_manual_startup(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='red', state_field='state') obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'startup')) gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red'))
class NavigationGameFSM(FysomGlobalMixin, object): GSM = FysomGlobal(initial='not_running', events=[{ 'name': 'run', 'src': ['not_running', 'has_won', 'dead'], 'dst': 'menu' }, ('win', 'playing', 'has_won'), ('next', 'menu', 'black_screen'), ('play', 'black_screen', 'playing'), ('die', 'playing', 'dead')], state_field='state') def __init__(self): self.state = None super(NavigationGameFSM, self).__init__()
class Model(FysomGlobalMixin, object): GSM = FysomGlobal( events=[ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" { True: 'is_very_angry', 'else': 'yellow' } ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green') ], initial='green', final='red', state_field='state') def __init__(self): self._state = None super(Model, self).__init__() @property def state(self): return self._state @state.setter def state(self, value): self._state = value print(f'new state: {value}') def is_angry(self, event): return True def is_very_angry(self, event): return False def __str__(self): return self.state
def test_callable_or_basestring_condition(self): gsm = FysomGlobal(events=[{ 'name': 'calm', 'src': 'red', 'dst': 'yellow', 'cond': lambda e: True, }, { 'name': 'clear', 'src': 'yellow', 'dst': 'green', 'cond': 'check_false' }], initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertRaises(Canceled, gsm.clear, obj) self.assertTrue(gsm.is_state(obj, 'yellow'))
def test_manual_startup_event_name(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial={ 'state': 'red', 'event': 'my_startup' }, state_field='state') obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'my_startup')) gsm.my_startup(obj) self.assertTrue(gsm.current(obj) == 'red')
class Machine(FysomGlobalMixin, models.Model): GSM = FysomGlobal( events=[ ('key_put', 'idle', 'engine_starting'), ('key_turn', 'engine_starting', 'stopped'), ('pedal_gas', 'stopped', 'moving'), ('pedal_stop', ['moving', 'stopped'], 'stopped'), ('key_out', 'stopped', 'idle'), ], initial='idle', state_field='state', ) state = models.CharField(max_length=128) def on_enter_stopped(self, e): print(f"{self} stopped. Ready to move") def __str__(self): return f'Car "{self.state}" state'
def setUp(self): self.GSM = FysomGlobal( events=[ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" { True: 'is_very_angry', 'else': 'yellow' } ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green') ], initial='green', final='red', state_field='state') class BaseModel(object): def __init__(self): self.state = None self.can_angry = True self.can_very_angry = False self.logs = [] def is_angry(self, event): return self.can_angry def is_very_angry(self, event): return self.can_very_angry def on_change_state(self, event): self.logs.append('on_change_state') def check_true(self, event): return True def check_false(self, event): return False for _state in ('green', 'yellow', 'red'): for _at in ('enter', 'reenter', 'leave'): attr_name = 'on_%s_%s' % (_at, _state) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) for _event in ('warn', 'panic', 'calm', 'clear'): for _at in ('before', 'after'): attr_name = 'on_%s_%s' % (_at, _event) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) self.BaseModel = BaseModel class MixinModel(FysomGlobalMixin, BaseModel): GSM = self.GSM self.MixinModel = MixinModel
class FysomGlobalTests(unittest.TestCase): def setUp(self): self.GSM = FysomGlobal( events=[ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" { True: 'is_very_angry', 'else': 'yellow' } ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green') ], initial='green', final='red', state_field='state') class BaseModel(object): def __init__(self): self.state = None self.can_angry = True self.can_very_angry = False self.logs = [] def is_angry(self, event): return self.can_angry def is_very_angry(self, event): return self.can_very_angry def on_change_state(self, event): self.logs.append('on_change_state') def check_true(self, event): return True def check_false(self, event): return False for _state in ('green', 'yellow', 'red'): for _at in ('enter', 'reenter', 'leave'): attr_name = 'on_%s_%s' % (_at, _state) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) for _event in ('warn', 'panic', 'calm', 'clear'): for _at in ('before', 'after'): attr_name = 'on_%s_%s' % (_at, _event) def f(obj, event, attr_name=attr_name): obj.logs.append(attr_name) setattr(BaseModel, attr_name, f) self.BaseModel = BaseModel class MixinModel(FysomGlobalMixin, BaseModel): GSM = self.GSM self.MixinModel = MixinModel def test_no_mixin_initial_state(self): obj = self.BaseModel() self.assertTrue(self.GSM.isstate(obj, 'none')) self.assertTrue(self.GSM.is_state(obj, 'none')) def test_mixin_initial_state(self): obj = self.MixinModel() self.assertTrue(obj.isstate('green')) self.assertTrue(obj.is_state('green')) def test_mixin_initial_event(self): obj = self.MixinModel() self.assertEqual(obj.logs, ['on_enter_green', 'on_change_state']) def test_mixin_current_property(self): obj = self.MixinModel() self.assertEqual(obj.current, 'green') obj.current = 'yellow' self.assertEqual(obj.current, 'yellow') def test_is_finished(self): obj = self.MixinModel() obj.can_very_angry = True obj.panic() self.assertTrue(obj.is_finished()) def test_valid_transition_is_allowed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.warn() self.assertTrue(obj.is_state('yellow')) def test_invalid_transition_is_not_allowed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.can_very_angry = True obj.warn() self.assertTrue(obj.is_state('yellow')) obj.panic() self.assertTrue(obj.is_state('red')) self.assertRaises(FysomError, obj.clear) def tests_callbacks_order(self): obj = self.MixinModel() obj.logs = [] obj.warn() self.assertEqual(obj.logs, [ 'on_before_warn', 'on_leave_green', 'on_enter_yellow', 'on_change_state', 'on_after_warn' ]) obj.can_angry = False obj.logs = [] self.assertRaises(Canceled, obj.panic) self.assertEqual(obj.logs, []) self.assertTrue(obj.is_state('yellow')) obj.can_angry = True obj.can_very_angry = False obj.panic() self.assertEqual( obj.logs, ['on_before_panic', 'on_reenter_yellow', 'on_after_panic']) def test_ok_condition_passed(self): obj = self.MixinModel() obj.can_very_angry = True obj.panic() self.assertTrue(obj.is_state('red')) def test_not_ok_condition_rejected(self): obj = self.MixinModel() obj.can_angry = False self.assertRaises(Canceled, obj.panic) self.assertTrue(obj.is_state('green')) def test_conditional_transition_passed(self): obj = self.MixinModel() self.assertTrue(obj.is_state('green')) obj.panic() self.assertFalse(obj.is_state('red')) self.assertTrue(obj.is_state('yellow')) def test_canceled_exception_with_event(self): obj = self.MixinModel() obj.can_angry = False self.assertRaises(Canceled, obj.panic) try: obj.panic() except Canceled as err: self.assertTrue(hasattr(err, 'event')) exc_event = err.event self.assertTrue(hasattr(exc_event, 'fsm')) self.assertTrue(hasattr(exc_event, 'obj')) self.assertTrue(hasattr(exc_event, 'src')) self.assertEqual(exc_event.src, 'green') self.assertTrue(hasattr(exc_event, 'dst')) self.assertEqual(exc_event.dst, 'red') self.assertTrue(hasattr(exc_event, 'args')) self.assertTrue(hasattr(exc_event, 'kwargs')) def test_trigger_works(self): obj = self.MixinModel() obj.trigger('warn') self.assertEqual(obj.current, 'yellow') self.assertRaises(FysomError, obj.trigger, 'unknown_event') self.assertEqual(obj.current, 'yellow') def test_no_state_field_specified(self): def _t(): gsm = FysomGlobal(events=[]) self.assertRaises(FysomError, _t) def test_cfg_parameter(self): gsm = FysomGlobal( cfg={ 'events': [ ('warn', 'green', 'yellow'), { 'name': 'panic', 'src': ['green', 'yellow'], 'dst': 'red', 'cond': [ # can be function object or method name 'is_angry', # by default target is "True" { True: 'is_very_angry', 'else': 'yellow' } ] }, ('calm', 'red', 'yellow'), ('clear', 'yellow', 'green') ] }, initial='green', final='red', state_field='state') self.assertTrue(hasattr(gsm, 'warn')) self.assertTrue(hasattr(gsm, 'panic')) self.assertTrue(hasattr(gsm, 'calm')) self.assertTrue(hasattr(gsm, 'clear')) def test_manual_startup(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial='red', state_field='state') obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'startup')) gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) def test_manual_startup_event_name(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], initial={ 'state': 'red', 'event': 'my_startup' }, state_field='state') obj = self.BaseModel() self.assertTrue(gsm.is_state(obj, 'none')) self.assertTrue(hasattr(gsm, 'my_startup')) gsm.my_startup(obj) self.assertTrue(gsm.current(obj) == 'red') def test_function_callback(self): def _func(event): event.obj.logs.append('function_callback') gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_clear': _func}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) gsm.clear(obj) self.assertTrue('function_callback' in obj.logs) def test_asynchronous_transition(self): def _func(event): event.obj.logs.append('function_callback') def on_leave_red(event): return False gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={ 'on_leave_red': on_leave_red, 'on_after_calm': _func }, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'red')) self.assertTrue(hasattr(obj, 'transition')) self.assertFalse('functioon_callback' in obj.logs) obj.transition() self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertFalse(hasattr(obj, 'transition')) self.assertTrue('function_callback' in obj.logs) def test_transition_with_args_kwargs(self): def _func(event): self.assertTrue(hasattr(event, 'args')) self.assertTrue(hasattr(event, 'kwargs')) self.assertTrue(hasattr(event, 'msg')) event.obj.logs.append('function_callback') gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_after_startup': _func}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj, msg='msg') self.assertTrue('function_callback' in obj.logs) def test_canceled_before_event_false(self): gsm = FysomGlobal(events=[('calm', 'red', 'yellow'), ('clear', 'yellow', 'green')], callbacks={'on_before_calm': lambda e: False}, initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertRaises(Canceled, gsm.calm, obj) def test_callable_or_basestring_condition(self): gsm = FysomGlobal(events=[{ 'name': 'calm', 'src': 'red', 'dst': 'yellow', 'cond': lambda e: True, }, { 'name': 'clear', 'src': 'yellow', 'dst': 'green', 'cond': 'check_false' }], initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow')) self.assertRaises(Canceled, gsm.clear, obj) self.assertTrue(gsm.is_state(obj, 'yellow')) def test_unknown_event(self): obj = self.MixinModel() self.assertFalse(obj.can('unknown_event')) self.assertTrue(self.GSM.cannot(obj, 'unknown_event')) def test_wildcard_src(self): gsm = FysomGlobal(events=[{ 'name': 'calm', 'dst': 'yellow' }], initial='red', state_field='state') obj = self.BaseModel() gsm.startup(obj) self.assertTrue(gsm.is_state(obj, 'red')) gsm.calm(obj) self.assertTrue(gsm.is_state(obj, 'yellow'))
def _t(): gsm = FysomGlobal(events=[])
class HVAC(FysomGlobalMixin): GSM = FysomGlobal( events=[('cool', 'idle', 'cool'), ('heat', 'idle', 'heat'), ('fan', 'idle', 'fan'), ('idle', ['idle', 'cool', 'heat'], 'idle')], callbacks={'on_before_idle': ready_for_change}, initial='idle', state_field='state', ) def __init__(self): # pin assignments self.ORANGE_PIN = 6 self.YELLOW_PIN = 13 self.GREEN_PIN = 19 self.AUX_PIN = 26 GPIO.setup(self.ORANGE_PIN, GPIO.OUT) GPIO.setup(self.YELLOW_PIN, GPIO.OUT) GPIO.setup(self.GREEN_PIN, GPIO.OUT) GPIO.setup(self.AUX_PIN, GPIO.OUT) self.SAFETY_TIMER = 300 # minimum seconds between state changes, protects compressor self.last_state_change = 0 self.state = 'idle' super(HVAC, self).__init__() self.onchangestate = self.printstatechange def printstatechange(self, e): print('change from {}, to {}'.format(e.src, e.dst)) last_state_change = time.time() def set_state(self, target_state): try: print("setting state") self.state = target_state except: print("FAILED") if (time.time() - self.last_state_change) > self.SAFETY_TIMER: print("enough time has passed sinced last state change") self.last_state_change = time.time() print("setting state, current state = {}".format(self.current)) if self.is_state("cool"): print("COOLING") GPIO.output(self.ORANGE_PIN, True) GPIO.output(self.YELLOW_PIN, True) GPIO.output(self.GREEN_PIN, True) GPIO.output(self.AUX_PIN, False) elif self.current == "heat": print("HEATING") GPIO.output(self.ORANGE_PIN, False) GPIO.output(self.YELLOW_PIN, True) GPIO.output(self.GREEN_PIN, True) GPIO.output(self.AUX_PIN, False) elif self.current == "fan": GPIO.output(self.ORANGE_PIN, False) GPIO.output(self.YELLOW_PIN, False) GPIO.output(self.GREEN_PIN, True) GPIO.output(self.AUX_PIN, False) elif self.current == "aux": GPIO.output(self.ORANGE_PIN, False) GPIO.output(self.YELLOW_PIN, True) GPIO.output(self.GREEN_PIN, True) GPIO.output(self.AUX_PIN, True) elif self.current == "idle": print("IDLE") GPIO.output(self.ORANGE_PIN, False) GPIO.output(self.YELLOW_PIN, False) GPIO.output(self.GREEN_PIN, False) GPIO.output(self.AUX_PIN, False) else: GPIO.output(self.ORANGE_PIN, False) GPIO.output(self.YELLOW_PIN, False) GPIO.output(self.GREEN_PIN, False) GPIO.output(self.AUX_PIN, False)