fetched mlz version
- before some chamges in the gerrit pipline Change-Id: I33eb2d75f83345a7039d0fb709e66defefb1c3e0
This commit is contained in:
@ -21,18 +21,31 @@
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from secop.lib.statemachine import StateMachine, Stop, Retry
|
||||
from frappy.core import Drivable, Parameter
|
||||
from frappy.datatypes import StatusType, Enum
|
||||
from frappy.states import StateMachine, Stop, Retry, Finish, Start, HasStates, status_code
|
||||
from frappy.lib import generalConfig
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def info(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
|
||||
def debug(self, fmt, *args):
|
||||
pass
|
||||
|
||||
warning = exception = error = info
|
||||
handlers = []
|
||||
|
||||
|
||||
def rise(state):
|
||||
state.step += 1
|
||||
print('rise', state.step)
|
||||
if state.init:
|
||||
state.status = 'rise'
|
||||
state.level += 1
|
||||
if state.level > 3:
|
||||
return turn
|
||||
return Retry()
|
||||
return Retry
|
||||
|
||||
|
||||
def turn(state):
|
||||
@ -42,7 +55,7 @@ def turn(state):
|
||||
state.direction += 1
|
||||
if state.direction > 3:
|
||||
return fall
|
||||
return Retry()
|
||||
return Retry
|
||||
|
||||
|
||||
def fall(state):
|
||||
@ -52,32 +65,35 @@ def fall(state):
|
||||
state.level -= 1
|
||||
if state.level < 0:
|
||||
raise ValueError('crash')
|
||||
return Retry(0) # retry until crash!
|
||||
return fall # retry until crash!
|
||||
|
||||
|
||||
def error_handler(state):
|
||||
state.last_error_name = type(state.last_error).__name__
|
||||
def finish(state):
|
||||
return Finish
|
||||
|
||||
|
||||
class LoggerStub:
|
||||
def debug(self, fmt, *args):
|
||||
print(fmt % args)
|
||||
info = warning = exception = error = debug
|
||||
handlers = []
|
||||
class Result:
|
||||
cleanup_reason = None
|
||||
|
||||
def __init__(self):
|
||||
self.states = []
|
||||
|
||||
class DummyThread:
|
||||
def is_alive(self):
|
||||
return True
|
||||
def on_error(self, sm):
|
||||
self.cleanup_reason = sm.cleanup_reason
|
||||
|
||||
def on_transition(self, sm, newstate):
|
||||
self.states.append(newstate)
|
||||
|
||||
|
||||
def test_fun():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
assert s.step == 0
|
||||
assert s.status == ''
|
||||
s.cycle() # do nothing
|
||||
assert s.step == 0
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.start(rise, cleanup=obj.on_error, level=0, direction=0)
|
||||
s.cycle()
|
||||
for i in range(1, 4):
|
||||
assert s.status == 'rise'
|
||||
assert s.step == i
|
||||
@ -91,56 +107,217 @@ def test_fun():
|
||||
assert s.direction == i - 4
|
||||
s.cycle()
|
||||
s.cycle() # -> crash
|
||||
assert isinstance(s.last_error, ValueError)
|
||||
assert str(s.last_error) == 'crash'
|
||||
assert s.state is None
|
||||
assert isinstance(obj.cleanup_reason, ValueError)
|
||||
assert str(obj.cleanup_reason) == 'crash'
|
||||
assert obj.states == [rise, turn, fall, fall, fall, fall, fall, None]
|
||||
assert s.statefunc is None
|
||||
|
||||
|
||||
def test_max_chain():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(fall, level=999+1, direction=0)
|
||||
assert isinstance(s.last_error, RuntimeError)
|
||||
assert s.state is None
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
s.start(fall, cleanup=obj.on_error, level=999+1, direction=0)
|
||||
s.cycle()
|
||||
assert isinstance(obj.cleanup_reason, RuntimeError)
|
||||
assert s.statefunc is None
|
||||
|
||||
|
||||
def test_stop():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
for _ in range(1, 3):
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
s.start(rise, cleanup=obj.on_error, level=0, direction=0)
|
||||
for _ in range(3):
|
||||
s.cycle()
|
||||
s.stop()
|
||||
s.cycle()
|
||||
assert s.last_error is Stop
|
||||
assert s.state is None
|
||||
assert isinstance(obj.cleanup_reason, Stop)
|
||||
assert obj.states == [rise, None]
|
||||
assert s.statefunc is None
|
||||
|
||||
|
||||
def test_std_error_handling():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
def test_error_handling():
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
s.start(rise, cleanup=obj.on_error, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.level = None # -> TypeError on next step
|
||||
s.cycle()
|
||||
assert s.state is None # default error handler: stop machine
|
||||
assert isinstance(s.last_error, TypeError)
|
||||
assert not hasattr(s, 'last_error_name')
|
||||
|
||||
|
||||
def test_default_error_handling():
|
||||
s = StateMachine(step=0, status='', cleanup=error_handler, threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.level = None
|
||||
s.cycle()
|
||||
assert s.state is None
|
||||
assert s.last_error_name == 'TypeError'
|
||||
assert isinstance(s.last_error, TypeError)
|
||||
assert isinstance(obj.cleanup_reason, TypeError)
|
||||
assert obj.states == [rise, None]
|
||||
assert s.statefunc is None
|
||||
|
||||
|
||||
def test_cleanup_on_restart():
|
||||
s = StateMachine(step=0, status='', threaded=False, logger=LoggerStub())
|
||||
s.start(rise, level=0, direction=0)
|
||||
def test_on_restart():
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
s.start(rise, cleanup=obj.on_error, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.cycle()
|
||||
s.start(turn)
|
||||
s.cycle()
|
||||
assert s.state is turn
|
||||
assert s.last_error is None
|
||||
assert isinstance(obj.cleanup_reason, Start)
|
||||
obj.cleanup_reason = None
|
||||
s.cycle()
|
||||
assert s.statefunc is turn
|
||||
assert obj.cleanup_reason is None
|
||||
assert obj.states == [rise, None, turn]
|
||||
|
||||
|
||||
def test_finish():
|
||||
obj = Result()
|
||||
s = StateMachine(step=0, status='', transition=obj.on_transition, logger=LoggerStub())
|
||||
s.start(finish, cleanup=obj.on_error, level=0, direction=0)
|
||||
s.cycle()
|
||||
s.cycle()
|
||||
assert obj.states == [finish, None]
|
||||
assert s.statefunc is None
|
||||
assert s.cleanup_reason is None
|
||||
|
||||
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
PREPARED=150,
|
||||
PREPARING=340,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
FINALIZING=390,
|
||||
)
|
||||
|
||||
|
||||
class DispatcherStub:
|
||||
# the first update from the poller comes a very short time after the
|
||||
# initial value from the timestamp. However, in the test below
|
||||
# the second update happens after the updates dict is cleared
|
||||
# -> we have to inhibit the 'omit unchanged update' feature
|
||||
|
||||
def __init__(self, updates):
|
||||
generalConfig.testinit(omit_unchanged_within=0)
|
||||
self.updates = updates
|
||||
|
||||
def announce_update(self, modulename, pname, pobj):
|
||||
assert modulename == 'obj'
|
||||
if pobj.readerror:
|
||||
self.updates.append((pname, pobj.readerror))
|
||||
else:
|
||||
self.updates.append((pname, pobj.value))
|
||||
|
||||
|
||||
class ServerStub:
|
||||
def __init__(self, updates):
|
||||
self.dispatcher = DispatcherStub(updates)
|
||||
|
||||
|
||||
class Mod(HasStates, Drivable):
|
||||
status = Parameter(datatype=StatusType(Status))
|
||||
_my_time = 0
|
||||
|
||||
def artificial_time(self):
|
||||
return self._my_time
|
||||
|
||||
def state_transition(self, sm, newstate):
|
||||
self.statelist.append(getattr(newstate, '__name__', None))
|
||||
super().state_transition(sm, newstate)
|
||||
|
||||
def state_one(self, sm):
|
||||
if sm.init:
|
||||
return Retry
|
||||
return self.state_two
|
||||
|
||||
@status_code('PREPARING', 'state 2')
|
||||
def state_two(self, sm):
|
||||
return self.state_three
|
||||
|
||||
@status_code('FINALIZING')
|
||||
def state_three(self, sm):
|
||||
if sm.init:
|
||||
return Retry
|
||||
return self.final_status('IDLE', 'finished')
|
||||
|
||||
def cleanup_one(self, sm):
|
||||
self.statelist.append('cleanup one')
|
||||
return self.cleanup_two
|
||||
|
||||
@status_code('BUSY', 'after cleanup')
|
||||
def cleanup_two(self, sm):
|
||||
if sm.init:
|
||||
return Retry
|
||||
return Finish
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
self._my_time += 1
|
||||
|
||||
|
||||
def create_module():
|
||||
updates = []
|
||||
obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates))
|
||||
obj.initModule()
|
||||
obj.statelist = []
|
||||
try:
|
||||
obj._Module__pollThread(obj.polledModules, None)
|
||||
except TypeError:
|
||||
pass # None is not callable
|
||||
updates.clear()
|
||||
return obj, updates
|
||||
|
||||
|
||||
def test_updates():
|
||||
obj, updates = create_module()
|
||||
obj.start_machine(obj.state_one)
|
||||
for _ in range(10):
|
||||
obj.doPoll()
|
||||
assert updates == [
|
||||
('status', (Status.BUSY, 'state one')), # default: BUSY, function name without '_'
|
||||
('status', (Status.PREPARING, 'state 2')), # explicitly given
|
||||
('status', (Status.FINALIZING, 'state three')), # only code given
|
||||
('status', (Status.IDLE, 'finished')),
|
||||
]
|
||||
|
||||
|
||||
def test_stop_without_cleanup():
|
||||
obj, updates = create_module()
|
||||
obj.start_machine(obj.state_one)
|
||||
obj.doPoll()
|
||||
obj.stop_machine()
|
||||
for _ in range(10):
|
||||
obj.doPoll()
|
||||
assert updates == [
|
||||
('status', (Status.BUSY, 'state one')),
|
||||
('status', (Status.BUSY, 'stopping')),
|
||||
('status', (Status.IDLE, 'stopped')),
|
||||
]
|
||||
assert obj.statelist == ['state_one', None]
|
||||
|
||||
|
||||
def test_stop_with_cleanup():
|
||||
obj, updates = create_module()
|
||||
obj.start_machine(obj.state_one, cleanup=obj.cleanup_one)
|
||||
obj.doPoll()
|
||||
obj.stop_machine()
|
||||
for _ in range(10):
|
||||
obj.doPoll()
|
||||
assert obj.statelist == ['state_one', 'cleanup one', 'cleanup_two', None]
|
||||
assert updates == [
|
||||
('status', (Status.BUSY, 'state one')),
|
||||
('status', (Status.BUSY, 'stopping')),
|
||||
('status', (Status.BUSY, 'stopping (after cleanup)')),
|
||||
('status', (Status.IDLE, 'stopped')),
|
||||
]
|
||||
|
||||
|
||||
def test_all_restart():
|
||||
obj, updates = create_module()
|
||||
obj.start_machine(obj.state_one, cleanup=obj.cleanup_one, statelist=[])
|
||||
obj.doPoll()
|
||||
obj.start_machine(obj.state_three)
|
||||
for _ in range(10):
|
||||
obj.doPoll()
|
||||
assert obj.statelist == ['state_one', 'cleanup one', 'cleanup_two', None, 'state_three', None]
|
||||
assert updates == [
|
||||
('status', (Status.BUSY, 'state one')),
|
||||
('status', (Status.FINALIZING, 'restarting')),
|
||||
('status', (Status.FINALIZING, 'restarting (after cleanup)')),
|
||||
('status', (Status.FINALIZING, 'state three')),
|
||||
('status', (Status.IDLE, 'finished')),
|
||||
]
|
||||
|
Reference in New Issue
Block a user