Esempio n. 1
0
async def _unittest_serial_transport_capture(caplog: typing.Any) -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer
    from pyuavcan.transport import Priority, Timestamp, OutputSessionSpecifier

    get_monotonic = asyncio.get_event_loop().time

    tr = SerialTransport(serial_port="loop://",
                         local_node_id=42,
                         mtu=1024,
                         service_transfer_multiplier=2)
    sft_capacity = 1024
    payload_single = [_mem("qwertyui"), _mem("01234567")
                      ] * (sft_capacity // 16)
    assert sum(map(len, payload_single)) == sft_capacity
    payload_x3 = (payload_single * 3)[:-1]
    payload_x3_size_bytes = sft_capacity * 3 - 8
    assert sum(map(len, payload_x3)) == payload_x3_size_bytes

    broadcaster = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None),
        PayloadMetadata(10000))
    client_requester = tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
            3210),
        PayloadMetadata(10000),
    )

    events: typing.List[SerialCapture] = []
    events2: typing.List[pyuavcan.transport.Capture] = []

    def append_events(cap: pyuavcan.transport.Capture) -> None:
        assert isinstance(cap, SerialCapture)
        events.append(cap)

    tr.begin_capture(append_events)
    tr.begin_capture(events2.append)
    assert events == []
    assert events2 == []

    #
    # Multi-frame message.
    #
    ts = Timestamp.now()
    assert await broadcaster.send(
        Transfer(timestamp=ts,
                 priority=Priority.LOW,
                 transfer_id=777,
                 fragmented_payload=payload_x3),
        monotonic_deadline=get_monotonic() + 5.0,
    )
    await asyncio.sleep(0.1)
    assert events == events2
    # Send three, receive three.
    # Sorting is required because the ordering of the events in the middle is not defined: arrival events
    # may or may not be registered before the emission event depending on how the serial loopback is operating.
    a, b, c, d, e, f = sorted(events, key=lambda x: not x.own)
    assert isinstance(a, SerialCapture) and a.own
    assert isinstance(b, SerialCapture) and b.own
    assert isinstance(c, SerialCapture) and c.own
    assert isinstance(d, SerialCapture) and not d.own
    assert isinstance(e, SerialCapture) and not e.own
    assert isinstance(f, SerialCapture) and not f.own

    def parse(x: SerialCapture) -> SerialFrame:
        out = SerialFrame.parse_from_cobs_image(x.fragment)
        assert out is not None
        return out

    assert parse(a).transfer_id == 777
    assert parse(b).transfer_id == 777
    assert parse(c).transfer_id == 777
    assert a.timestamp.monotonic >= ts.monotonic
    assert b.timestamp.monotonic >= ts.monotonic
    assert c.timestamp.monotonic >= ts.monotonic
    assert parse(a).index == 0
    assert parse(b).index == 1
    assert parse(c).index == 2
    assert not parse(a).end_of_transfer
    assert not parse(b).end_of_transfer
    assert parse(c).end_of_transfer

    assert a.fragment.tobytes().strip(b"\x00") == d.fragment.tobytes().strip(
        b"\x00")
    assert b.fragment.tobytes().strip(b"\x00") == e.fragment.tobytes().strip(
        b"\x00")
    assert c.fragment.tobytes().strip(b"\x00") == f.fragment.tobytes().strip(
        b"\x00")

    events.clear()
    events2.clear()

    #
    # Single-frame service request with dual frame duplication.
    #
    ts = Timestamp.now()
    assert await client_requester.send(
        Transfer(timestamp=ts,
                 priority=Priority.HIGH,
                 transfer_id=888,
                 fragmented_payload=payload_single),
        monotonic_deadline=get_monotonic() + 5.0,
    )
    await asyncio.sleep(0.1)
    assert events == events2
    # Send two, receive two.
    # Sorting is required because the order of the two events in the middle is not defined: the arrival event
    # may or may not be registered before the emission event depending on how the serial loopback is operating.
    a, b, c, d = sorted(events, key=lambda x: not x.own)
    assert isinstance(a, SerialCapture) and a.own
    assert isinstance(b, SerialCapture) and b.own
    assert isinstance(c, SerialCapture) and not c.own
    assert isinstance(d, SerialCapture) and not d.own

    assert parse(a).transfer_id == 888
    assert parse(b).transfer_id == 888
    assert a.timestamp.monotonic >= ts.monotonic
    assert b.timestamp.monotonic >= ts.monotonic
    assert parse(a).index == 0
    assert parse(b).index == 0
    assert parse(a).end_of_transfer
    assert parse(b).end_of_transfer

    assert a.fragment.tobytes().strip(b"\x00") == c.fragment.tobytes().strip(
        b"\x00")
    assert b.fragment.tobytes().strip(b"\x00") == d.fragment.tobytes().strip(
        b"\x00")

    events.clear()
    events2.clear()

    #
    # Out-of-band data.
    #
    grownups = b"Aren't there any grownups at all? - No grownups!\x00"
    with caplog.at_level(logging.CRITICAL,
                         logger=pyuavcan.transport.serial.__name__):
        # The frame delimiter is needed to force new frame into the state machine.
        tr.serial_port.write(grownups)
        await asyncio.sleep(1)
    assert events == events2
    (oob, ) = events
    assert isinstance(oob, SerialCapture)
    assert not oob.own
    assert bytes(oob.fragment) == grownups

    events.clear()
    events2.clear()
Esempio n. 2
0
async def _unittest_redundant_transport(caplog: typing.Any) -> None:
    from pyuavcan.transport import MessageDataSpecifier, PayloadMetadata, Transfer
    from pyuavcan.transport import Priority, Timestamp, InputSessionSpecifier, OutputSessionSpecifier
    from pyuavcan.transport import ProtocolParameters

    loop = asyncio.get_event_loop()
    loop.slow_callback_duration = 1.0

    tr_a = RedundantTransport()
    tr_b = RedundantTransport(loop)
    assert tr_a.sample_statistics() == RedundantTransportStatistics([])
    assert tr_a.inferiors == []
    assert tr_a.local_node_id is None
    assert tr_a.loop is asyncio.get_event_loop()
    assert tr_a.local_node_id is None
    assert tr_a.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=0,
        max_nodes=0,
        mtu=0,
    )
    assert tr_a.descriptor == '<redundant></redundant>'  # Empty, no inferiors.
    assert tr_a.input_sessions == []
    assert tr_a.output_sessions == []

    assert tr_a.loop == tr_b.loop

    #
    # Instantiate session objects.
    #
    meta = PayloadMetadata(10_240)

    pub_a = tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_any_a = tr_a.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert pub_a is tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert set(tr_a.input_sessions) == {sub_any_a}
    assert set(tr_a.output_sessions) == {pub_a}
    assert tr_a.sample_statistics() == RedundantTransportStatistics()

    pub_b = tr_b.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_any_b = tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    sub_sel_b = tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta)
    assert sub_sel_b is tr_b.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta)
    assert set(tr_b.input_sessions) == {sub_any_b, sub_sel_b}
    assert set(tr_b.output_sessions) == {pub_b}
    assert tr_b.sample_statistics() == RedundantTransportStatistics()

    #
    # Exchange test with no inferiors, expected to fail.
    #
    assert len(pub_a.inferiors) == 0
    assert len(sub_any_a.inferiors) == 0
    assert not await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=1,
        fragmented_payload=[memoryview(b'abc')]),
                                      monotonic_deadline=loop.time() + 1.0)
    assert not await sub_any_a.receive_until(loop.time() + 0.1)
    assert not await sub_any_b.receive_until(loop.time() + 0.1)
    assert tr_a.sample_statistics() == RedundantTransportStatistics()
    assert tr_b.sample_statistics() == RedundantTransportStatistics()

    #
    # Adding inferiors - loopback, transport A only.
    #
    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*loop.*'):
        tr_a.attach_inferior(
            LoopbackTransport(
                111, loop=asyncio.new_event_loop()))  # Wrong event loop.
    assert len(pub_a.inferiors) == 0
    assert len(sub_any_a.inferiors) == 0

    lo_mono_0 = LoopbackTransport(111)
    lo_mono_1 = LoopbackTransport(111)

    tr_a.attach_inferior(lo_mono_0)
    assert len(pub_a.inferiors) == 1
    assert len(sub_any_a.inferiors) == 1

    with pytest.raises(ValueError):
        tr_a.detach_inferior(lo_mono_1)  # Not a registered inferior (yet).

    tr_a.attach_inferior(lo_mono_1)
    assert len(pub_a.inferiors) == 2
    assert len(sub_any_a.inferiors) == 2

    with pytest.raises(ValueError):
        tr_a.attach_inferior(lo_mono_0)  # Double-add not allowed.

    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*node-id.*'):
        tr_a.attach_inferior(LoopbackTransport(None))  # Wrong node-ID.

    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*node-id.*'):
        tr_a.attach_inferior(LoopbackTransport(1230))  # Wrong node-ID.

    assert tr_a.inferiors == [lo_mono_0, lo_mono_1]
    assert len(pub_a.inferiors) == 2
    assert len(sub_any_a.inferiors) == 2

    assert tr_a.sample_statistics() == RedundantTransportStatistics(inferiors=[
        lo_mono_0.sample_statistics(),
        lo_mono_1.sample_statistics(),
    ])
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>'

    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=2,
        fragmented_payload=[memoryview(b'def')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_a.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'def')]
    assert rx.transfer_id == 2
    assert not await sub_any_b.receive_until(loop.time() + 0.1)

    #
    # Incapacitate one inferior, ensure things are still OK.
    #
    with caplog.at_level(logging.CRITICAL,
                         logger=pyuavcan.transport.redundant.__name__):
        for s in lo_mono_0.output_sessions:
            s.exception = RuntimeError('INTENDED EXCEPTION')

        assert await pub_a.send_until(Transfer(
            timestamp=Timestamp.now(),
            priority=Priority.LOW,
            transfer_id=3,
            fragmented_payload=[memoryview(b'qwe')]),
                                      monotonic_deadline=loop.time() + 1.0)
        rx = await sub_any_a.receive_until(loop.time() + 1.0)
        assert rx is not None
        assert rx.fragmented_payload == [memoryview(b'qwe')]
        assert rx.transfer_id == 3

    #
    # Remove old loopback transports. Configure new ones with cyclic TID.
    #
    lo_cyc_0 = LoopbackTransport(111)
    lo_cyc_1 = LoopbackTransport(111)
    cyc_proto_params = ProtocolParameters(
        transfer_id_modulo=32,  # Like CAN
        max_nodes=128,  # Like CAN
        mtu=63,  # Like CAN
    )
    lo_cyc_0.protocol_parameters = cyc_proto_params
    lo_cyc_1.protocol_parameters = cyc_proto_params
    assert lo_cyc_0.protocol_parameters == lo_cyc_1.protocol_parameters == cyc_proto_params

    assert tr_a.protocol_parameters.transfer_id_modulo >= 2**56
    with pytest.raises(InconsistentInferiorConfigurationError,
                       match='(?i).*transfer-id.*'):
        tr_a.attach_inferior(lo_cyc_0)  # Transfer-ID modulo mismatch

    tr_a.detach_inferior(lo_mono_0)
    tr_a.detach_inferior(lo_mono_1)
    del lo_mono_0  # Prevent accidental reuse.
    del lo_mono_1
    assert tr_a.inferiors == []  # All removed, okay.
    assert pub_a.inferiors == []
    assert sub_any_a.inferiors == []
    assert tr_a.local_node_id is None  # Back to the roots
    assert tr_a.descriptor == '<redundant></redundant>'  # Yes yes

    # Now we can add our cyclic transports safely.
    tr_a.attach_inferior(lo_cyc_0)
    assert tr_a.protocol_parameters.transfer_id_modulo == 32
    tr_a.attach_inferior(lo_cyc_1)
    assert tr_a.protocol_parameters == cyc_proto_params, 'Protocol parameter mismatch'
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == '<redundant><loopback/><loopback/></redundant>'

    # Exchange test.
    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=4,
        fragmented_payload=[memoryview(b'rty')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_a.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'rty')]
    assert rx.transfer_id == 4

    #
    # Real heterogeneous transport test.
    #
    tr_a.detach_inferior(lo_cyc_0)
    tr_a.detach_inferior(lo_cyc_1)
    del lo_cyc_0  # Prevent accidental reuse.
    del lo_cyc_1

    udp_a = UDPTransport('127.0.0.111/8')
    udp_b = UDPTransport('127.0.0.222/8')

    serial_a = SerialTransport(SERIAL_URI, 111)
    serial_b = SerialTransport(SERIAL_URI, 222, mtu=2048)  # Heterogeneous.

    tr_a.attach_inferior(udp_a)
    tr_a.attach_inferior(serial_a)

    tr_b.attach_inferior(udp_b)
    tr_b.attach_inferior(serial_b)

    print('tr_a.descriptor', tr_a.descriptor)
    print('tr_b.descriptor', tr_b.descriptor)

    assert tr_a.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=2**64,
        max_nodes=4096,
        mtu=1024,
    )
    assert tr_a.local_node_id == 111
    assert tr_a.descriptor == f'<redundant>{udp_a.descriptor}{serial_a.descriptor}</redundant>'

    assert tr_b.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=2**64,
        max_nodes=4096,
        mtu=1024,
    )
    assert tr_b.local_node_id == 222
    assert tr_b.descriptor == f'<redundant>{udp_b.descriptor}{serial_b.descriptor}</redundant>'

    assert await pub_a.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=5,
        fragmented_payload=[memoryview(b'uio')]),
                                  monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_b.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'uio')]
    assert rx.transfer_id == 5
    assert not await sub_any_a.receive_until(loop.time() + 0.1)
    assert not await sub_any_b.receive_until(loop.time() + 0.1)
    assert not await sub_sel_b.receive_until(loop.time() + 0.1)

    #
    # Construct new session with the transports configured.
    #
    pub_a_new = tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta)
    assert pub_a_new is tr_a.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), 222), meta)
    assert set(tr_a.output_sessions) == {pub_a, pub_a_new}

    assert await pub_a_new.send_until(Transfer(
        timestamp=Timestamp.now(),
        priority=Priority.LOW,
        transfer_id=6,
        fragmented_payload=[memoryview(b'asd')]),
                                      monotonic_deadline=loop.time() + 1.0)
    rx = await sub_any_b.receive_until(loop.time() + 1.0)
    assert rx is not None
    assert rx.fragmented_payload == [memoryview(b'asd')]
    assert rx.transfer_id == 6

    #
    # Termination.
    #
    tr_a.close()
    tr_a.close()  # Idempotency
    tr_b.close()
    tr_b.close()  # Idempotency

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the inferiors are closed.
        udp_a.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the inferiors are closed.
        serial_b.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    with pytest.raises(pyuavcan.transport.ResourceClosedError
                       ):  # Make sure the sessions are closed.
        await pub_a.send_until(Transfer(timestamp=Timestamp.now(),
                                        priority=Priority.LOW,
                                        transfer_id=100,
                                        fragmented_payload=[]),
                               monotonic_deadline=loop.time() + 1.0)

    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.
Esempio n. 3
0
async def _unittest_serial_transport(caplog: typing.Any) -> None:
    from pyuavcan.transport import MessageDataSpecifier, ServiceDataSpecifier, PayloadMetadata, Transfer, TransferFrom
    from pyuavcan.transport import Priority, Timestamp, InputSessionSpecifier, OutputSessionSpecifier
    from pyuavcan.transport import ProtocolParameters

    get_monotonic = asyncio.get_event_loop().time

    service_multiplication_factor = 2

    with pytest.raises(ValueError):
        _ = SerialTransport(serial_port="loop://", local_node_id=None, mtu=1)

    with pytest.raises(ValueError):
        _ = SerialTransport(serial_port="loop://",
                            local_node_id=None,
                            service_transfer_multiplier=10000)

    with pytest.raises(pyuavcan.transport.InvalidMediaConfigurationError):
        _ = SerialTransport(serial_port=serial.serial_for_url(
            "loop://", do_not_open=True),
                            local_node_id=None)

    tr = SerialTransport(serial_port="loop://", local_node_id=None, mtu=1024)

    assert tr.local_node_id is None
    assert tr.serial_port.is_open

    assert tr.input_sessions == []
    assert tr.output_sessions == []

    assert tr.protocol_parameters == ProtocolParameters(
        transfer_id_modulo=2**64,
        max_nodes=4096,
        mtu=1024,
    )

    assert tr.sample_statistics() == SerialTransportStatistics()

    sft_capacity = 1024

    payload_single = [_mem("qwertyui"), _mem("01234567")
                      ] * (sft_capacity // 16)
    assert sum(map(len, payload_single)) == sft_capacity

    payload_x3 = (payload_single * 3)[:-1]
    payload_x3_size_bytes = sft_capacity * 3 - 8
    assert sum(map(len, payload_x3)) == payload_x3_size_bytes

    #
    # Instantiate session objects.
    #
    meta = PayloadMetadata(10000)

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

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

    subscriber_selective = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), meta)
    assert subscriber_selective is tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), 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)

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

    print("INPUTS:", tr.input_sessions)
    print("OUTPUTS:", tr.output_sessions)
    assert set(tr.input_sessions) == {
        subscriber_promiscuous, subscriber_selective, server_listener,
        client_listener
    }
    assert set(tr.output_sessions) == {broadcaster}
    assert tr.sample_statistics() == SerialTransportStatistics()

    #
    # Message exchange test.
    #
    assert await broadcaster.send(
        Transfer(timestamp=Timestamp.now(),
                 priority=Priority.LOW,
                 transfer_id=77777,
                 fragmented_payload=payload_single),
        monotonic_deadline=get_monotonic() + 5.0,
    )

    rx_transfer = await subscriber_promiscuous.receive(get_monotonic() + 5.0)
    print("PROMISCUOUS SUBSCRIBER TRANSFER:", rx_transfer)
    assert isinstance(rx_transfer, TransferFrom)
    assert rx_transfer.priority == Priority.LOW
    assert rx_transfer.transfer_id == 77777
    assert rx_transfer.fragmented_payload == [b"".join(payload_single)]

    print(tr.sample_statistics())
    assert tr.sample_statistics().in_bytes >= 32 + sft_capacity + 2
    assert tr.sample_statistics().in_frames == 1
    assert tr.sample_statistics().in_out_of_band_bytes == 0
    assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes
    assert tr.sample_statistics().out_frames == 1
    assert tr.sample_statistics().out_transfers == 1
    assert tr.sample_statistics().out_incomplete == 0

    with pytest.raises(
            pyuavcan.transport.OperationNotDefinedForAnonymousNodeError):
        # Anonymous nodes can't send multiframe transfers.
        assert await broadcaster.send(
            Transfer(timestamp=Timestamp.now(),
                     priority=Priority.LOW,
                     transfer_id=77777,
                     fragmented_payload=payload_x3),
            monotonic_deadline=get_monotonic() + 5.0,
        )

    assert None is await subscriber_selective.receive(get_monotonic() + 0.1)
    assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1)
    assert None is await server_listener.receive(get_monotonic() + 0.1)
    assert None is await client_listener.receive(get_monotonic() + 0.1)

    #
    # Service exchange test.
    #
    with pytest.raises(
            pyuavcan.transport.OperationNotDefinedForAnonymousNodeError):
        # Anonymous nodes can't emit service transfers.
        tr.get_output_session(
            OutputSessionSpecifier(
                ServiceDataSpecifier(333, ServiceDataSpecifier.Role.REQUEST),
                3210), meta)

    #
    # Replace the transport with a different one where the local node-ID is not None.
    #
    tr = SerialTransport(serial_port="loop://", local_node_id=3210, mtu=1024)
    assert tr.local_node_id == 3210

    #
    # Re-instantiate session objects because the transport instances have been replaced.
    #
    broadcaster = tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)
    assert broadcaster is tr.get_output_session(
        OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    subscriber_promiscuous = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    subscriber_selective = tr.get_input_session(
        InputSessionSpecifier(MessageDataSpecifier(2345), 3210), 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),
            3210), meta)
    assert server_responder is tr.get_output_session(
        OutputSessionSpecifier(
            ServiceDataSpecifier(333, ServiceDataSpecifier.Role.RESPONSE),
            3210), meta)

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

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

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

    assert await client_requester.send(
        Transfer(timestamp=Timestamp.now(),
                 priority=Priority.HIGH,
                 transfer_id=88888,
                 fragmented_payload=payload_x3),
        monotonic_deadline=get_monotonic() + 5.0,
    )

    rx_transfer = await server_listener.receive(get_monotonic() + 5.0)
    print("SERVER LISTENER TRANSFER:", rx_transfer)
    assert isinstance(rx_transfer, TransferFrom)
    assert rx_transfer.priority == Priority.HIGH
    assert rx_transfer.transfer_id == 88888
    assert len(rx_transfer.fragmented_payload) == 3
    assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3)

    assert None is await subscriber_selective.receive(get_monotonic() + 0.1)
    assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1)
    assert None is await server_listener.receive(get_monotonic() + 0.1)
    assert None is await client_listener.receive(get_monotonic() + 0.1)

    print(tr.sample_statistics())
    assert tr.sample_statistics().in_bytes >= (
        32 * 3 + payload_x3_size_bytes + 2) * service_multiplication_factor
    assert tr.sample_statistics(
    ).in_frames == 3 * service_multiplication_factor
    assert tr.sample_statistics().in_out_of_band_bytes == 0
    assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes
    assert tr.sample_statistics(
    ).out_frames == 3 * service_multiplication_factor
    assert tr.sample_statistics(
    ).out_transfers == 1 * service_multiplication_factor
    assert tr.sample_statistics().out_incomplete == 0

    #
    # Write timeout test.
    #
    assert not await broadcaster.send(
        Transfer(timestamp=Timestamp.now(),
                 priority=Priority.IMMEDIATE,
                 transfer_id=99999,
                 fragmented_payload=payload_x3),
        monotonic_deadline=get_monotonic() -
        5.0,  # The deadline is in the past.
    )

    assert None is await subscriber_selective.receive(get_monotonic() + 0.1)
    assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1)
    assert None is await server_listener.receive(get_monotonic() + 0.1)
    assert None is await client_listener.receive(get_monotonic() + 0.1)

    print(tr.sample_statistics())
    assert tr.sample_statistics().in_bytes >= (
        32 * 3 + payload_x3_size_bytes + 2) * service_multiplication_factor
    assert tr.sample_statistics(
    ).in_frames == 3 * service_multiplication_factor
    assert tr.sample_statistics().in_out_of_band_bytes == 0
    assert tr.sample_statistics().out_bytes == tr.sample_statistics().in_bytes
    assert tr.sample_statistics(
    ).out_frames == 3 * service_multiplication_factor
    assert tr.sample_statistics(
    ).out_transfers == 1 * service_multiplication_factor
    assert tr.sample_statistics().out_incomplete == 1  # INCREMENTED HERE

    #
    # Selective message exchange test.
    #
    assert await broadcaster.send(
        Transfer(timestamp=Timestamp.now(),
                 priority=Priority.IMMEDIATE,
                 transfer_id=99999,
                 fragmented_payload=payload_x3),
        monotonic_deadline=get_monotonic() + 5.0,
    )

    rx_transfer = await subscriber_promiscuous.receive(get_monotonic() + 5.0)
    print("PROMISCUOUS SUBSCRIBER TRANSFER:", rx_transfer)
    assert isinstance(rx_transfer, TransferFrom)
    assert rx_transfer.priority == Priority.IMMEDIATE
    assert rx_transfer.transfer_id == 99999
    assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3)

    rx_transfer = await subscriber_selective.receive(get_monotonic() + 1.0)
    print("SELECTIVE SUBSCRIBER TRANSFER:", rx_transfer)
    assert isinstance(rx_transfer, TransferFrom)
    assert rx_transfer.priority == Priority.IMMEDIATE
    assert rx_transfer.transfer_id == 99999
    assert b"".join(rx_transfer.fragmented_payload) == b"".join(payload_x3)

    assert None is await subscriber_selective.receive(get_monotonic() + 0.1)
    assert None is await subscriber_promiscuous.receive(get_monotonic() + 0.1)
    assert None is await server_listener.receive(get_monotonic() + 0.1)
    assert None is await client_listener.receive(get_monotonic() + 0.1)

    #
    # Out-of-band data test.
    #
    with caplog.at_level(logging.CRITICAL,
                         logger=pyuavcan.transport.serial.__name__):
        stats_reference = tr.sample_statistics()

        # The frame delimiter is needed to force new frame into the state machine.
        grownups = b"Aren't there any grownups at all? - No grownups!\x00"
        tr.serial_port.write(grownups)
        stats_reference.in_bytes += len(grownups)
        stats_reference.in_out_of_band_bytes += len(grownups)

        # Wait for the reader thread to catch up.
        assert None is await subscriber_selective.receive(get_monotonic() +
                                                          0.2)
        assert None is await subscriber_promiscuous.receive(get_monotonic() +
                                                            0.2)
        assert None is await server_listener.receive(get_monotonic() + 0.2)
        assert None is await client_listener.receive(get_monotonic() + 0.2)

        print(tr.sample_statistics())
        assert tr.sample_statistics() == stats_reference

        # The frame delimiter is needed to force new frame into the state machine.
        tr.serial_port.write(
            bytes([0xFF, 0xFF, SerialFrame.FRAME_DELIMITER_BYTE]))
        stats_reference.in_bytes += 3
        stats_reference.in_out_of_band_bytes += 3

        # Wait for the reader thread to catch up.
        assert None is await subscriber_selective.receive(get_monotonic() +
                                                          0.2)
        assert None is await subscriber_promiscuous.receive(get_monotonic() +
                                                            0.2)
        assert None is await server_listener.receive(get_monotonic() + 0.2)
        assert None is await client_listener.receive(get_monotonic() + 0.2)

        print(tr.sample_statistics())
        assert tr.sample_statistics() == stats_reference

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

    subscriber_promiscuous.close()
    subscriber_promiscuous.close()  # Idempotency.

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

    broadcaster.close()
    broadcaster.close()  # Idempotency.

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

    tr.close()
    tr.close()  # Idempotency.

    assert not set(tr.input_sessions)
    assert not set(tr.output_sessions)

    with pytest.raises(pyuavcan.transport.ResourceClosedError):
        _ = tr.get_output_session(
            OutputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    with pytest.raises(pyuavcan.transport.ResourceClosedError):
        _ = tr.get_input_session(
            InputSessionSpecifier(MessageDataSpecifier(2345), None), meta)

    await asyncio.sleep(
        1
    )  # Let all pending tasks finalize properly to avoid stack traces in the output.