def test_multiple_events(circuit): """Test multiple events.""" dest1 = EventMemory('dest2') dest2 = EventMemory('dest1') src = edzed.Input('src', on_output=[ edzed.Event(dest=dest1, etype='ev1'), edzed.Event(dest=dest2, etype='ev2') ], initdef=None) init(circuit) CDATA = {'source': 'src', 'trigger': 'output'} src.put(0) assert dest1.output == ('ev1', {**CDATA, 'previous': None, 'value': 0}) assert dest2.output == ('ev2', {**CDATA, 'previous': None, 'value': 0}) src.put(911) assert dest1.output[1] == dest2.output[1] == { **CDATA, 'previous': 0, 'value': 911 } src.put('last') assert dest1.output[1] == dest2.output[1] == { **CDATA, 'previous': 911, 'value': 'last' }
def test_on_every_output(circuit): """Test that on_output generates events.""" dest = EventMemory('dest') dest_every = EventMemory('dest_every') src = edzed.Input('src', on_output=edzed.Event(dest, etype='ev'), on_every_output=edzed.Event(dest_every, etype='ev'), initdef=None) init(circuit) CDATA = {'source': 'src', 'trigger': 'output'} src.put(0) assert dest.output == dest_every.output == ('ev', { **CDATA, 'previous': None, 'value': 0 }) src.put(911) assert dest.output == dest_every.output == ('ev', { **CDATA, 'previous': 0, 'value': 911 }) dest.event('ev', cleared=True) dest_every.event('ev', cleared=True) src.put(911) assert dest.output == ('ev', {'cleared': True}) assert dest_every.output == ('ev', { **CDATA, 'previous': 911, 'value': 911 })
async def t1sec(circuit, dynamic): """Activate for the next one second.""" logger = TimeLogger('logger', mstop=True) timelimit(3.0, error=True) now = time.time() now_sec = HMS(time.localtime(now)).seconds() ms = now % 1 delay = 1 if ms < 0.950 else 2 # leave at least 50ms for circuit setup targ = f"{HMS(now_sec+delay)}-{HMS(now_sec+delay+1)}" s1 = edzed.TimeDate("1sec", times=None if dynamic else targ, on_output=(edzed.Event(logger), edzed.Event('_ctrl', 'shutdown', efilter=edzed.Edge(fall=True)))) simtask = asyncio.create_task(circuit.run_forever()) await circuit.wait_init() if dynamic: s1.event('reconfig', times=targ) with pytest.raises(asyncio.CancelledError): await simtask LOG = [ (0, False), (1000 * (delay - ms), True), (1000 * (delay + 1 - ms), False), (1000 * (delay + 1 - ms), '--stop--'), ] logger.compare(LOG)
def test_persistent_state(circuit): class Dummy(edzed.FSM): STATES = list("ABCDEF") def forbidden(): assert False mem = EventMemory('mem') fsm = Dummy( 'test', persistent=True, enter_D=forbidden, on_enter_D=edzed.Event(mem, '+D'), on_exit_D=edzed.Event(mem, '-D') ) assert fsm.key == "<Dummy 'test'>" storage = {fsm.key: ['D', None, {'x':'y', '_z':3}]} # (state, timer, sdata) circuit.set_persistent_data(storage) # enter_D and on_enter_D will be suppressed init(circuit) assert fsm.sdata == {'x':'y', '_z':3} assert fsm.state == 'D' assert mem.output is None fsm.event(edzed.Goto('F')) assert mem.output == ( '-D', # '_z' is not present in 'sdata', because it is considered private {'source': 'test', 'trigger': 'exit', 'state': 'D', 'sdata': {'x':'y'}, 'value': 'D'} ) del fsm.sdata['x'] assert storage == {fsm.key: ('F', None, {'_z':3})}
def test_no_circular_init_by_event(circuit): """Circular (recursive in general) events are forbidden.""" inp_a = edzed.Input('inp_a', on_output=edzed.Event('inp_b'), initdef='ok') inp_b = edzed.Input( 'inp_b', on_output=edzed.Event('inp_a')) # cannot send back to inp_a with pytest.raises(edzed.EdzedCircuitError, match="Forbidden recursive event"): init(circuit)
def test_init_by_event(circuit): """Test initialization by an event.""" src = edzed.Input('src', on_output=edzed.Event('dest'), initdef='ok') dest = edzed.Input('dest', on_output=edzed.Event('mem')) # note: no inittdef=... ! mem = EventMemory('mem') init(circuit) assert src.output == dest.output == 'ok' assert mem.output == ('put', { 'source': 'dest', 'trigger': 'output', 'previous': edzed.UNDEF, 'value': 'ok' })
def test_on_output(circuit): """Test that on_output generates events.""" dest = EventMemory('dest') src = edzed.Input('src', check=lambda x: x != 'NO!', on_output=edzed.Event(dest, etype='ev'), initdef=None) init(circuit) CDATA = {'source': 'src', 'trigger': 'output'} src.put(0) assert dest.output == ('ev', {**CDATA, 'previous': None, 'value': 0}) src.put(911) assert dest.output == ('ev', {**CDATA, 'previous': 0, 'value': 911}) src.put('string') assert dest.output == ('ev', {**CDATA, 'previous': 911, 'value': 'string'}) dest.event('clear') src.put('string') # same value, no change, no output event assert dest.output == ('clear', {}) src.put('NO!') # forbidden value (check=...), no output event assert dest.output == ('clear', {}) src.put(0) assert dest.output == ('ev', {**CDATA, 'previous': 'string', 'value': 0})
async def test_expiration(circuit): """Test the value expiration.""" logger = TimeLogger('logger', mstop=True) inpexp = edzed.InputExp('ie', duration=0.2, expired=-1, initdef=99, on_output=edzed.Event(logger)) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.25) assert inpexp.state == 'expired' inpexp.put(77) assert inpexp.state == 'valid' await asyncio.sleep(0.1) inpexp.put(55) await asyncio.sleep(0.25) inpexp.put(33, duration="0.08s") # override await asyncio.sleep(0.2) await circuit.shutdown() LOG = [ (0, 99), (200, -1), (250, 77), (350, 55), (550, -1), (600, 33), (680, -1), (800, "--stop--"), ] logger.compare(LOG)
async def output_async(circuit, *, log, t1=0.1, t2=0.0, test_error=False, mstop=True, on_error=None, **kwargs): async def wait120(arg): logger.put(f'start {arg}') if test_error: # pylint: disable=pointless-statement 1 / 0 # BOOM! await asyncio.sleep(0.12) logger.put(f'stop {arg}') return f'ok {arg}' try: inp = edzed.Input('inp', initdef='i1', on_output=edzed.Event('echo')) logger = TimeLogger('logger', mstop=mstop) edzed.OutputAsync('echo', coro=wait120, on_error=on_error, **kwargs) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(t1) if circuit.is_ready(): # skip after an error, inp.put('i2') if t2 > 0.0: await asyncio.sleep(t2) inp.put('i3') await asyncio.sleep(0.05) await circuit.shutdown() logger.put("END") finally: logger.compare(log)
async def test_smode_stop(circuit): """Test the stop data in start mode.""" LOG = [ (0, 'start i1'), (40, 'start i2'), (80, 'start i3'), (120, 'stop i1'), (130, '--stop--'), (160, 'stop i2'), (200, 'stop i3'), # stop data is always processed last (200, 'start CLEANUP'), (320, 'stop CLEANUP'), (320, 'END') ] VLOG = [ (120, 'ok i1'), (160, 'ok i2'), (200, 'ok i3'), (320, 'ok CLEANUP'), ] vlog = TimeLogger('vlog') await output_async(circuit, mode='s', stop_data={'value': 'CLEANUP'}, t1=0.04, t2=0.04, log=LOG, on_success=edzed.Event('vlog')) vlog.compare(VLOG)
async def output_func(circuit, *, log, v2=2, on_error=None, mstop=True, **kwargs): def worker(arg): v = 12 // arg logger.put(v) return 100 + v try: inp = edzed.Input('inp', initdef=6, on_output=edzed.Event('echo')) logger = TimeLogger('logger', mstop=mstop) edzed.OutputFunc('echo', func=worker, on_error=on_error, **kwargs) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.05) inp.put(v2) if circuit.is_ready(): # skip after an error, await asyncio.sleep(0.05) inp.put(3) await circuit.shutdown() logger.put("END") finally: logger.compare(log)
async def ptest(circuit, delay, slog): ie1 = edzed.InputExp('ie', duration=0.25, expired="exp", initdef="ok1", persistent=True) state = {} circuit.set_persistent_data(state) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.1) await circuit.shutdown() assert ie1.key in state # circuit 2 await asyncio.sleep(delay) edzed.reset_circuit() circuit = edzed.get_circuit() circuit.set_persistent_data(state) logger = TimeLogger('logger') ie2 = edzed.InputExp('ie', duration=0.25, expired="exp", initdef="ok2", persistent=True, on_output=edzed.Event(logger)) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.30) await circuit.shutdown() logger.compare(slog)
async def test_executor(circuit): """Test execution of blocking output functions in threads.""" THREADS = 8 # add 1 ms for large overhead (10->11) LOG = [(11 + 20 * i, i) for i in range(THREADS)] + [(210, '--stop--')] def blocking(v): time.sleep(0.01 + 0.015 * v) return v log = TimeLogger('log', mstop=True) blocks = [ edzed.OutputAsync(str(i), mode='c', coro=edzed.InExecutor(blocking), on_success=edzed.Event(log), on_error=edzed.Event.abort()) for i in range(THREADS) ] asyncio.create_task(circuit.run_forever()) await circuit.wait_init() for i, blk in enumerate(blocks): blk.put(i) await asyncio.sleep(0.005) await asyncio.sleep(0.17) await circuit.shutdown() log.compare(LOG)
def test_chained_dataedit(circuit): """Chaining of event data edit functions is allowed.""" def check(expected): def _check(data): assert data == expected return data return _check src = edzed.Input( 'src', on_output=edzed.Event( 'dest', efilter=( edzed.not_from_undef, check({'source': 'src', 'trigger': 'output', 'previous': None, 'value': 'V'}), edzed.DataEdit \ .permit('source', 'trigger', 'value') \ .setdefault(source='fake') \ .copy('value', 'saved') \ .rename('source', 'src'), check( {'src': 'src', 'trigger': 'output', 'saved': 'V', 'value': 'V'}), edzed.DataEdit.permit('trigger', 'src') \ .modify('src', lambda x: x[::-1]) \ .add(a=1) )), initdef=None) dest = EventMemory('dest') init(circuit) src.put('V') assert dest.output == ('put', {'a': 1, 'src': 'crs', 'trigger': 'output'})
def test_edge_detector_from_undef(circuit): """Test the edge detector's parameters u_rise, u_fall.""" setup = [] i = 0 for value in (False, True): for ur in (False, True, None): for uf in (False, True): for r in (False, True): dest = edzed.Input(f'test_event_{i}', initdef=None) edzed.Input(f"test_edge_{i}", on_output=edzed.Event(dest, efilter=edzed.Edge( rise=r, u_rise=ur, u_fall=uf)), initdef=value) if ur is None: ur = r event = ur if value else uf result = value if event else None setup.append((dest, result, (value, ur, uf, r))) i += 1 init(circuit) for dest, result, args in setup: assert dest.output is result, f"failed for (value, u_rise, u_fall, rise) = {args}"
async def test_on_success(circuit): def check_trigger(data): assert data['trigger'] == 'success' v = data['value'] if v.startswith('ok'): assert v.endswith(data['put']['value']) return True LOG = [ (0, 'start i1'), (120, 'stop i1'), (120, 'ok i1'), # on_success - coroutine return value (120, 'start i2'), (150, '--stop--'), (240, 'stop i2'), (240, 'ok i2'), (240, 'start i3'), (360, 'stop i3'), (360, 'ok i3'), # on_success (360, 'END') ] await output_async(circuit, mode='w', on_success=edzed.Event('logger', efilter=check_trigger), log=LOG)
def test_ifoutput(circuit): """Test the IfOutput.""" enable = edzed.Input('enable', initdef='truthy') cnt = edzed.Counter('cnt') cnt2 = edzed.Counter('cnt2') increment = edzed.Event('cnt', 'inc', efilter=edzed.IfOutput(enable)) increment2 = edzed.Event('cnt2', 'inc', efilter=edzed.IfOutput('_not_enable')) src = Noop('src', comment='faked event source') init(circuit) inv = circuit.findblock('_not_enable') assert cnt.output == cnt2.output == 0 assert increment.send(src) assert cnt.output == 1 assert not increment2.send(src) assert cnt2.output == 0 assert increment.send(src) assert cnt.output == 2 assert not increment2.send(src) assert cnt2.output == 0 enable.put(False) inv.eval_block() assert not increment.send(src) assert cnt.output == 2 assert increment2.send(src) assert cnt2.output == 1 enable.put(0) inv.eval_block() assert not increment.send(src) assert cnt.output == 2 assert increment2.send(src) assert cnt2.output == 2 enable.put(1) inv.eval_block() assert increment.send(src) assert cnt.output == 3 assert not increment2.send(src) assert cnt2.output == 2
def test_edge_detector(circuit): """Test the edge detector's parameters rise and fall.""" SEQ1 = (False, True, False, True, False, True, False, True) SEQ2 = (True, False, True, False, True) cnt = edzed.Counter('counter') i = 0 setup = [] for r in (False, True): for f in (False, True): inp1 = edzed.Input(f"test{i}_seq1", on_output=edzed.Event(cnt, 'inc', efilter=edzed.Edge( rise=r, fall=f)), initdef=SEQ1[0]) inp2 = edzed.Input(f"test{i}_seq2", on_output=edzed.Event(cnt, 'inc', efilter=edzed.Edge( rise=r, fall=f)), initdef=SEQ2[0]) s1 = s2 = 0 # s1, s2 = expected event count for SEQ1, SEQ2 if r: s1 += 4 # 4 rising edges s2 += 2 # 3 rising edges, but 1 of them is initial # and is counted immediately at the block creation if f: s1 += 3 # 4 falling edges, but 1 of them is initial # and is suppressed by uf=False (default) s2 += 2 # 2 falling edges setup.append((inp1, SEQ1, s1, (r, f))) setup.append((inp2, SEQ2, s2, (r, f))) i += 1 init(circuit) assert cnt.output == 2 # 2 times the initial rising edge of S2R1F10 and S2R1F1 for inp, seq, result, args in setup: cnt.put(0) assert cnt.output == 0 for val in seq: inp.put(val) assert cnt.output == result, f"failed for {inp.name}, (rise, fall) = {args}"
def test_context(circuit): """Each event being handled has its own context.""" class Simple(edzed.FSM): STATES = ['st0', 'st1'] EVENTS = [('ev01', None, 'st1')] def cond_ev01(self): data = edzed.fsm_event_data.get() assert data['sent_to'] == self.name return True afsm = Simple( 'A', on_enter_st1=edzed.Event('B', 'ev01', efilter=edzed.DataEdit.add(sent_to='B'))) bfsm = Simple( 'B', on_enter_st1=edzed.Event('C', 'ev01', efilter=edzed.DataEdit.add(sent_to='C'))) cfsm = Simple('C') init(circuit) afsm.event('ev01', sent_to='A') # A -> B -> C assert cfsm.state == 'st1'
def test_transition_chaining(circuit): """Test the chained transition A->B->C->D.""" class ABCD(edzed.FSM): STATES = ['A', 'B', 'C', 'D'] EVENTS = [ ('start', ['A'], 'B') ] def enter_B(self): self.event(edzed.Goto('C')) def enter_C(self): self.event(edzed.Goto('D')) def calc_output(self): assert self.state not in ('B', 'C') # no output for intermediate states return f'in_{self.state}' abcd = ABCD( 'abcd', on_enter_B=edzed.Event('mem', '+B'), on_exit_B=edzed.Event('mem', '-B'), on_enter_C=edzed.Event('mem', '+C'), on_exit_C=edzed.Event('mem', '-C'), on_exit_D=edzed.Event('mem', '-D'), ) mem = EventMemory('mem') init(circuit) assert abcd.state == 'A' assert abcd.output == 'in_A' assert mem.output is None abcd.event('start') assert abcd.state == 'D' assert abcd.output == 'in_D' assert mem.output is None # no events for intermediate states abcd.event(edzed.Goto('A')) assert abcd.state == 'A' assert abcd.output == 'in_A' assert mem.output == ( '-D', {'source': 'abcd', 'trigger': 'exit', 'state': 'D', 'sdata': {}, 'value': 'in_D'} )
async def test_restartable(circuit): """Test restartable vs. not restartable.""" rlogger = TimeLogger('rlogger') rmono = edzed.Timer('rtimer', t_on=0.12, on_output=edzed.Event(rlogger)) nlogger = TimeLogger('nlogger') nmono = edzed.Timer('ntimer', t_on=0.12, on_output=edzed.Event(nlogger), restartable=False) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.05) assert rmono.event('start') # start OK assert nmono.event('start') # start OK await asyncio.sleep(0.05) assert rmono.event('start') # re-start OK assert not nmono.event('start') # re-start not ok! await asyncio.sleep(0.1) assert rmono.event('start') # re-start OK assert nmono.event('start') # start OK await asyncio.sleep(0.05) rmono.event('start') nmono.event('start') await asyncio.sleep(0.25) await circuit.shutdown() RLOG = [ (0, False), (50, True), (370, False), ] rlogger.compare(RLOG) NLOG = [ (0, False), (50, True), (170, False), # 120 ms (200, True), (320, False), # 120 ms ] nlogger.compare(NLOG)
def test_no_cross_circuit_events(circuit): inp1 = edzed.Input('inp', comment="circuit 1", initdef=None) event = edzed.Event(inp1) init(circuit) event.send(inp1, value=1) assert inp1.output == 1 edzed.reset_circuit() inp2 = edzed.Input('inp', comment="circuit 2", initdef=None) init(circuit) with pytest.raises(edzed.EdzedCircuitError, match="not in the current circuit"): event.send(inp2, value=2)
def test_any_name(circuit): """No reserved names (workaround for a limitation of Python < 3.8 was required).""" dest = EventMemory('dest') event = edzed.Event(dest, 'msg') src = Noop('mysrc', comment="fake event source") init(circuit) event.send(src, self='SELF', etype='ETYPE', source='anything') assert dest.output == ('msg', { 'source': 'mysrc', 'self': 'SELF', 'etype': 'ETYPE' })
async def test_init_event(circuit): """Test the initialization by an event.""" class EventOnly(edzed.SBlock): """Can be initialized only by an event.""" def _event_start(self, **data): self.set_output(data['value'] + '!') ev = EventOnly('ev') edzed.Input('inp', initdef='IV', on_output=edzed.Event(ev, 'start')) asyncio.create_task(circuit.run_forever()) await circuit.wait_init() await circuit.shutdown() assert ev.output == 'IV!'
def test_no_transition_chaining2(circuit): """Test forbidden chained transition""" class ABC(edzed.FSM): STATES = ['A', 'B', 'C'] EVENTS = [ ('start', ['A'], 'B') ] # a FSM can't send events to itself this way abc = ABC('abc', on_enter_B=edzed.Event('abc', edzed.Goto('C'))) init(circuit) with pytest.raises(edzed.EdzedCircuitError, match='Forbidden recursive event'): abc.event('start')
def test_dest_name(circuit): """Destination block names gets resolved.""" edzed.Input('src', on_output=edzed.Event('event_dest_block_name'), initdef='ok') dest = EventMemory('event_dest_block_name') init(circuit) assert dest.output == ('put', { 'source': 'src', 'trigger': 'output', 'previous': edzed.UNDEF, 'value': 'ok' })
def timelimit(limit, error): """Create a timer stopping the circuit after some time limit.""" edzed.Timer( 'test_utils_timelimit', t_off=limit, on_output=edzed.Event( '_ctrl', # automatic edzed.ControlBlock('_ctrl') 'abort' if error else 'shutdown', efilter=( edzed.Edge(rise=True), lambda data: { **data, 'error': 'time limit exceeded' } # shutdown event type will ignore the 'error' item )))
async def test_init_timeout(circuit): """Test the initialization time_out.""" logger = TimeLogger('logger') out = edzed.ValuePoll( 'out', func=lambda: edzed.UNDEF, # it never delivers interval=10, # don't care init_timeout=0.12, on_output=edzed.Event(logger), initdef='DEFAULT') asyncio.create_task(circuit.run_forever()) await circuit.wait_init() await circuit.shutdown() logger.compare([(120, 'DEFAULT')])
async def test_output(circuit): """Output is properly set when on_output is called.""" def test_timer(value): assert value == timer.output assert timer.state == ('on' if value else 'off') testfunc = edzed.OutputFunc('testfunc', func=test_timer, on_error=None) timer = edzed.Timer('timer', on_output=edzed.Event(testfunc)) asyncio.create_task(circuit.run_forever()) await asyncio.sleep(0.0) timer.event('start') timer.event('stop') timer.event('start') timer.event('stop') await circuit.shutdown()
def test_not_from_undef(circuit): """Test the not_from_undef filter.""" dest = edzed.Input('dest', initdef=None) src = edzed.Input('src', on_output=edzed.Event(dest, efilter=edzed.not_from_undef), initdef=1) init(circuit) assert src.output == 1 assert dest.output is None # UNDEF -> 1 suppressed by the filter src.put(3) assert src.output == 3 assert dest.output == 3 src.put(5) assert src.output == 5 assert dest.output == 5