async def _unittest_redundant_input_monotonic() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 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: 2 ** 56, # Like UDP or serial - infinite modulo. 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")], ), asyncio.get_running_loop().time() + 1.0, ) ) assert await ( tx_x.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=3, fragmented_payload=[memoryview(b"ghi")], ), asyncio.get_running_loop().time() + 1.0, ) ) tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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(asyncio.get_running_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")], ), asyncio.get_running_loop().time() + 1.0, ) ) tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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() tr_a.close() tr_b.close() inf_a.close() inf_b.close() await asyncio.sleep(2.0)
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_input_cyclic() -> None: asyncio.get_running_loop().slow_callback_duration = 5.0 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, 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 = asyncio.get_running_loop().time() assert not await (ses.receive(asyncio.get_running_loop().time() + 2.0)) assert ( 1.0 < asyncio.get_running_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")], ), asyncio.get_running_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 = asyncio.get_running_loop().time() tr, _ = await ( asyncio.gather( # Start reception here. It would stall for two seconds because no inferiors. ses.receive(asyncio.get_running_loop().time() + 2.0), # While the transmission is stalled, add one inferior with a delay. add_inferior(inf_a), ) ) assert ( 0.0 < asyncio.get_running_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 <= (asyncio.get_running_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")], ), asyncio.get_running_loop().time() + 1.0, ) ) assert await ( tx_b.send( Transfer( timestamp=Timestamp.now(), priority=Priority.HIGH, transfer_id=3, fragmented_payload=[memoryview(b"ghi")], ), asyncio.get_running_loop().time() + 1.0, ) ) tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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(asyncio.get_running_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")], ), asyncio.get_running_loop().time() + 1.0, ) ) assert None is await (ses.receive(asyncio.get_running_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")], ), asyncio.get_running_loop().time() + 1.0, ) ) tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1)) assert isinstance(tr, RedundantTransferFrom) assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_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)) tr_a.close() tr_b.close() inf_a.close() inf_b.close() await asyncio.sleep(2.0)