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_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)
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)
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