def _unittest_redundant_transport_reconfiguration() -> None: from pyuavcan.transport import OutputSessionSpecifier, MessageDataSpecifier, PayloadMetadata tr = RedundantTransport() tr.attach_inferior(LoopbackTransport(1234)) ses = tr.get_output_session(OutputSessionSpecifier(MessageDataSpecifier(5555), None), PayloadMetadata(0)) assert ses tr.detach_inferior(tr.inferiors[0]) tr.attach_inferior(LoopbackTransport(1235)) # Different node-ID tr.detach_inferior(tr.inferiors[0]) tr.attach_inferior(LoopbackTransport(None, allow_anonymous_transfers=True)) # Anonymous with pytest.raises(pyuavcan.transport.OperationNotDefinedForAnonymousNodeError): tr.attach_inferior(LoopbackTransport(None, allow_anonymous_transfers=False)) assert len(tr.inferiors) == 1
def _unittest_redundant_transport_capture() -> None: def mon(_x: object) -> None: return None tr = RedundantTransport() inf_a = LoopbackTransport(1234) inf_b = LoopbackTransport(1234) tr.begin_capture(mon) assert inf_a.capture_handlers == [] assert inf_b.capture_handlers == [] tr.attach_inferior(inf_a) assert inf_a.capture_handlers == [mon] assert inf_b.capture_handlers == [] tr.attach_inferior(inf_b) assert inf_a.capture_handlers == [mon] assert inf_b.capture_handlers == [mon]
async def _unittest_slow_diagnostic(generated_packages: typing.List[ pyuavcan.dsdl.GeneratedPackageInfo], caplog: typing.Any) -> None: from pyuavcan.application import diagnostic from uavcan.time import SynchronizedTimestamp_1_0 assert generated_packages pres = Presentation(LoopbackTransport(2222)) pub = pres.make_publisher_with_fixed_subject_id(diagnostic.Record) diag = diagnostic.DiagnosticSubscriber(pres) diag.start() caplog.clear() await pub.publish( diagnostic.Record( timestamp=SynchronizedTimestamp_1_0(123456789), severity=diagnostic.Severity(diagnostic.Severity.INFO), text="Hello world!", )) await asyncio.sleep(1.0) print("Captured log records:") for lr in caplog.records: print(" ", lr) assert isinstance(lr, logging.LogRecord) pat = r"uavcan\.diagnostic\.Record: node=2222 severity=2 ts_sync=123\.456789 ts_local=\S+:\nHello world!" if lr.levelno == logging.INFO and re.match(pat, lr.message): break else: assert False, "Expected log message not captured" diag.close() pub.close() pres.close() await asyncio.sleep(1.0) # Let the background tasks terminate.
def _make_loopback( registers: MutableMapping[str, ValueProxy], node_id: Optional[int]) -> Iterator[pyuavcan.transport.Transport]: # Not sure if exposing this is a good idea because the loopback transport is hardly useful outside of test envs. if registers.setdefault("uavcan.loopback", ValueProxy(False)): from pyuavcan.transport.loopback import LoopbackTransport yield LoopbackTransport(node_id)
async def _unittest_slow_diagnostic_subscriber(compiled: typing.List[ pyuavcan.dsdl.GeneratedPackageInfo], caplog: typing.Any) -> None: from pyuavcan.application import make_node, NodeInfo, diagnostic, make_registry from uavcan.time import SynchronizedTimestamp_1_0 assert compiled asyncio.get_running_loop().slow_callback_duration = 1.0 node = make_node( NodeInfo(), make_registry(None, typing.cast(Dict[str, bytes], {})), transport=LoopbackTransport(2222), ) node.start() pub = node.make_publisher(diagnostic.Record) diagnostic.DiagnosticSubscriber(node) caplog.clear() await pub.publish( diagnostic.Record( timestamp=SynchronizedTimestamp_1_0(123456789), severity=diagnostic.Severity(diagnostic.Severity.INFO), text="Hello world!", )) await asyncio.sleep(1.0) print("Captured log records:") for lr in caplog.records: print(" ", lr) assert isinstance(lr, logging.LogRecord) pat = r"uavcan\.diagnostic\.Record: node=2222 severity=2 ts_sync=123\.456789 ts_local=\S+:\nHello world!" if lr.levelno == logging.INFO and re.match(pat, lr.message): break else: assert False, "Expected log message not captured" pub.close() node.close() await asyncio.sleep(1.0) # Let the background tasks terminate.
def _unittest_output_tid_file_path() -> None: from pyuavcan.transport.redundant import RedundantTransport from pyuavcan.transport.loopback import LoopbackTransport def once(tr: Transport) -> typing.Optional[pathlib.Path]: return _get_output_transfer_id_map_path(tr) assert once(LoopbackTransport(None)) is None assert once(LoopbackTransport(123)) == OUTPUT_TRANSFER_ID_MAP_DIR / "123" red = RedundantTransport() assert once(red) is None red.attach_inferior(LoopbackTransport(4000)) red.attach_inferior(LoopbackTransport(4000)) assert once(red) == OUTPUT_TRANSFER_ID_MAP_DIR / "4000" red = RedundantTransport() red.attach_inferior(LoopbackTransport(None)) red.attach_inferior(LoopbackTransport(None)) assert once(red) is None
def _unittest_redundant_input_monotonic() -> None: import pytest from pyuavcan.transport import Transfer, Timestamp, Priority from pyuavcan.transport.loopback import LoopbackTransport loop = asyncio.get_event_loop() await_ = loop.run_until_complete spec = pyuavcan.transport.InputSessionSpecifier( pyuavcan.transport.MessageDataSpecifier(4321), None) spec_tx = pyuavcan.transport.OutputSessionSpecifier( spec.data_specifier, None) meta = pyuavcan.transport.PayloadMetadata(30) ts = Timestamp.now() tr_a = LoopbackTransport(111) tr_b = LoopbackTransport(111) tx_a = tr_a.get_output_session(spec_tx, meta) tx_b = tr_b.get_output_session(spec_tx, meta) inf_a = tr_a.get_input_session(spec, meta) inf_b = tr_b.get_input_session(spec, meta) inf_a.transfer_id_timeout = 1.1 # This is used to ensure that the transfer-ID timeout is handled correctly. ses = RedundantInputSession( spec, meta, tid_modulo_provider=lambda: None, # Like UDP or serial - infinite modulo. loop=loop, finalizer=lambda: None, ) assert ses.specifier is spec assert ses.payload_metadata is meta assert not ses.inferiors assert ses.sample_statistics() == RedundantSessionStatistics() assert pytest.approx(0.0) == ses.transfer_id_timeout # Add inferiors. ses._add_inferior(inf_a) # No change, added above # pylint: disable=protected-access assert ses.inferiors == [inf_a] ses._add_inferior(inf_b) # pylint: disable=protected-access assert ses.inferiors == [inf_a, inf_b] ses.transfer_id_timeout = 1.1 assert ses.transfer_id_timeout == pytest.approx(1.1) assert inf_a.transfer_id_timeout == pytest.approx(1.1) assert inf_b.transfer_id_timeout == pytest.approx(1.1) # Redundant reception from multiple interfaces concurrently. for tx_x in (tx_a, tx_b): assert await_( tx_x.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=2, fragmented_payload=[memoryview(b"def")], ), loop.time() + 1.0, )) assert await_( tx_x.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=3, fragmented_payload=[memoryview(b"ghi")], ), loop.time() + 1.0, )) tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 2 assert tr.fragmented_payload == [memoryview(b"def")] tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 3 assert tr.fragmented_payload == [memoryview(b"ghi")] assert None is await_( ses.receive(loop.time() + 2.0)) # Nothing left to read now. # This one will be accepted despite a smaller transfer-ID because of the TID timeout. assert await_( tx_a.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=1, fragmented_payload=[memoryview(b"acc")], ), loop.time() + 1.0, )) tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 1 assert tr.fragmented_payload == [memoryview(b"acc")] assert tr.inferior_session == inf_a # Stats check. assert ses.sample_statistics() == RedundantSessionStatistics( transfers=3, frames=inf_a.sample_statistics().frames + inf_b.sample_statistics().frames, payload_bytes=9, errors=0, drops=0, inferiors=[ inf_a.sample_statistics(), inf_b.sample_statistics(), ], ) ses.close()
def _unittest_redundant_input_cyclic() -> None: import time import pytest from pyuavcan.transport import Transfer, Timestamp, Priority, ResourceClosedError from pyuavcan.transport.loopback import LoopbackTransport loop = asyncio.get_event_loop() await_ = loop.run_until_complete spec = pyuavcan.transport.InputSessionSpecifier( pyuavcan.transport.MessageDataSpecifier(4321), None) spec_tx = pyuavcan.transport.OutputSessionSpecifier( spec.data_specifier, None) meta = pyuavcan.transport.PayloadMetadata(30) ts = Timestamp.now() tr_a = LoopbackTransport(111) tr_b = LoopbackTransport(111) tx_a = tr_a.get_output_session(spec_tx, meta) tx_b = tr_b.get_output_session(spec_tx, meta) inf_a = tr_a.get_input_session(spec, meta) inf_b = tr_b.get_input_session(spec, meta) inf_a.transfer_id_timeout = 1.1 # This is used to ensure that the transfer-ID timeout is handled correctly. is_retired = False def retire() -> None: nonlocal is_retired is_retired = True ses = RedundantInputSession( spec, meta, tid_modulo_provider=lambda: 32, loop=loop, finalizer=retire # Like CAN, for example. ) assert not is_retired assert ses.specifier is spec assert ses.payload_metadata is meta assert not ses.inferiors assert ses.sample_statistics() == RedundantSessionStatistics() assert pytest.approx(0.0) == ses.transfer_id_timeout # Empty inferior set reception. time_before = loop.time() assert not await_(ses.receive(loop.time() + 2.0)) assert 1.0 < loop.time( ) - time_before < 5.0, "The method should have returned in about two seconds." # Begin reception, then add an inferior while the reception is in progress. assert await_( tx_a.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=1, fragmented_payload=[memoryview(b"abc")], ), loop.time() + 1.0, )) async def add_inferior(inferior: pyuavcan.transport.InputSession) -> None: await asyncio.sleep(1.0) ses._add_inferior(inferior) # pylint: disable=protected-access time_before = loop.time() tr, _ = await_( asyncio.gather( # Start reception here. It would stall for two seconds because no inferiors. ses.receive(loop.time() + 2.0), # While the transmission is stalled, add one inferior with a delay. add_inferior(inf_a), )) assert 0.0 < loop.time( ) - time_before < 5.0, "The method should have returned in about one second." assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 1 assert tr.fragmented_payload == [memoryview(b"abc")] assert tr.inferior_session == inf_a # More inferiors assert ses.transfer_id_timeout == pytest.approx(1.1) ses._add_inferior(inf_a) # No change, added above # pylint: disable=protected-access assert ses.inferiors == [inf_a] ses._add_inferior(inf_b) # pylint: disable=protected-access assert ses.inferiors == [inf_a, inf_b] assert ses.transfer_id_timeout == pytest.approx(1.1) assert inf_b.transfer_id_timeout == pytest.approx(1.1) # Redundant reception - new transfers accepted because the iface switch timeout is exceeded. time.sleep(ses.transfer_id_timeout ) # Just to make sure that it is REALLY exceeded. assert await_( tx_b.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=2, fragmented_payload=[memoryview(b"def")], ), loop.time() + 1.0, )) assert await_( tx_b.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=3, fragmented_payload=[memoryview(b"ghi")], ), loop.time() + 1.0, )) tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 2 assert tr.fragmented_payload == [memoryview(b"def")] assert tr.inferior_session == inf_b tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 3 assert tr.fragmented_payload == [memoryview(b"ghi")] assert tr.inferior_session == inf_b assert None is await_( ses.receive(loop.time() + 1.0)) # Nothing left to read now. # This one will be rejected because wrong iface and the switch timeout is not yet exceeded. assert await_( tx_a.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=4, fragmented_payload=[memoryview(b"rej")], ), loop.time() + 1.0, )) assert None is await_(ses.receive(loop.time() + 0.1)) # Transfer-ID timeout reconfiguration. ses.transfer_id_timeout = 3.0 with pytest.raises(ValueError): ses.transfer_id_timeout = -0.0 assert ses.transfer_id_timeout == pytest.approx(3.0) assert inf_a.transfer_id_timeout == pytest.approx(3.0) assert inf_a.transfer_id_timeout == pytest.approx(3.0) # Inferior removal resets the state of the deduplicator. ses._close_inferior(0) # pylint: disable=protected-access ses._close_inferior(1) # Out of range, no effect. # pylint: disable=protected-access assert ses.inferiors == [inf_b] assert await_( tx_b.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=1, fragmented_payload=[memoryview(b"acc")], ), loop.time() + 1.0, )) tr = await_(ses.receive(loop.time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (loop.time() + 1e-3) assert tr.priority == Priority.HIGH assert tr.transfer_id == 1 assert tr.fragmented_payload == [memoryview(b"acc")] assert tr.inferior_session == inf_b # Stats check. assert ses.sample_statistics() == RedundantSessionStatistics( transfers=4, frames=inf_b.sample_statistics().frames, payload_bytes=12, errors=0, drops=0, inferiors=[ inf_b.sample_statistics(), ], ) # Closure. assert not is_retired ses.close() assert is_retired is_retired = False ses.close() assert not is_retired assert not ses.inferiors with pytest.raises(ResourceClosedError): await_(ses.receive(0))
async def _unittest_redundant_transport(caplog: typing.Any) -> None: from pyuavcan.transport import MessageDataSpecifier, PayloadMetadata, Transfer from pyuavcan.transport import Priority, Timestamp, InputSessionSpecifier, OutputSessionSpecifier from pyuavcan.transport import ProtocolParameters loop = asyncio.get_event_loop() loop.slow_callback_duration = 1.0 tr_a = RedundantTransport() tr_b = RedundantTransport(loop) assert tr_a.sample_statistics() == RedundantTransportStatistics([]) assert tr_a.inferiors == [] assert tr_a.local_node_id is None assert tr_a.loop is asyncio.get_event_loop() assert tr_a.local_node_id is None assert tr_a.protocol_parameters == ProtocolParameters( transfer_id_modulo=0, max_nodes=0, mtu=0, ) assert tr_a.descriptor == '<redundant></redundant>' # Empty, no inferiors. assert tr_a.input_sessions == [] assert tr_a.output_sessions == [] assert tr_a.loop == tr_b.loop # # Instantiate session objects. # meta = PayloadMetadata(10_240) pub_a = tr_a.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) sub_any_a = tr_a.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), None), meta) assert pub_a is tr_a.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) assert set(tr_a.input_sessions) == {sub_any_a} assert set(tr_a.output_sessions) == {pub_a} assert tr_a.sample_statistics() == RedundantTransportStatistics() pub_b = tr_b.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) sub_any_b = tr_b.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), None), meta) sub_sel_b = tr_b.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta) assert sub_sel_b is tr_b.get_input_session( InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta) assert set(tr_b.input_sessions) == {sub_any_b, sub_sel_b} assert set(tr_b.output_sessions) == {pub_b} assert tr_b.sample_statistics() == RedundantTransportStatistics() # # Exchange test with no inferiors, expected to fail. # assert len(pub_a.inferiors) == 0 assert len(sub_any_a.inferiors) == 0 assert not await pub_a.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=1, fragmented_payload=[memoryview(b'abc')]), monotonic_deadline=loop.time() + 1.0) assert not await sub_any_a.receive_until(loop.time() + 0.1) assert not await sub_any_b.receive_until(loop.time() + 0.1) assert tr_a.sample_statistics() == RedundantTransportStatistics() assert tr_b.sample_statistics() == RedundantTransportStatistics() # # Adding inferiors - loopback, transport A only. # with pytest.raises(InconsistentInferiorConfigurationError, match='(?i).*loop.*'): tr_a.attach_inferior( LoopbackTransport( 111, loop=asyncio.new_event_loop())) # Wrong event loop. assert len(pub_a.inferiors) == 0 assert len(sub_any_a.inferiors) == 0 lo_mono_0 = LoopbackTransport(111) lo_mono_1 = LoopbackTransport(111) tr_a.attach_inferior(lo_mono_0) assert len(pub_a.inferiors) == 1 assert len(sub_any_a.inferiors) == 1 with pytest.raises(ValueError): tr_a.detach_inferior(lo_mono_1) # Not a registered inferior (yet). tr_a.attach_inferior(lo_mono_1) assert len(pub_a.inferiors) == 2 assert len(sub_any_a.inferiors) == 2 with pytest.raises(ValueError): tr_a.attach_inferior(lo_mono_0) # Double-add not allowed. with pytest.raises(InconsistentInferiorConfigurationError, match='(?i).*node-id.*'): tr_a.attach_inferior(LoopbackTransport(None)) # Wrong node-ID. with pytest.raises(InconsistentInferiorConfigurationError, match='(?i).*node-id.*'): tr_a.attach_inferior(LoopbackTransport(1230)) # Wrong node-ID. assert tr_a.inferiors == [lo_mono_0, lo_mono_1] assert len(pub_a.inferiors) == 2 assert len(sub_any_a.inferiors) == 2 assert tr_a.sample_statistics() == RedundantTransportStatistics(inferiors=[ lo_mono_0.sample_statistics(), lo_mono_1.sample_statistics(), ]) assert tr_a.local_node_id == 111 assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>' assert await pub_a.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=2, fragmented_payload=[memoryview(b'def')]), monotonic_deadline=loop.time() + 1.0) rx = await sub_any_a.receive_until(loop.time() + 1.0) assert rx is not None assert rx.fragmented_payload == [memoryview(b'def')] assert rx.transfer_id == 2 assert not await sub_any_b.receive_until(loop.time() + 0.1) # # Incapacitate one inferior, ensure things are still OK. # with caplog.at_level(logging.CRITICAL, logger=pyuavcan.transport.redundant.__name__): for s in lo_mono_0.output_sessions: s.exception = RuntimeError('INTENDED EXCEPTION') assert await pub_a.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=3, fragmented_payload=[memoryview(b'qwe')]), monotonic_deadline=loop.time() + 1.0) rx = await sub_any_a.receive_until(loop.time() + 1.0) assert rx is not None assert rx.fragmented_payload == [memoryview(b'qwe')] assert rx.transfer_id == 3 # # Remove old loopback transports. Configure new ones with cyclic TID. # lo_cyc_0 = LoopbackTransport(111) lo_cyc_1 = LoopbackTransport(111) cyc_proto_params = ProtocolParameters( transfer_id_modulo=32, # Like CAN max_nodes=128, # Like CAN mtu=63, # Like CAN ) lo_cyc_0.protocol_parameters = cyc_proto_params lo_cyc_1.protocol_parameters = cyc_proto_params assert lo_cyc_0.protocol_parameters == lo_cyc_1.protocol_parameters == cyc_proto_params assert tr_a.protocol_parameters.transfer_id_modulo >= 2**56 with pytest.raises(InconsistentInferiorConfigurationError, match='(?i).*transfer-id.*'): tr_a.attach_inferior(lo_cyc_0) # Transfer-ID modulo mismatch tr_a.detach_inferior(lo_mono_0) tr_a.detach_inferior(lo_mono_1) del lo_mono_0 # Prevent accidental reuse. del lo_mono_1 assert tr_a.inferiors == [] # All removed, okay. assert pub_a.inferiors == [] assert sub_any_a.inferiors == [] assert tr_a.local_node_id is None # Back to the roots assert tr_a.descriptor == '<redundant></redundant>' # Yes yes # Now we can add our cyclic transports safely. tr_a.attach_inferior(lo_cyc_0) assert tr_a.protocol_parameters.transfer_id_modulo == 32 tr_a.attach_inferior(lo_cyc_1) assert tr_a.protocol_parameters == cyc_proto_params, 'Protocol parameter mismatch' assert tr_a.local_node_id == 111 assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>' # Exchange test. assert await pub_a.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=4, fragmented_payload=[memoryview(b'rty')]), monotonic_deadline=loop.time() + 1.0) rx = await sub_any_a.receive_until(loop.time() + 1.0) assert rx is not None assert rx.fragmented_payload == [memoryview(b'rty')] assert rx.transfer_id == 4 # # Real heterogeneous transport test. # tr_a.detach_inferior(lo_cyc_0) tr_a.detach_inferior(lo_cyc_1) del lo_cyc_0 # Prevent accidental reuse. del lo_cyc_1 udp_a = UDPTransport('127.0.0.111/8') udp_b = UDPTransport('127.0.0.222/8') serial_a = SerialTransport(SERIAL_URI, 111) serial_b = SerialTransport(SERIAL_URI, 222, mtu=2048) # Heterogeneous. tr_a.attach_inferior(udp_a) tr_a.attach_inferior(serial_a) tr_b.attach_inferior(udp_b) tr_b.attach_inferior(serial_b) print('tr_a.descriptor', tr_a.descriptor) print('tr_b.descriptor', tr_b.descriptor) assert tr_a.protocol_parameters == ProtocolParameters( transfer_id_modulo=2**64, max_nodes=4096, mtu=1024, ) assert tr_a.local_node_id == 111 assert tr_a.descriptor == f'<redundant>{udp_a.descriptor}{serial_a.descriptor}</redundant>' assert tr_b.protocol_parameters == ProtocolParameters( transfer_id_modulo=2**64, max_nodes=4096, mtu=1024, ) assert tr_b.local_node_id == 222 assert tr_b.descriptor == f'<redundant>{udp_b.descriptor}{serial_b.descriptor}</redundant>' assert await pub_a.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=5, fragmented_payload=[memoryview(b'uio')]), monotonic_deadline=loop.time() + 1.0) rx = await sub_any_b.receive_until(loop.time() + 1.0) assert rx is not None assert rx.fragmented_payload == [memoryview(b'uio')] assert rx.transfer_id == 5 assert not await sub_any_a.receive_until(loop.time() + 0.1) assert not await sub_any_b.receive_until(loop.time() + 0.1) assert not await sub_sel_b.receive_until(loop.time() + 0.1) # # Construct new session with the transports configured. # pub_a_new = tr_a.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta) assert pub_a_new is tr_a.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta) assert set(tr_a.output_sessions) == {pub_a, pub_a_new} assert await pub_a_new.send_until(Transfer( timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=6, fragmented_payload=[memoryview(b'asd')]), monotonic_deadline=loop.time() + 1.0) rx = await sub_any_b.receive_until(loop.time() + 1.0) assert rx is not None assert rx.fragmented_payload == [memoryview(b'asd')] assert rx.transfer_id == 6 # # Termination. # tr_a.close() tr_a.close() # Idempotency tr_b.close() tr_b.close() # Idempotency with pytest.raises(pyuavcan.transport.ResourceClosedError ): # Make sure the inferiors are closed. udp_a.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) with pytest.raises(pyuavcan.transport.ResourceClosedError ): # Make sure the inferiors are closed. serial_b.get_output_session( OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta) with pytest.raises(pyuavcan.transport.ResourceClosedError ): # Make sure the sessions are closed. await pub_a.send_until(Transfer(timestamp=Timestamp.now(), priority=Priority.LOW, transfer_id=100, fragmented_payload=[]), monotonic_deadline=loop.time() + 1.0) await asyncio.sleep( 1 ) # Let all pending tasks finalize properly to avoid stack traces in the output.
async def _unittest_spoofer(caplog: pytest.LogCaptureFixture) -> None: dcs_pres = Presentation(LoopbackTransport(1234)) dcs_pub_spoof = dcs_pres.make_publisher(Spoof, 1) spoofer = Spoofer(dcs_pres.make_subscriber(Spoof, 1)) # No target transports configured -- spoofing will do nothing except incrementing the transfer-ID counter. assert await dcs_pub_spoof.publish( Spoof( timeout=uavcan.si.unit.duration.Scalar_1_0(1.0), priority=org_uavcan_yukon.io.transfer.Priority_1_0(3), session=org_uavcan_yukon.io.transfer.Session_0_1( subject=org_uavcan_yukon.io.transfer.SubjectSession_0_1( subject_id=uavcan.node.port.SubjectID_1_0(6666), source=[uavcan.node.ID_1_0(1234)])), transfer_id=[], iface_id=[], payload=org_uavcan_yukon.io.transfer.Payload_1_0(b"Hello world!"), )) await asyncio.sleep(0.5) # Validate the transfer-ID map. assert len(spoofer._transfer_id_map) == 1 assert list(spoofer._transfer_id_map.keys())[0].source_node_id == 1234 assert list(spoofer._transfer_id_map.values())[0]._value == 1 # Configure transports. cap_a: typing.List[pyuavcan.transport.Capture] = [] cap_b: typing.List[pyuavcan.transport.Capture] = [] target_tr_a = LoopbackTransport(None) target_tr_b = LoopbackTransport(None) target_tr_a.begin_capture(cap_a.append) target_tr_b.begin_capture(cap_b.append) spoofer.add_iface(111, target_tr_a) spoofer.add_iface(222, target_tr_b) # Spoof on both, successfully. spoof = Spoof( timeout=uavcan.si.unit.duration.Scalar_1_0(1.0), priority=org_uavcan_yukon.io.transfer.Priority_1_0(3), session=org_uavcan_yukon.io.transfer.Session_0_1( subject=org_uavcan_yukon.io.transfer.SubjectSession_0_1( subject_id=uavcan.node.port.SubjectID_1_0(6666), source=[uavcan.node.ID_1_0(1234)])), transfer_id=[9876543210], # This transfer will not touch the TID map. iface_id=[], # All ifaces. payload=org_uavcan_yukon.io.transfer.Payload_1_0(b"abcd"), ) assert await dcs_pub_spoof.publish(spoof) await asyncio.sleep(0.5) assert len(cap_a) == len(cap_b) (cap, ) = cap_a cap_a.clear() cap_b.clear() assert isinstance(cap, LoopbackCapture) assert cap.transfer.metadata.transfer_id == 9876543210 assert len(spoofer._transfer_id_map) == 1 # New entry was not created. assert spoofer.status == { 111: IfaceStatus(num_bytes=4, num_errors=0, num_timeouts=0, num_transfers=1, backlog=0, backlog_peak=0), 222: IfaceStatus(num_bytes=4, num_errors=0, num_timeouts=0, num_transfers=1, backlog=0, backlog_peak=0), } # Make one time out, the other raise an error, third one is closed. target_tr_a.spoof_result = False target_tr_b.spoof_result = RuntimeError("Intended exception") target_tr_c = LoopbackTransport(None) target_tr_c.close() spoofer.add_iface(0, target_tr_c) with caplog.at_level(logging.CRITICAL): assert await dcs_pub_spoof.publish(spoof) await asyncio.sleep(2.0) assert not cap_a assert not cap_b old_status = spoofer.status assert old_status == { 0: IfaceStatus(num_bytes=0, num_errors=1, num_timeouts=0, num_transfers=0, backlog=0, backlog_peak=0), 111: IfaceStatus(num_bytes=4, num_errors=0, num_timeouts=1, num_transfers=1, backlog=0, backlog_peak=0), 222: IfaceStatus(num_bytes=4, num_errors=1, num_timeouts=0, num_transfers=1, backlog=0, backlog_peak=0), } # Force only one iface out of three. Check that the backlog counter goes up. spoof.iface_id = [0] assert await dcs_pub_spoof.publish(spoof) assert await dcs_pub_spoof.publish(spoof) assert await dcs_pub_spoof.publish(spoof) await asyncio.sleep(2.0) assert spoofer.status[0].backlog > 0 assert spoofer.status[0].backlog_peak > 0 assert spoofer.status[111] == old_status[111] assert spoofer.status[222] == old_status[222] # Finalize. spoofer.close() target_tr_a.close() target_tr_b.close() target_tr_c.close() dcs_pres.close() await asyncio.sleep(1.0)
async def _unittest_redundant_output_exceptions(caplog: typing.Any) -> None: loop = asyncio.get_event_loop() spec = pyuavcan.transport.OutputSessionSpecifier( pyuavcan.transport.MessageDataSpecifier(4321), None) spec_rx = pyuavcan.transport.InputSessionSpecifier(spec.data_specifier, None) meta = pyuavcan.transport.PayloadMetadata(30 * 1024 * 1024) ts = Timestamp.now() is_retired = False def retire() -> None: nonlocal is_retired is_retired = True ses = RedundantOutputSession(spec, meta, finalizer=retire) assert not is_retired assert ses.specifier is spec assert ses.payload_metadata is meta assert not ses.inferiors assert ses.sample_statistics() == RedundantSessionStatistics() tr_a = LoopbackTransport(111) tr_b = LoopbackTransport(111) inf_a = tr_a.get_output_session(spec, meta) inf_b = tr_b.get_output_session(spec, meta) rx_a = tr_a.get_input_session(spec_rx, meta) rx_b = tr_b.get_input_session(spec_rx, meta) ses._add_inferior(inf_a) # pylint: disable=protected-access ses._add_inferior(inf_b) # pylint: disable=protected-access # Transmission with exceptions. # If at least one transmission succeeds, the call succeeds. with caplog.at_level(logging.CRITICAL, logger=__name__): inf_a.exception = RuntimeError("INTENDED EXCEPTION") assert await (ses.send( Transfer( timestamp=ts, priority=Priority.FAST, transfer_id=444444444444, fragmented_payload=[memoryview(b"INTENDED EXCEPTION")], ), loop.time() + 1.0, )) assert ses.sample_statistics() == RedundantSessionStatistics( transfers=1, frames=1, payload_bytes=len("INTENDED EXCEPTION"), errors=0, drops=0, inferiors=[ SessionStatistics( transfers=0, frames=0, payload_bytes=0, ), SessionStatistics( transfers=1, frames=1, payload_bytes=len("INTENDED EXCEPTION"), ), ], ) assert None is await (rx_a.receive(loop.time() + 1)) tf_rx = await (rx_b.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 444444444444 assert tf_rx.fragmented_payload == [memoryview(b"INTENDED EXCEPTION")] # Transmission timeout. # One times out, one raises an exception --> the result is timeout. inf_b.should_timeout = True assert not await (ses.send( Transfer( timestamp=ts, priority=Priority.FAST, transfer_id=2222222222222, fragmented_payload=[memoryview(b"INTENDED EXCEPTION")], ), loop.time() + 1.0, )) assert ses.sample_statistics().transfers == 1 assert ses.sample_statistics().payload_bytes == len( "INTENDED EXCEPTION") assert ses.sample_statistics().errors == 0 assert ses.sample_statistics().drops == 1 assert None is await (rx_a.receive(loop.time() + 1)) assert None is await (rx_b.receive(loop.time() + 1)) # Transmission with exceptions. # If all transmissions fail, the call fails. inf_b.exception = RuntimeError("INTENDED EXCEPTION") with pytest.raises(RuntimeError, match="INTENDED EXCEPTION"): assert await (ses.send( Transfer( timestamp=ts, priority=Priority.FAST, transfer_id=3333333333333, fragmented_payload=[memoryview(b"INTENDED EXCEPTION")], ), loop.time() + 1.0, )) assert ses.sample_statistics().transfers == 1 assert ses.sample_statistics().payload_bytes == len( "INTENDED EXCEPTION") assert ses.sample_statistics().errors == 1 assert ses.sample_statistics().drops == 1 assert None is await (rx_a.receive(loop.time() + 1)) assert None is await (rx_b.receive(loop.time() + 1)) # Retirement. assert not is_retired ses.close() assert is_retired # Make sure the inferiors have been closed. assert not tr_a.output_sessions assert not tr_b.output_sessions # Idempotency. is_retired = False ses.close() assert not is_retired await asyncio.sleep(2.0)
async def _unittest_redundant_output() -> None: loop = asyncio.get_event_loop() spec = pyuavcan.transport.OutputSessionSpecifier( pyuavcan.transport.MessageDataSpecifier(4321), None) spec_rx = pyuavcan.transport.InputSessionSpecifier(spec.data_specifier, None) meta = pyuavcan.transport.PayloadMetadata(30 * 1024 * 1024) ts = Timestamp.now() is_retired = False def retire() -> None: nonlocal is_retired is_retired = True ses = RedundantOutputSession(spec, meta, finalizer=retire) assert not is_retired assert ses.specifier is spec assert ses.payload_metadata is meta assert not ses.inferiors assert ses.sample_statistics() == RedundantSessionStatistics() # Transmit with an empty set of inferiors. time_before = loop.time() assert not await (ses.send( Transfer( timestamp=ts, priority=Priority.IMMEDIATE, transfer_id=1234567890, fragmented_payload=[memoryview(b"abc")], ), loop.time() + 2.0, )) assert 1.0 < loop.time( ) - time_before < 5.0, "The method should have returned in about two seconds." assert ses.sample_statistics() == RedundantSessionStatistics(drops=1, ) # Create inferiors. tr_a = LoopbackTransport(111) tr_b = LoopbackTransport(111) inf_a = tr_a.get_output_session(spec, meta) inf_b = tr_b.get_output_session(spec, meta) rx_a = tr_a.get_input_session(spec_rx, meta) rx_b = tr_b.get_input_session(spec_rx, meta) # Begin transmission, then add an inferior while it is in progress. async def add_inferior(inferior: pyuavcan.transport.OutputSession) -> None: print("sleeping before adding the inferior...") await asyncio.sleep(2.0) print("adding the inferior...") ses._add_inferior(inferior) # pylint: disable=protected-access print("inferior has been added.") assert await (asyncio.gather( # Start transmission here. It would stall for up to five seconds because no inferiors. ses.send( Transfer( timestamp=ts, priority=Priority.IMMEDIATE, transfer_id=9876543210, fragmented_payload=[memoryview(b"def")], ), loop.time() + 5.0, ), # While the transmission is stalled, add one inferior with a 2-sec delay. It will unlock the stalled task. add_inferior(inf_a), # Then make sure that the transmission has actually taken place about after two seconds from the start. )), "Transmission should have succeeded" assert 1.0 < loop.time( ) - time_before < 5.0, "The method should have returned in about two seconds." assert ses.sample_statistics() == RedundantSessionStatistics( transfers=1, frames=1, payload_bytes=3, drops=1, inferiors=[ SessionStatistics( transfers=1, frames=1, payload_bytes=3, ), ], ) tf_rx = await (rx_a.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 9876543210 assert tf_rx.fragmented_payload == [memoryview(b"def")] assert None is await (rx_b.receive(loop.time() + 0.1)) # Enable feedback. feedback: typing.List[RedundantFeedback] = [] ses.enable_feedback(feedback.append) assert await (ses.send( Transfer( timestamp=ts, priority=Priority.LOW, transfer_id=555555555555, fragmented_payload=[memoryview(b"qwerty")], ), loop.time() + 1.0, )) assert ses.sample_statistics() == RedundantSessionStatistics( transfers=2, frames=2, payload_bytes=9, drops=1, inferiors=[ SessionStatistics( transfers=2, frames=2, payload_bytes=9, ), ], ) assert len(feedback) == 1 assert feedback[0].inferior_session is inf_a assert feedback[0].original_transfer_timestamp == ts assert ts.system <= feedback[ 0].first_frame_transmission_timestamp.system <= time.time() assert ts.monotonic <= feedback[ 0].first_frame_transmission_timestamp.monotonic <= time.monotonic() assert isinstance(feedback[0].inferior_feedback, LoopbackFeedback) feedback.pop() assert not feedback tf_rx = await (rx_a.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 555555555555 assert tf_rx.fragmented_payload == [memoryview(b"qwerty")] assert None is await (rx_b.receive(loop.time() + 0.1)) # Add a new inferior and ensure that its feedback is auto-enabled! ses._add_inferior(inf_b) # pylint: disable=protected-access assert ses.inferiors == [ inf_a, inf_b, ] # Double-add has no effect. ses._add_inferior(inf_b) # pylint: disable=protected-access assert ses.inferiors == [ inf_a, inf_b, ] assert await (ses.send( Transfer( timestamp=ts, priority=Priority.FAST, transfer_id=777777777777, fragmented_payload=[memoryview(b"fgsfds")], ), loop.time() + 1.0, )) assert ses.sample_statistics() == RedundantSessionStatistics( transfers=3, frames=3 + 1, payload_bytes=15, drops=1, inferiors=[ SessionStatistics( transfers=3, frames=3, payload_bytes=15, ), SessionStatistics( transfers=1, frames=1, payload_bytes=6, ), ], ) assert len(feedback) == 2 feedback.sort(key=lambda x: x.inferior_session is not inf_a ) # Ensure consistent ordering assert feedback[0].inferior_session is inf_a assert feedback[0].original_transfer_timestamp == ts assert ts.system <= feedback[ 0].first_frame_transmission_timestamp.system <= time.time() assert ts.monotonic <= feedback[ 0].first_frame_transmission_timestamp.monotonic <= time.monotonic() assert isinstance(feedback[0].inferior_feedback, LoopbackFeedback) feedback.pop(0) assert len(feedback) == 1 assert feedback[0].inferior_session is inf_b assert feedback[0].original_transfer_timestamp == ts assert ts.system <= feedback[ 0].first_frame_transmission_timestamp.system <= time.time() assert ts.monotonic <= feedback[ 0].first_frame_transmission_timestamp.monotonic <= time.monotonic() assert isinstance(feedback[0].inferior_feedback, LoopbackFeedback) feedback.pop() assert not feedback tf_rx = await (rx_a.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 777777777777 assert tf_rx.fragmented_payload == [memoryview(b"fgsfds")] tf_rx = await (rx_b.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 777777777777 assert tf_rx.fragmented_payload == [memoryview(b"fgsfds")] # Remove the first inferior. ses._close_inferior(0) # pylint: disable=protected-access assert ses.inferiors == [inf_b] ses._close_inferior(1) # Out of range, no effect. # pylint: disable=protected-access assert ses.inferiors == [inf_b] # Make sure the removed inferior has been closed. assert not tr_a.output_sessions # Transmission test with the last inferior. assert await (ses.send( Transfer( timestamp=ts, priority=Priority.HIGH, transfer_id=88888888888888, fragmented_payload=[memoryview(b"hedgehog")], ), loop.time() + 1.0, )) assert ses.sample_statistics().transfers == 4 # We don't check frames because this stat metric is computed quite clumsily atm, this may change later. assert ses.sample_statistics().payload_bytes == 23 assert ses.sample_statistics().drops == 1 assert ses.sample_statistics().inferiors == [ SessionStatistics( transfers=2, frames=2, payload_bytes=14, ), ] assert len(feedback) == 1 assert feedback[0].inferior_session is inf_b assert feedback[0].original_transfer_timestamp == ts assert ts.system <= feedback[ 0].first_frame_transmission_timestamp.system <= time.time() assert ts.monotonic <= feedback[ 0].first_frame_transmission_timestamp.monotonic <= time.monotonic() assert isinstance(feedback[0].inferior_feedback, LoopbackFeedback) feedback.pop() assert not feedback assert None is await (rx_a.receive(loop.time() + 1)) tf_rx = await (rx_b.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 88888888888888 assert tf_rx.fragmented_payload == [memoryview(b"hedgehog")] # Disable the feedback. ses.disable_feedback() # A diversion - enable the feedback in the inferior and make sure it's not propagated. ses._enable_feedback_on_inferior(inf_b) # pylint: disable=protected-access assert await (ses.send( Transfer( timestamp=ts, priority=Priority.OPTIONAL, transfer_id=666666666666666, fragmented_payload=[memoryview(b"horse")], ), loop.time() + 1.0, )) assert ses.sample_statistics().transfers == 5 # We don't check frames because this stat metric is computed quite clumsily atm, this may change later. assert ses.sample_statistics().payload_bytes == 28 assert ses.sample_statistics().drops == 1 assert ses.sample_statistics().inferiors == [ SessionStatistics( transfers=3, frames=3, payload_bytes=19, ), ] assert not feedback assert None is await (rx_a.receive(loop.time() + 1)) tf_rx = await (rx_b.receive(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 666666666666666 assert tf_rx.fragmented_payload == [memoryview(b"horse")] # Retirement. assert not is_retired ses.close() assert is_retired # Make sure the inferiors have been closed. assert not tr_a.output_sessions assert not tr_b.output_sessions # Idempotency. is_retired = False ses.close() assert not is_retired # Use after close. with pytest.raises(ResourceClosedError): await (ses.send( Transfer( timestamp=ts, priority=Priority.OPTIONAL, transfer_id=1111111111111, fragmented_payload=[memoryview(b"cat")], ), loop.time() + 1.0, )) assert None is await (rx_a.receive(loop.time() + 1)) assert None is await (rx_b.receive(loop.time() + 1)) await asyncio.sleep(2.0)
def _unittest_redundant_output_exceptions() -> None: import pytest from pyuavcan.transport import Transfer, Timestamp, Priority, SessionStatistics from pyuavcan.transport import TransferFrom from pyuavcan.transport.loopback import LoopbackTransport loop = asyncio.get_event_loop() await_ = loop.run_until_complete spec = pyuavcan.transport.OutputSessionSpecifier(pyuavcan.transport.MessageDataSpecifier(4321), None) spec_rx = pyuavcan.transport.InputSessionSpecifier(spec.data_specifier, None) meta = pyuavcan.transport.PayloadMetadata(0x_deadbeef_deadbeef, 30 * 1024 * 1024) ts = Timestamp.now() is_retired = False def retire() -> None: nonlocal is_retired is_retired = True ses = RedundantOutputSession(spec, meta, loop=loop, finalizer=retire) assert not is_retired assert ses.specifier is spec assert ses.payload_metadata is meta assert not ses.inferiors assert ses.sample_statistics() == RedundantSessionStatistics() tr_a = LoopbackTransport(111) tr_b = LoopbackTransport(111) inf_a = tr_a.get_output_session(spec, meta) inf_b = tr_b.get_output_session(spec, meta) rx_a = tr_a.get_input_session(spec_rx, meta) rx_b = tr_b.get_input_session(spec_rx, meta) # noinspection PyProtectedMember ses._add_inferior(inf_a) # noinspection PyProtectedMember ses._add_inferior(inf_b) # Transmission with exceptions. # If at least one transmission succeeds, the call succeeds. inf_a.exception = RuntimeError('EXCEPTION SUKA') assert await_(ses.send_until( Transfer(timestamp=ts, priority=Priority.FAST, transfer_id=444444444444, fragmented_payload=[memoryview(b'exception suka')]), loop.time() + 1.0 )) assert ses.sample_statistics() == RedundantSessionStatistics( transfers=1, frames=1, payload_bytes=len('exception suka'), errors=0, drops=0, inferiors=[ SessionStatistics( transfers=0, frames=0, payload_bytes=0, ), SessionStatistics( transfers=1, frames=1, payload_bytes=len('exception suka'), ), ], ) assert None is await_(rx_a.receive_until(loop.time() + 1)) tf_rx = await_(rx_b.receive_until(loop.time() + 1)) assert isinstance(tf_rx, TransferFrom) assert tf_rx.transfer_id == 444444444444 assert tf_rx.fragmented_payload == [memoryview(b'exception suka')] # Transmission timeout. # One times out, one raises an exception --> the result is timeout. inf_b.should_timeout = True assert not await_(ses.send_until( Transfer(timestamp=ts, priority=Priority.FAST, transfer_id=2222222222222, fragmented_payload=[memoryview(b'exception suka')]), loop.time() + 1.0 )) assert ses.sample_statistics().transfers == 1 assert ses.sample_statistics().payload_bytes == len('exception suka') assert ses.sample_statistics().errors == 0 assert ses.sample_statistics().drops == 1 assert None is await_(rx_a.receive_until(loop.time() + 1)) assert None is await_(rx_b.receive_until(loop.time() + 1)) # Transmission with exceptions. # If all transmissions fail, the call fails. inf_b.exception = RuntimeError('EXCEPTION SUKA') with pytest.raises(RuntimeError, match='EXCEPTION SUKA'): assert await_(ses.send_until( Transfer(timestamp=ts, priority=Priority.FAST, transfer_id=3333333333333, fragmented_payload=[memoryview(b'exception suka')]), loop.time() + 1.0 )) assert ses.sample_statistics().transfers == 1 assert ses.sample_statistics().payload_bytes == len('exception suka') assert ses.sample_statistics().errors == 1 assert ses.sample_statistics().drops == 1 assert None is await_(rx_a.receive_until(loop.time() + 1)) assert None is await_(rx_b.receive_until(loop.time() + 1)) # Retirement. assert not is_retired ses.close() assert is_retired # Make sure the inferiors have been closed. assert not tr_a.output_sessions assert not tr_b.output_sessions # Idempotency. is_retired = False ses.close() assert not is_retired