Exemple #1
0
async def _unittest_can_transport_non_anon() -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom
    from pyuavcan.transport import UnsupportedSessionConfigurationError, Priority, SessionStatistics, Timestamp
    from pyuavcan.transport import ResourceClosedError, InputSessionSpecifier, OutputSessionSpecifier
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._identifier import MessageCANID, ServiceCANID
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._frame import UAVCANFrame
    from .media.mock import MockMedia, FrameCollector

    peers: typing.Set[MockMedia] = set()
    media = MockMedia(peers, 64, 10)
    media2 = MockMedia(peers, 64, 3)
    peeper = MockMedia(peers, 64, 10)
    assert len(peers) == 3

    tr = can.CANTransport(media, 5)
    tr2 = can.CANTransport(media2, 123)

    assert tr.protocol_parameters == pyuavcan.transport.ProtocolParameters(
        transfer_id_modulo=32, max_nodes=128, mtu=63)
    assert tr.local_node_id == 5
    assert tr.protocol_parameters == tr2.protocol_parameters

    assert media.automatic_retransmission_enabled
    assert media2.automatic_retransmission_enabled

    #
    # Instantiate session objects
    #
    meta = PayloadMetadata(0x_bad_c0ffee_0dd_f00d, 10000)

    with pytest.raises(Exception):  # Can't broadcast service calls
        tr.get_output_session(
            OutputSessionSpecifier(
                ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE),
                None), meta)

    with pytest.raises(
            UnsupportedSessionConfigurationError):  # Can't unicast messages
        tr.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(1234), 123), meta)

    broadcaster = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    subscriber_promiscuous = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)
    assert subscriber_promiscuous is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)

    subscriber_selective = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta)

    server_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)

    server_responder = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    client_requester = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 123),
        meta)

    client_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    assert set(tr.input_sessions) == {
        subscriber_promiscuous, subscriber_selective, server_listener,
        client_listener
    }
    assert set(tr.output_sessions) == {
        broadcaster, server_responder, client_requester
    }

    #
    # Basic exchange test, no one is listening
    #
    media2.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])
    peeper.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])

    collector = FrameCollector()
    peeper.start(collector.give, False)

    assert tr.sample_statistics() == can.CANTransportStatistics()
    assert tr2.sample_statistics() == can.CANTransportStatistics()

    ts = Timestamp.now()

    def validate_timestamp(timestamp: Timestamp) -> None:
        assert ts.monotonic_ns <= timestamp.monotonic_ns <= time.monotonic_ns()
        assert ts.system_ns <= timestamp.system_ns <= time.time_ns()

    assert await broadcaster.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem('abc'), _mem('def')]),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=1, in_frames_uavcan=1)
    assert tr.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(1)
    assert tr2.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(0)
    assert tr.sample_statistics().lost_loopback_frames == 0
    assert tr2.sample_statistics().lost_loopback_frames == 0

    assert collector.pop().is_same_manifestation(
        UAVCANFrame(
            identifier=MessageCANID(Priority.IMMEDIATE, 5, 12345).compile(
                [_mem('abcdef')]),  # payload fragments joined
            padded_payload=_mem('abcdef'),
            transfer_id=11,
            start_of_transfer=True,
            end_of_transfer=True,
            toggle_bit=True,
            loopback=False).compile())
    assert collector.empty

    #
    # Broadcast exchange with input dispatch test
    #
    selective_m12345_5 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 5), meta)
    selective_m12345_9 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 9), meta)
    promiscuous_m12345 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    assert await broadcaster.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem('abc'), _mem('def')]),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=2, frames=2, payload_bytes=12)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=2)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=2, in_frames_uavcan=2, in_frames_uavcan_accepted=1)

    received = await promiscuous_m12345.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id == 5
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem('abcdef')]

    assert selective_m12345_5.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert selective_m12345_9.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert promiscuous_m12345.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert media.automatic_retransmission_enabled
    assert media2.automatic_retransmission_enabled

    feedback_collector = _FeedbackCollector()

    broadcaster.enable_feedback(feedback_collector.give)
    assert await broadcaster.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.SLOW,
            transfer_id=2,
            fragmented_payload=[_mem('qwe'), _mem('rty')] *
            50  # Lots of data here, very multiframe
        ),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=3, frames=7, payload_bytes=312)
    broadcaster.disable_feedback()

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=7, out_frames_loopback=1, in_frames_loopback=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=7, in_frames_uavcan=7, in_frames_uavcan_accepted=6)

    fb = feedback_collector.take()
    assert fb.original_transfer_timestamp == ts
    validate_timestamp(fb.first_frame_transmission_timestamp)

    received = await promiscuous_m12345.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 2
    assert received.source_node_id == 5
    assert received.priority == Priority.SLOW
    validate_timestamp(received.timestamp)
    assert b''.join(
        received.fragmented_payload
    ) == b'qwerty' * 50 + b'\x00' * 13  # The 0x00 at the end is padding

    assert await broadcaster.send_until(
        Transfer(timestamp=ts,
                 priority=Priority.OPTIONAL,
                 transfer_id=3,
                 fragmented_payload=[_mem('qwe'), _mem('rty')]),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=4, frames=8, payload_bytes=318)

    received = await promiscuous_m12345.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 3
    assert received.source_node_id == 5
    assert received.priority == Priority.OPTIONAL
    validate_timestamp(received.timestamp)
    assert list(received.fragmented_payload) == [_mem('qwerty')]

    assert promiscuous_m12345.sample_statistics() == SessionStatistics(
        transfers=3, frames=7, payload_bytes=325)

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=8, out_frames_loopback=1, in_frames_loopback=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=8, in_frames_uavcan=8, in_frames_uavcan_accepted=7)

    broadcaster.close()
    with pytest.raises(ResourceClosedError):
        assert await broadcaster.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.LOW,
                     transfer_id=4,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)
    broadcaster.close()  # Does nothing

    # Final checks for the broadcaster - make sure nothing is left in the queue
    assert (await promiscuous_m12345.receive_until(tr.loop.time() +
                                                   _RX_TIMEOUT)) is None

    # The selective listener was not supposed to pick up anything because it's selective for node 9, not 5
    assert (await selective_m12345_9.receive_until(tr.loop.time() +
                                                   _RX_TIMEOUT)) is None

    # Now, there are a bunch of items awaiting in the selective input for node 5, collect them and check the stats
    assert selective_m12345_5.source_node_id == 5

    received = await selective_m12345_5.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id == 5
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem('abcdef')]

    received = await selective_m12345_5.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 2
    assert received.source_node_id == 5
    assert received.priority == Priority.SLOW
    validate_timestamp(received.timestamp)
    assert b''.join(
        received.fragmented_payload
    ) == b'qwerty' * 50 + b'\x00' * 13  # The 0x00 at the end is padding

    received = await selective_m12345_5.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 3
    assert received.source_node_id == 5
    assert received.priority == Priority.OPTIONAL
    validate_timestamp(received.timestamp)
    assert list(received.fragmented_payload) == [_mem('qwerty')]

    assert selective_m12345_5.sample_statistics(
    ) == promiscuous_m12345.sample_statistics()

    #
    # Unicast exchange test
    #
    selective_server_s333_5 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 5),
        meta)
    selective_server_s333_9 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 9),
        meta)
    promiscuous_server_s333 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)

    selective_client_s333_5 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 5),
        meta)
    selective_client_s333_9 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE), 9),
        meta)
    promiscuous_client_s333 = tr2.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            None), meta)

    assert await client_requester.send_until(
        Transfer(timestamp=ts,
                 priority=Priority.FAST,
                 transfer_id=11,
                 fragmented_payload=[]),
        tr.loop.time() + 1.0)
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=0)

    received = await selective_server_s333_5.receive_until(
        tr.loop.time() + 1.0)  # Same thing here
    assert received is not None
    assert received.transfer_id == 11
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [b'']

    assert (await selective_server_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None

    received = await promiscuous_server_s333.receive_until(
        tr.loop.time() + 1.0)  # Same thing here
    assert received is not None
    assert received.transfer_id == 11
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [b'']

    assert selective_server_s333_5.sample_statistics() == SessionStatistics(
        transfers=1, frames=1)
    assert selective_server_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_server_s333.sample_statistics() == SessionStatistics(
        transfers=1, frames=1)

    assert (await selective_client_s333_5.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert selective_client_s333_5.sample_statistics() == SessionStatistics()
    assert selective_client_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_client_s333.sample_statistics() == SessionStatistics()

    client_requester.enable_feedback(
        feedback_collector.give)  # FEEDBACK ENABLED HERE

    # Will fail with an error; make sure it's counted properly. The feedback registry entry will remain pending!
    media.raise_on_send_once(RuntimeError('Induced failure'))
    with pytest.raises(RuntimeError, match='Induced failure'):
        assert await client_requester.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.FAST,
                     transfer_id=12,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=0, errors=1)

    # Some malformed feedback frames which will be ignored
    media.inject_received([
        UAVCANFrame(
            identifier=ServiceCANID(priority=Priority.FAST,
                                    source_node_id=5,
                                    destination_node_id=123,
                                    service_id=333,
                                    request_not_response=True).compile(
                                        [_mem('Ignored')]),
            padded_payload=_mem('Ignored'),
            start_of_transfer=False,  # Ignored because not start-of-frame
            end_of_transfer=False,
            toggle_bit=True,
            transfer_id=12,
            loopback=True).compile()
    ])

    media.inject_received([
        UAVCANFrame(
            identifier=ServiceCANID(priority=Priority.FAST,
                                    source_node_id=5,
                                    destination_node_id=123,
                                    service_id=333,
                                    request_not_response=True).compile(
                                        [_mem('Ignored')]),
            padded_payload=_mem('Ignored'),
            start_of_transfer=True,
            end_of_transfer=False,
            toggle_bit=True,
            transfer_id=
            9,  # Ignored because there is no such transfer-ID in the registry
            loopback=True).compile()
    ])

    # Now, this transmission will succeed, but a pending loopback registry entry will be overwritten, which will be
    # reflected in the error counter.
    assert await client_requester.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.FAST,
            transfer_id=12,
            fragmented_payload=[
                _mem(
                    'Until philosophers are kings, or the kings and princes of this world have the spirit and power of '
                    'philosophy, and political greatness and wisdom meet in one, and those commoner natures who pursue '
                    'either to the exclusion of the other are compelled to stand aside, cities will never have rest from '
                    'their evils '),
                _mem('- no, nor the human race, as I believe - '),
                _mem(
                    'and then only will this our State have a possibility of life and behold the light of day.'
                ),
            ]),
        tr.loop.time() + 1.0)
    client_requester.disable_feedback()
    assert client_requester.sample_statistics() == SessionStatistics(
        transfers=2, frames=8, payload_bytes=438, errors=2)

    # The feedback is disabled, but we will send a valid loopback frame anyway to make sure it is silently ignored
    media.inject_received([
        UAVCANFrame(identifier=ServiceCANID(priority=Priority.FAST,
                                            source_node_id=5,
                                            destination_node_id=123,
                                            service_id=333,
                                            request_not_response=True).compile(
                                                [_mem('Ignored')]),
                    padded_payload=_mem('Ignored'),
                    start_of_transfer=True,
                    end_of_transfer=False,
                    toggle_bit=True,
                    transfer_id=12,
                    loopback=True).compile()
    ])

    client_requester.close()
    with pytest.raises(ResourceClosedError):
        assert await client_requester.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.LOW,
                     transfer_id=4,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0)

    fb = feedback_collector.take()
    assert fb.original_transfer_timestamp == ts
    validate_timestamp(fb.first_frame_transmission_timestamp)

    received = await promiscuous_server_s333.receive_until(tr.loop.time() +
                                                           1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 5
    assert received.transfer_id == 12
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert len(received.fragmented_payload) == 7  # Equals the number of frames
    assert sum(map(
        len, received.fragmented_payload)) == 438 + 1  # Padding also included
    assert b'Until philosophers are kings' in bytes(
        received.fragmented_payload[0])
    assert b'behold the light of day.' in bytes(
        received.fragmented_payload[-1])

    received = await selective_server_s333_5.receive_until(
        tr.loop.time() + 1.0)  # Same thing here
    assert received is not None
    assert received.transfer_id == 12
    assert received.priority == Priority.FAST
    validate_timestamp(received.timestamp)
    assert len(received.fragmented_payload) == 7  # Equals the number of frames
    assert sum(map(
        len, received.fragmented_payload)) == 438 + 1  # Padding also included
    assert b'Until philosophers are kings' in bytes(
        received.fragmented_payload[0])
    assert b'behold the light of day.' in bytes(
        received.fragmented_payload[-1])

    # Nothing is received - non-matching node ID selector
    assert (await selective_server_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None

    # Nothing is received - non-matching role (not server)
    assert (await selective_client_s333_5.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await selective_client_s333_9.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert (await promiscuous_client_s333.receive_until(tr.loop.time() +
                                                        _RX_TIMEOUT)) is None
    assert selective_client_s333_5.sample_statistics() == SessionStatistics()
    assert selective_client_s333_9.sample_statistics() == SessionStatistics()
    assert promiscuous_client_s333.sample_statistics() == SessionStatistics()

    # Final transport stats check; additional loopback frames are due to our manual tests above
    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16, out_frames_loopback=2, in_frames_loopback=5)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15)

    #
    # Drop non-UAVCAN frames silently
    #
    media.inject_received([
        can.media.DataFrame(
            identifier=ServiceCANID(priority=Priority.FAST,
                                    source_node_id=5,
                                    destination_node_id=123,
                                    service_id=333,
                                    request_not_response=True).compile(
                                        [_mem('')]),
            data=bytearray(
                b''
            ),  # The CAN ID is valid for UAVCAN, but the payload is not - no tail byte
            format=can.media.FrameFormat.EXTENDED,
            loopback=False)
    ])

    media.inject_received([
        can.media.DataFrame(
            identifier=0,  # The CAN ID is not valid for UAVCAN
            data=bytearray(b'123'),
            format=can.media.FrameFormat.BASE,
            loopback=False)
    ])

    media.inject_received([
        UAVCANFrame(
            identifier=ServiceCANID(
                priority=Priority.FAST,
                source_node_id=5,
                destination_node_id=123,
                service_id=444,  # No such service
                request_not_response=True).compile([_mem('Ignored')]),
            padded_payload=_mem('Ignored'),
            start_of_transfer=True,
            end_of_transfer=False,
            toggle_bit=True,
            transfer_id=12,
            loopback=True).compile()
    ])

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=2,
        out_frames_loopback=2,
        in_frames_loopback=6)

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=16, in_frames_uavcan=16, in_frames_uavcan_accepted=15)

    #
    # Reception logic test.
    #
    pub_m2222 = tr2.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2222), None), meta)

    # Transfer ID timeout configuration - one of them will be configured very short for testing purposes
    subscriber_promiscuous.transfer_id_timeout = 1e-9  # Very low, basically zero timeout
    with pytest.raises(ValueError):
        subscriber_promiscuous.transfer_id_timeout = -1
    with pytest.raises(ValueError):
        subscriber_promiscuous.transfer_id_timeout = float('nan')
    assert subscriber_promiscuous.transfer_id_timeout == pytest.approx(1e-9)

    subscriber_selective.transfer_id_timeout = 1.0
    with pytest.raises(ValueError):
        subscriber_selective.transfer_id_timeout = -1
    with pytest.raises(ValueError):
        subscriber_selective.transfer_id_timeout = float('nan')
    assert subscriber_selective.transfer_id_timeout == pytest.approx(1.0)

    # Queue capacity configuration
    assert subscriber_selective.frame_queue_capacity is None  # Unlimited by default
    subscriber_selective.frame_queue_capacity = 2
    with pytest.raises(ValueError):
        subscriber_selective.frame_queue_capacity = 0
    assert subscriber_selective.frame_queue_capacity == 2

    assert await pub_m2222.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.EXCEPTIONAL,
            transfer_id=7,
            fragmented_payload=[
                _mem('Finally, from so little sleeping and so much reading, '),
                _mem(
                    'his brain dried up and he went completely out of his mind.'
                ),  # Two frames.
            ]),
        tr.loop.time() + 1.0)

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=4,
        in_frames_uavcan=2,
        in_frames_uavcan_accepted=2,
        out_frames_loopback=2,
        in_frames_loopback=6)

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        out_frames=2,
        in_frames=16,
        in_frames_uavcan=16,
        in_frames_uavcan_accepted=15)

    received = await subscriber_promiscuous.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 123
    assert received.priority == Priority.EXCEPTIONAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert bytes(received.fragmented_payload[0]).startswith(b'Finally')
    assert bytes(received.fragmented_payload[-1]).rstrip(b'\x00').endswith(
        b'out of his mind.')

    received = await subscriber_selective.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert received.priority == Priority.EXCEPTIONAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert bytes(received.fragmented_payload[0]).startswith(b'Finally')
    assert bytes(received.fragmented_payload[-1]).rstrip(b'\x00').endswith(
        b'out of his mind.')

    assert subscriber_selective.sample_statistics(
    ) == subscriber_promiscuous.sample_statistics()
    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=1, frames=2, payload_bytes=124)  # Includes padding!

    assert await pub_m2222.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=
            7,  # Same transfer ID, will be accepted only by the instance with low TID timeout
            fragmented_payload=[]),
        tr.loop.time() + 1.0)

    assert tr.sample_statistics() == can.CANTransportStatistics(
        out_frames=16,
        in_frames=5,
        in_frames_uavcan=3,
        in_frames_uavcan_accepted=3,
        out_frames_loopback=2,
        in_frames_loopback=6)

    assert tr2.sample_statistics() == can.CANTransportStatistics(
        out_frames=3,
        in_frames=16,
        in_frames_uavcan=16,
        in_frames_uavcan_accepted=15)

    received = await subscriber_promiscuous.receive_until(tr.loop.time() +
                                                          10.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.source_node_id == 123
    assert received.priority == Priority.NOMINAL
    assert received.transfer_id == 7
    validate_timestamp(received.timestamp)
    assert b''.join(received.fragmented_payload) == b''

    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=2, frames=3, payload_bytes=124)

    # Discarded because of the same transfer ID
    assert (await subscriber_selective.receive_until(tr.loop.time() +
                                                     _RX_TIMEOUT)) is None
    assert subscriber_selective.sample_statistics() == SessionStatistics(
        transfers=1,
        frames=3,
        payload_bytes=124,
        errors=1  # Error due to the repeated transfer ID
    )

    assert await pub_m2222.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.HIGH,
            transfer_id=8,
            fragmented_payload=[
                _mem('a' * 63),
                _mem('b' * 63),
                _mem('c' * 63),
                _mem(
                    'd' * 62
                ),  # Tricky case - one of the CRC bytes spills over into the fifth frame
            ]),
        tr.loop.time() + 1.0)

    # The promiscuous one is able to receive the transfer since its queue is large enough
    received = await subscriber_promiscuous.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert received.priority == Priority.HIGH
    assert received.transfer_id == 8
    validate_timestamp(received.timestamp)
    assert list(map(bytes, received.fragmented_payload)) == [
        b'a' * 63,
        b'b' * 63,
        b'c' * 63,
        b'd' * 62,
    ]
    assert subscriber_promiscuous.sample_statistics() == SessionStatistics(
        transfers=3, frames=8, payload_bytes=375)

    # The selective one is unable to do so since its RX queue is too small; it is reflected in the error counter
    assert (await subscriber_selective.receive_until(tr.loop.time() +
                                                     _RX_TIMEOUT)) is None
    assert subscriber_selective.sample_statistics() == SessionStatistics(
        transfers=1, frames=5, payload_bytes=124, errors=1,
        drops=3)  # Overruns!

    #
    # Finalization.
    #
    print('str(CANTransport):', tr)
    print('str(CANTransport):', tr2)
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
    # Double-close has no effect:
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
Exemple #2
0
def _unittest_output_session() -> None:
    from pytest import raises
    from pyuavcan.transport import OutputSessionSpecifier, MessageDataSpecifier, ServiceDataSpecifier, Priority
    from pyuavcan.transport import PayloadMetadata, SessionStatistics, Timestamp, Feedback, Transfer

    ts = Timestamp.now()
    loop = asyncio.get_event_loop()
    run_until_complete = loop.run_until_complete
    finalized = False

    def do_finalize() -> None:
        nonlocal finalized
        finalized = True

    def check_timestamp(t: pyuavcan.transport.Timestamp) -> bool:
        now = pyuavcan.transport.Timestamp.now()
        s = ts.system_ns <= t.system_ns <= now.system_ns
        m = ts.monotonic_ns <= t.monotonic_ns <= now.system_ns
        return s and m

    destination_endpoint = '127.100.0.1', 25406

    sock_rx = socket_.socket(socket_.AF_INET, socket_.SOCK_DGRAM)
    sock_rx.bind(destination_endpoint)
    sock_rx.settimeout(1.0)

    def make_sock() -> socket_.socket:
        sock = socket_.socket(socket_.AF_INET, socket_.SOCK_DGRAM)
        sock.bind(('127.100.0.2', 0))
        sock.connect(destination_endpoint)
        sock.setblocking(False)
        return sock

    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(MessageDataSpecifier(3210), None),
        payload_metadata=PayloadMetadata(0xdead_beef_badc0ffe, 1024),
        mtu=11,
        multiplier=1,
        sock=make_sock(),
        loop=asyncio.get_event_loop(),
        finalizer=do_finalize,
    )

    assert sos.specifier == OutputSessionSpecifier(MessageDataSpecifier(3210),
                                                   None)
    assert sos.destination_node_id is None
    assert sos.payload_metadata == PayloadMetadata(0xdead_beef_badc0ffe, 1024)
    assert sos.sample_statistics() == SessionStatistics()

    assert run_until_complete(
        sos.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.NOMINAL,
                     transfer_id=12340,
                     fragmented_payload=[
                         memoryview(b'one'),
                         memoryview(b'two'),
                         memoryview(b'three')
                     ]),
            loop.time() + 10.0))

    rx_data, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    assert rx_data == (
        b'\x00\x04\x00\x00\x00\x00\x00\x8040\x00\x00\x00\x00\x00\x00\xfe\x0f\xdc\xba\xef\xbe\xad\xde'
        + b'one'
        b'two'
        b'three')
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    last_feedback: typing.Optional[Feedback] = None

    def feedback_handler(feedback: Feedback) -> None:
        nonlocal last_feedback
        last_feedback = feedback

    sos.enable_feedback(feedback_handler)

    assert last_feedback is None
    assert run_until_complete(
        sos.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.NOMINAL,
                     transfer_id=12340,
                     fragmented_payload=[]),
            loop.time() + 10.0))
    assert last_feedback is not None
    assert last_feedback.original_transfer_timestamp == ts
    assert check_timestamp(last_feedback.first_frame_transmission_timestamp)

    sos.disable_feedback()
    sos.disable_feedback()  # Idempotency check

    _, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    assert sos.sample_statistics() == SessionStatistics(transfers=2,
                                                        frames=2,
                                                        payload_bytes=11,
                                                        errors=0,
                                                        drops=0)

    assert sos.socket.fileno() >= 0
    assert not finalized
    sos.close()
    assert finalized
    assert sos.socket.fileno() < 0  # The socket is supposed to be disposed of.
    finalized = False

    # Multi-frame with multiplication
    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(
            ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
            2222),
        payload_metadata=PayloadMetadata(0xdead_beef_badc0ffe, 1024),
        mtu=10,
        multiplier=2,
        sock=make_sock(),
        loop=asyncio.get_event_loop(),
        finalizer=do_finalize,
    )
    assert run_until_complete(
        sos.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.OPTIONAL,
                     transfer_id=54321,
                     fragmented_payload=[
                         memoryview(b'one'),
                         memoryview(b'two'),
                         memoryview(b'three')
                     ]),
            loop.time() + 10.0))
    data_main_a, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    data_main_b, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    data_redundant_a, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    data_redundant_b, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == '127.100.0.2'
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    print('data_main_a', data_main_a)
    print('data_main_b', data_main_b)
    print('data_redundant_a', data_redundant_a)
    print('data_redundant_b', data_redundant_b)

    assert data_main_a == data_redundant_a
    assert data_main_b == data_redundant_b
    assert data_main_a == (
        b'\x00\x07\x00\x00\x00\x00\x00\x001\xd4\x00\x00\x00\x00\x00\x00\xfe\x0f\xdc\xba\xef\xbe\xad\xde'
        + b'one'
        b'two'
        b'three'[:-1])
    assert data_main_b == (
        b'\x00\x07\x00\x00\x01\x00\x00\x801\xd4\x00\x00\x00\x00\x00\x00\xfe\x0f\xdc\xba\xef\xbe\xad\xde'
        + b'e' + pyuavcan.transport.commons.crc.CRC32C.new(
            b'one', b'two', b'three').value_as_bytes)

    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(
            ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
            2222),
        payload_metadata=PayloadMetadata(0xdead_beef_badc0ffe, 1024),
        mtu=10,
        multiplier=1,
        sock=make_sock(),
        loop=asyncio.get_event_loop(),
        finalizer=do_finalize,
    )

    # Induced timeout
    assert not run_until_complete(
        sos.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.NOMINAL,
                     transfer_id=12340,
                     fragmented_payload=[
                         memoryview(b'one'),
                         memoryview(b'two'),
                         memoryview(b'three')
                     ]),
            loop.time() - 0.1  # Expired on arrival
        ))

    assert sos.sample_statistics() == SessionStatistics(
        transfers=0,
        frames=0,
        payload_bytes=0,
        errors=0,
        drops=2  # Because multiframe
    )

    # Induced failure
    sos.socket.close()
    with raises(OSError):
        assert not run_until_complete(
            sos.send_until(
                Transfer(timestamp=ts,
                         priority=Priority.NOMINAL,
                         transfer_id=12340,
                         fragmented_payload=[
                             memoryview(b'one'),
                             memoryview(b'two'),
                             memoryview(b'three')
                         ]),
                loop.time() + 10.0))

    assert sos.sample_statistics() == SessionStatistics(transfers=0,
                                                        frames=0,
                                                        payload_bytes=0,
                                                        errors=1,
                                                        drops=2)

    assert not finalized
    sos.close()
    assert finalized
    sos.close()  # Idempotency

    with raises(pyuavcan.transport.ResourceClosedError):
        run_until_complete(
            sos.send_until(
                Transfer(timestamp=ts,
                         priority=Priority.NOMINAL,
                         transfer_id=12340,
                         fragmented_payload=[
                             memoryview(b'one'),
                             memoryview(b'two'),
                             memoryview(b'three')
                         ]),
                loop.time() + 10.0))

    sock_rx.close()
Exemple #3
0
async def _unittest_can_transport_anon() -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom
    from pyuavcan.transport import UnsupportedSessionConfigurationError, Priority, SessionStatistics, Timestamp
    from pyuavcan.transport import OperationNotDefinedForAnonymousNodeError
    from pyuavcan.transport import InputSessionSpecifier, OutputSessionSpecifier
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._identifier import MessageCANID
    # noinspection PyProtectedMember
    from pyuavcan.transport.can._frame import UAVCANFrame
    from .media.mock import MockMedia, FrameCollector

    with pytest.raises(pyuavcan.transport.InvalidTransportConfigurationError):
        can.CANTransport(MockMedia(set(), 64, 0), None)

    with pytest.raises(pyuavcan.transport.InvalidTransportConfigurationError):
        can.CANTransport(MockMedia(set(), 7, 16), None)

    peers: typing.Set[MockMedia] = set()
    media = MockMedia(peers, 64, 10)
    media2 = MockMedia(peers, 64, 3)
    peeper = MockMedia(peers, 64, 10)
    assert len(peers) == 3

    tr = can.CANTransport(media, None)
    tr2 = can.CANTransport(media2, None)

    assert tr.protocol_parameters == pyuavcan.transport.ProtocolParameters(
        transfer_id_modulo=32, max_nodes=128, mtu=63)
    assert tr.local_node_id is None
    assert tr.protocol_parameters == tr2.protocol_parameters

    assert not media.automatic_retransmission_enabled
    assert not media2.automatic_retransmission_enabled

    assert tr.descriptor == f'<can><mock mtu="64">mock@{id(peers):08x}</mock></can>'

    #
    # Instantiate session objects
    #
    meta = PayloadMetadata(0x_bad_c0ffee_0dd_f00d, 10000)

    with pytest.raises(Exception):  # Can't broadcast service calls
        tr.get_output_session(
            OutputSessionSpecifier(
                ServiceDataSpecifier(123, ServiceDataSpecifier.Role.RESPONSE),
                None), meta)

    with pytest.raises(
            UnsupportedSessionConfigurationError):  # Can't unicast messages
        tr.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(1234), 123), meta)

    broadcaster = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    subscriber_promiscuous = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)
    assert subscriber_promiscuous is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), None), meta)

    subscriber_selective = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta)
    assert subscriber_selective is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2222), 123), meta)

    server_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)
    assert server_listener is tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            None), meta)

    server_responder = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)
    assert server_responder is tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    client_requester = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 123),
        meta)
    assert client_requester is tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST), 123),
        meta)

    client_listener = tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)
    assert client_listener is tr.get_input_session(
        InputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            123), meta)

    assert broadcaster.destination_node_id is None
    assert subscriber_promiscuous.source_node_id is None
    assert subscriber_selective.source_node_id == 123
    assert server_listener.source_node_id is None
    assert client_listener.source_node_id == 123

    base_ts = time.process_time()
    inputs = tr.input_sessions
    print(
        f'INPUTS (sampled in {time.process_time() - base_ts:.3f}s): {inputs}')
    assert set(inputs) == {
        subscriber_promiscuous, subscriber_selective, server_listener,
        client_listener
    }
    del inputs

    print('OUTPUTS:', tr.output_sessions)
    assert set(tr.output_sessions) == {
        broadcaster, server_responder, client_requester
    }

    #
    # Basic exchange test, no one is listening
    #
    media2.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])
    peeper.configure_acceptance_filters(
        [can.media.FilterConfiguration.new_promiscuous()])

    collector = FrameCollector()
    peeper.start(collector.give, False)

    assert tr.sample_statistics() == can.CANTransportStatistics()
    assert tr2.sample_statistics() == can.CANTransportStatistics()

    ts = Timestamp.now()

    def validate_timestamp(timestamp: Timestamp) -> None:
        assert ts.monotonic_ns <= timestamp.monotonic_ns <= time.monotonic_ns()
        assert ts.system_ns <= timestamp.system_ns <= time.time_ns()

    assert await broadcaster.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem('abc'), _mem('def')]),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=1)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=1, in_frames_uavcan=1)
    assert tr.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(1)
    assert tr2.sample_statistics(
    ).media_acceptance_filtering_efficiency == pytest.approx(0)
    assert tr.sample_statistics().lost_loopback_frames == 0
    assert tr2.sample_statistics().lost_loopback_frames == 0

    assert collector.pop().is_same_manifestation(
        UAVCANFrame(
            identifier=MessageCANID(Priority.IMMEDIATE, None, 12345).compile(
                [_mem('abcdef')]),  # payload fragments joined
            padded_payload=_mem('abcdef'),
            transfer_id=11,
            start_of_transfer=True,
            end_of_transfer=True,
            toggle_bit=True,
            loopback=False).compile())
    assert collector.empty

    # Can't send anonymous service transfers
    with pytest.raises(OperationNotDefinedForAnonymousNodeError):
        assert await client_requester.send_until(
            Transfer(timestamp=ts,
                     priority=Priority.IMMEDIATE,
                     transfer_id=0,
                     fragmented_payload=[]),
            tr.loop.time() + 1.0,
        )
    assert client_requester.sample_statistics() == SessionStatistics(
    )  # Not incremented!

    # Can't send multiframe anonymous messages
    with pytest.raises(OperationNotDefinedForAnonymousNodeError):
        assert await broadcaster.send_until(
            Transfer(
                timestamp=ts,
                priority=Priority.SLOW,
                transfer_id=2,
                fragmented_payload=[_mem('qwe'), _mem('rty')] *
                50  # Lots of data here, very multiframe
            ),
            tr.loop.time() + 1.0)

    #
    # Broadcast exchange with input dispatch test
    #
    selective_m12345_5 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 5), meta)
    selective_m12345_9 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), 9), meta)
    promiscuous_m12345 = tr2.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(12345), None), meta)

    assert await broadcaster.send_until(
        Transfer(
            timestamp=ts,
            priority=Priority.IMMEDIATE,
            transfer_id=32 + 11,  # Modulus 11
            fragmented_payload=[_mem('abc'), _mem('def')]),
        tr.loop.time() + 1.0)
    assert broadcaster.sample_statistics() == SessionStatistics(
        transfers=2, frames=2, payload_bytes=12)

    assert tr.sample_statistics() == can.CANTransportStatistics(out_frames=2)
    assert tr2.sample_statistics() == can.CANTransportStatistics(
        in_frames=2, in_frames_uavcan=2, in_frames_uavcan_accepted=1)

    received = await promiscuous_m12345.receive_until(tr.loop.time() + 1.0)
    assert received is not None
    assert isinstance(received, TransferFrom)
    assert received.transfer_id == 11
    assert received.source_node_id is None  # The sender is anonymous
    assert received.priority == Priority.IMMEDIATE
    validate_timestamp(received.timestamp)
    assert received.fragmented_payload == [_mem('abcdef')]

    assert selective_m12345_5.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert selective_m12345_9.sample_statistics() == SessionStatistics(
    )  # Nothing
    assert promiscuous_m12345.sample_statistics() == SessionStatistics(
        transfers=1, frames=1, payload_bytes=6)

    assert not media.automatic_retransmission_enabled
    assert not media2.automatic_retransmission_enabled

    #
    # Finalization.
    #
    print('str(CANTransport):', tr)
    print('str(CANTransport):', tr2)
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
    # Double-close has no effect:
    client_listener.close()
    server_listener.close()
    subscriber_promiscuous.close()
    subscriber_selective.close()
    tr.close()
    tr2.close()
async def _unittest_output_session() -> None:
    ts = Timestamp.now()
    loop = asyncio.get_event_loop()

    tx_timestamp: typing.Optional[Timestamp] = Timestamp.now()
    tx_exception: typing.Optional[Exception] = None
    last_sent_frames: typing.List[SerialFrame] = []
    last_monotonic_deadline = 0.0
    finalized = False

    async def do_send(frames: typing.Sequence[SerialFrame],
                      monotonic_deadline: float) -> typing.Optional[Timestamp]:
        nonlocal last_sent_frames
        nonlocal last_monotonic_deadline
        last_sent_frames = list(frames)
        last_monotonic_deadline = monotonic_deadline
        if tx_exception:
            raise tx_exception
        return tx_timestamp

    def do_finalize() -> None:
        nonlocal finalized
        finalized = True

    with raises(pyuavcan.transport.OperationNotDefinedForAnonymousNodeError):
        SerialOutputSession(
            specifier=OutputSessionSpecifier(
                ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
                1111),
            payload_metadata=PayloadMetadata(1024),
            mtu=10,
            local_node_id=None,
            send_handler=do_send,
            finalizer=do_finalize,
        )

    sos = SerialOutputSession(
        specifier=OutputSessionSpecifier(MessageDataSpecifier(3210), None),
        payload_metadata=PayloadMetadata(1024),
        mtu=11,
        local_node_id=None,
        send_handler=do_send,
        finalizer=do_finalize,
    )

    assert sos.specifier == OutputSessionSpecifier(MessageDataSpecifier(3210),
                                                   None)
    assert sos.destination_node_id is None
    assert sos.payload_metadata == PayloadMetadata(1024)
    assert sos.sample_statistics() == SessionStatistics()

    assert await (sos.send(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=12340,
            fragmented_payload=[
                memoryview(b"one"),
                memoryview(b"two"),
                memoryview(b"three")
            ],
        ),
        999999999.999,
    ))
    assert last_monotonic_deadline == approx(999999999.999)
    assert len(last_sent_frames) == 1

    with raises(pyuavcan.transport.OperationNotDefinedForAnonymousNodeError):
        await (sos.send(
            Transfer(
                timestamp=ts,
                priority=Priority.NOMINAL,
                transfer_id=12340,
                fragmented_payload=[
                    memoryview(b"one"),
                    memoryview(b"two"),
                    memoryview(b"three four five")
                ],
            ),
            loop.time() + 10.0,
        ))

    last_feedback: typing.Optional[Feedback] = None

    def feedback_handler(feedback: Feedback) -> None:
        nonlocal last_feedback
        last_feedback = feedback

    sos.enable_feedback(feedback_handler)

    assert last_feedback is None
    assert await (sos.send(
        Transfer(timestamp=ts,
                 priority=Priority.NOMINAL,
                 transfer_id=12340,
                 fragmented_payload=[]), 999999999.999))
    assert last_monotonic_deadline == approx(999999999.999)
    assert len(last_sent_frames) == 1
    assert last_feedback is not None
    assert last_feedback.original_transfer_timestamp == ts
    assert last_feedback.first_frame_transmission_timestamp == tx_timestamp

    sos.disable_feedback()
    sos.disable_feedback()  # Idempotency check

    assert sos.sample_statistics() == SessionStatistics(transfers=2,
                                                        frames=2,
                                                        payload_bytes=11,
                                                        errors=0,
                                                        drops=0)

    assert not finalized
    sos.close()
    assert finalized
    finalized = False

    sos = SerialOutputSession(
        specifier=OutputSessionSpecifier(
            ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
            2222),
        payload_metadata=PayloadMetadata(1024),
        mtu=10,
        local_node_id=1234,
        send_handler=do_send,
        finalizer=do_finalize,
    )

    # Induced failure
    tx_timestamp = None
    assert not await (sos.send(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=12340,
            fragmented_payload=[
                memoryview(b"one"),
                memoryview(b"two"),
                memoryview(b"three")
            ],
        ),
        999999999.999,
    ))
    assert last_monotonic_deadline == approx(999999999.999)
    assert len(last_sent_frames) == 2

    assert sos.sample_statistics() == SessionStatistics(transfers=0,
                                                        frames=0,
                                                        payload_bytes=0,
                                                        errors=0,
                                                        drops=2)

    tx_exception = RuntimeError()
    with raises(RuntimeError):
        _ = await (sos.send(
            Transfer(
                timestamp=ts,
                priority=Priority.NOMINAL,
                transfer_id=12340,
                fragmented_payload=[
                    memoryview(b"one"),
                    memoryview(b"two"),
                    memoryview(b"three")
                ],
            ),
            loop.time() + 10.0,
        ))

    assert sos.sample_statistics() == SessionStatistics(transfers=0,
                                                        frames=0,
                                                        payload_bytes=0,
                                                        errors=1,
                                                        drops=2)

    assert not finalized
    sos.close()
    assert finalized
    sos.close()  # Idempotency

    with raises(pyuavcan.transport.ResourceClosedError):
        await (sos.send(
            Transfer(
                timestamp=ts,
                priority=Priority.NOMINAL,
                transfer_id=12340,
                fragmented_payload=[
                    memoryview(b"one"),
                    memoryview(b"two"),
                    memoryview(b"three")
                ],
            ),
            loop.time() + 10.0,
        ))
async def _unittest_output_session() -> None:
    ts = Timestamp.now()
    loop = asyncio.get_event_loop()
    loop.slow_callback_duration = 5.0  # TODO use asyncio socket read and remove this thing.
    finalized = False

    def do_finalize() -> None:
        nonlocal finalized
        finalized = True

    def check_timestamp(t: Timestamp) -> bool:
        now = Timestamp.now()
        s = ts.system_ns <= t.system_ns <= now.system_ns
        m = ts.monotonic_ns <= t.monotonic_ns <= now.system_ns
        return s and m

    destination_endpoint = "127.100.0.1", 25406

    sock_rx = socket_.socket(socket_.AF_INET, socket_.SOCK_DGRAM)
    sock_rx.bind(destination_endpoint)
    sock_rx.settimeout(1.0)

    def make_sock() -> socket_.socket:
        sock = socket_.socket(socket_.AF_INET, socket_.SOCK_DGRAM)
        sock.bind(("127.100.0.2", 0))
        sock.connect(destination_endpoint)
        sock.setblocking(False)
        return sock

    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(MessageDataSpecifier(3210), None),
        payload_metadata=PayloadMetadata(1024),
        mtu=11,
        multiplier=1,
        sock=make_sock(),
        finalizer=do_finalize,
    )

    assert sos.specifier == OutputSessionSpecifier(MessageDataSpecifier(3210),
                                                   None)
    assert sos.destination_node_id is None
    assert sos.payload_metadata == PayloadMetadata(1024)
    assert sos.sample_statistics() == SessionStatistics()

    assert await (sos.send(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=12340,
            fragmented_payload=[
                memoryview(b"one"),
                memoryview(b"two"),
                memoryview(b"three")
            ],
        ),
        loop.time() + 10.0,
    ))

    rx_data, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    assert rx_data == (
        b"\x00\x04\x00\x00\x00\x00\x00\x8040\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        + b"one" + b"two" + b"three")
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    last_feedback: typing.Optional[Feedback] = None

    def feedback_handler(feedback: Feedback) -> None:
        nonlocal last_feedback
        last_feedback = feedback

    sos.enable_feedback(feedback_handler)

    assert last_feedback is None
    assert await (sos.send(
        Transfer(timestamp=ts,
                 priority=Priority.NOMINAL,
                 transfer_id=12340,
                 fragmented_payload=[]),
        loop.time() + 10.0,
    ))
    assert last_feedback is not None
    assert last_feedback.original_transfer_timestamp == ts
    assert check_timestamp(last_feedback.first_frame_transmission_timestamp)

    sos.disable_feedback()
    sos.disable_feedback()  # Idempotency check

    _, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    assert sos.sample_statistics() == SessionStatistics(transfers=2,
                                                        frames=2,
                                                        payload_bytes=11,
                                                        errors=0,
                                                        drops=0)

    assert sos.socket.fileno() >= 0
    assert not finalized
    sos.close()
    assert finalized
    assert sos.socket.fileno() < 0  # The socket is supposed to be disposed of.
    finalized = False

    # Multi-frame with multiplication
    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(
            ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
            2222),
        payload_metadata=PayloadMetadata(1024),
        mtu=10,
        multiplier=2,
        sock=make_sock(),
        finalizer=do_finalize,
    )
    assert await (sos.send(
        Transfer(
            timestamp=ts,
            priority=Priority.OPTIONAL,
            transfer_id=54321,
            fragmented_payload=[
                memoryview(b"one"),
                memoryview(b"two"),
                memoryview(b"three")
            ],
        ),
        loop.time() + 10.0,
    ))
    data_main_a, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    data_main_b, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    data_redundant_a, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    data_redundant_b, endpoint = sock_rx.recvfrom(1000)
    assert endpoint[0] == "127.100.0.2"
    with raises(socket_.timeout):
        sock_rx.recvfrom(1000)

    print("data_main_a", data_main_a)
    print("data_main_b", data_main_b)
    print("data_redundant_a", data_redundant_a)
    print("data_redundant_b", data_redundant_b)

    assert data_main_a == data_redundant_a
    assert data_main_b == data_redundant_b
    assert data_main_a == (
        b"\x00\x07\x00\x00\x00\x00\x00\x001\xd4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        + b"one" + b"two" + b"three"[:-1])
    assert data_main_b == (
        b"\x00\x07\x00\x00\x01\x00\x00\x801\xd4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        + b"e" + pyuavcan.transport.commons.crc.CRC32C.new(
            b"one", b"two", b"three").value_as_bytes)

    sos.socket.close()  # This is to prevent resource warning
    sos = UDPOutputSession(
        specifier=OutputSessionSpecifier(
            ServiceDataSpecifier(321, ServiceDataSpecifier.Role.REQUEST),
            2222),
        payload_metadata=PayloadMetadata(1024),
        mtu=10,
        multiplier=1,
        sock=make_sock(),
        finalizer=do_finalize,
    )

    # Induced timeout
    assert not await (sos.send(
        Transfer(
            timestamp=ts,
            priority=Priority.NOMINAL,
            transfer_id=12340,
            fragmented_payload=[
                memoryview(b"one"),
                memoryview(b"two"),
                memoryview(b"three")
            ],
        ),
        loop.time() - 0.1,  # Expired on arrival
    ))

    assert sos.sample_statistics() == SessionStatistics(
        transfers=0,
        frames=0,
        payload_bytes=0,
        errors=0,
        drops=2  # Because multiframe
    )

    # Induced failure
    sos.socket.close()
    with raises(OSError):
        assert not await (sos.send(
            Transfer(
                timestamp=ts,
                priority=Priority.NOMINAL,
                transfer_id=12340,
                fragmented_payload=[
                    memoryview(b"one"),
                    memoryview(b"two"),
                    memoryview(b"three")
                ],
            ),
            loop.time() + 10.0,
        ))

    assert sos.sample_statistics() == SessionStatistics(transfers=0,
                                                        frames=0,
                                                        payload_bytes=0,
                                                        errors=1,
                                                        drops=2)

    assert not finalized
    sos.close()
    assert finalized
    sos.close()  # Idempotency

    with raises(pyuavcan.transport.ResourceClosedError):
        await (sos.send(
            Transfer(
                timestamp=ts,
                priority=Priority.NOMINAL,
                transfer_id=12340,
                fragmented_payload=[
                    memoryview(b"one"),
                    memoryview(b"two"),
                    memoryview(b"three")
                ],
            ),
            loop.time() + 10.0,
        ))

    sock_rx.close()
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