Esempio n. 1
0
async def _unittest_redundant_input_monotonic() -> None:
    asyncio.get_running_loop().slow_callback_duration = 5.0

    spec = pyuavcan.transport.InputSessionSpecifier(pyuavcan.transport.MessageDataSpecifier(4321), None)
    spec_tx = pyuavcan.transport.OutputSessionSpecifier(spec.data_specifier, None)
    meta = pyuavcan.transport.PayloadMetadata(30)

    ts = Timestamp.now()

    tr_a = LoopbackTransport(111)
    tr_b = LoopbackTransport(111)
    tx_a = tr_a.get_output_session(spec_tx, meta)
    tx_b = tr_b.get_output_session(spec_tx, meta)
    inf_a = tr_a.get_input_session(spec, meta)
    inf_b = tr_b.get_input_session(spec, meta)

    inf_a.transfer_id_timeout = 1.1  # This is used to ensure that the transfer-ID timeout is handled correctly.

    ses = RedundantInputSession(
        spec,
        meta,
        tid_modulo_provider=lambda: 2 ** 56,  # Like UDP or serial - infinite modulo.
        finalizer=lambda: None,
    )
    assert ses.specifier is spec
    assert ses.payload_metadata is meta
    assert not ses.inferiors
    assert ses.sample_statistics() == RedundantSessionStatistics()
    assert pytest.approx(0.0) == ses.transfer_id_timeout

    # Add inferiors.
    ses._add_inferior(inf_a)  # No change, added above    # pylint: disable=protected-access
    assert ses.inferiors == [inf_a]
    ses._add_inferior(inf_b)  # pylint: disable=protected-access
    assert ses.inferiors == [inf_a, inf_b]

    ses.transfer_id_timeout = 1.1
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    assert inf_a.transfer_id_timeout == pytest.approx(1.1)
    assert inf_b.transfer_id_timeout == pytest.approx(1.1)

    # Redundant reception from multiple interfaces concurrently.
    for tx_x in (tx_a, tx_b):
        assert await (
            tx_x.send(
                Transfer(
                    timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    transfer_id=2,
                    fragmented_payload=[memoryview(b"def")],
                ),
                asyncio.get_running_loop().time() + 1.0,
            )
        )
        assert await (
            tx_x.send(
                Transfer(
                    timestamp=Timestamp.now(),
                    priority=Priority.HIGH,
                    transfer_id=3,
                    fragmented_payload=[memoryview(b"ghi")],
                ),
                asyncio.get_running_loop().time() + 1.0,
            )
        )

    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 2
    assert tr.fragmented_payload == [memoryview(b"def")]

    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 3
    assert tr.fragmented_payload == [memoryview(b"ghi")]

    assert None is await (ses.receive(asyncio.get_running_loop().time() + 2.0))  # Nothing left to read now.

    # This one will be accepted despite a smaller transfer-ID because of the TID timeout.
    assert await (
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"acc")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )
    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"acc")]
    assert tr.inferior_session == inf_a

    # Stats check.
    assert ses.sample_statistics() == RedundantSessionStatistics(
        transfers=3,
        frames=inf_a.sample_statistics().frames + inf_b.sample_statistics().frames,
        payload_bytes=9,
        errors=0,
        drops=0,
        inferiors=[
            inf_a.sample_statistics(),
            inf_b.sample_statistics(),
        ],
    )

    ses.close()
    tr_a.close()
    tr_b.close()
    inf_a.close()
    inf_b.close()
    await asyncio.sleep(2.0)
Esempio n. 2
0
async def _unittest_spoofer(caplog: pytest.LogCaptureFixture) -> None:
    dcs_pres = Presentation(LoopbackTransport(1234))
    dcs_pub_spoof = dcs_pres.make_publisher(Spoof, 1)
    spoofer = Spoofer(dcs_pres.make_subscriber(Spoof, 1))

    # No target transports configured -- spoofing will do nothing except incrementing the transfer-ID counter.
    assert await dcs_pub_spoof.publish(
        Spoof(
            timeout=uavcan.si.unit.duration.Scalar_1_0(1.0),
            priority=org_uavcan_yukon.io.transfer.Priority_1_0(3),
            session=org_uavcan_yukon.io.transfer.Session_0_1(
                subject=org_uavcan_yukon.io.transfer.SubjectSession_0_1(
                    subject_id=uavcan.node.port.SubjectID_1_0(6666),
                    source=[uavcan.node.ID_1_0(1234)])),
            transfer_id=[],
            iface_id=[],
            payload=org_uavcan_yukon.io.transfer.Payload_1_0(b"Hello world!"),
        ))

    await asyncio.sleep(0.5)

    # Validate the transfer-ID map.
    assert len(spoofer._transfer_id_map) == 1
    assert list(spoofer._transfer_id_map.keys())[0].source_node_id == 1234
    assert list(spoofer._transfer_id_map.values())[0]._value == 1

    # Configure transports.
    cap_a: typing.List[pyuavcan.transport.Capture] = []
    cap_b: typing.List[pyuavcan.transport.Capture] = []
    target_tr_a = LoopbackTransport(None)
    target_tr_b = LoopbackTransport(None)
    target_tr_a.begin_capture(cap_a.append)
    target_tr_b.begin_capture(cap_b.append)
    spoofer.add_iface(111, target_tr_a)
    spoofer.add_iface(222, target_tr_b)

    # Spoof on both, successfully.
    spoof = Spoof(
        timeout=uavcan.si.unit.duration.Scalar_1_0(1.0),
        priority=org_uavcan_yukon.io.transfer.Priority_1_0(3),
        session=org_uavcan_yukon.io.transfer.Session_0_1(
            subject=org_uavcan_yukon.io.transfer.SubjectSession_0_1(
                subject_id=uavcan.node.port.SubjectID_1_0(6666),
                source=[uavcan.node.ID_1_0(1234)])),
        transfer_id=[9876543210],  # This transfer will not touch the TID map.
        iface_id=[],  # All ifaces.
        payload=org_uavcan_yukon.io.transfer.Payload_1_0(b"abcd"),
    )
    assert await dcs_pub_spoof.publish(spoof)
    await asyncio.sleep(0.5)

    assert len(cap_a) == len(cap_b)
    (cap, ) = cap_a
    cap_a.clear()
    cap_b.clear()
    assert isinstance(cap, LoopbackCapture)
    assert cap.transfer.metadata.transfer_id == 9876543210
    assert len(spoofer._transfer_id_map) == 1  # New entry was not created.
    assert spoofer.status == {
        111:
        IfaceStatus(num_bytes=4,
                    num_errors=0,
                    num_timeouts=0,
                    num_transfers=1,
                    backlog=0,
                    backlog_peak=0),
        222:
        IfaceStatus(num_bytes=4,
                    num_errors=0,
                    num_timeouts=0,
                    num_transfers=1,
                    backlog=0,
                    backlog_peak=0),
    }

    # Make one time out, the other raise an error, third one is closed.
    target_tr_a.spoof_result = False
    target_tr_b.spoof_result = RuntimeError("Intended exception")
    target_tr_c = LoopbackTransport(None)
    target_tr_c.close()
    spoofer.add_iface(0, target_tr_c)

    with caplog.at_level(logging.CRITICAL):
        assert await dcs_pub_spoof.publish(spoof)
        await asyncio.sleep(2.0)
    assert not cap_a
    assert not cap_b
    old_status = spoofer.status
    assert old_status == {
        0:
        IfaceStatus(num_bytes=0,
                    num_errors=1,
                    num_timeouts=0,
                    num_transfers=0,
                    backlog=0,
                    backlog_peak=0),
        111:
        IfaceStatus(num_bytes=4,
                    num_errors=0,
                    num_timeouts=1,
                    num_transfers=1,
                    backlog=0,
                    backlog_peak=0),
        222:
        IfaceStatus(num_bytes=4,
                    num_errors=1,
                    num_timeouts=0,
                    num_transfers=1,
                    backlog=0,
                    backlog_peak=0),
    }

    # Force only one iface out of three. Check that the backlog counter goes up.
    spoof.iface_id = [0]
    assert await dcs_pub_spoof.publish(spoof)
    assert await dcs_pub_spoof.publish(spoof)
    assert await dcs_pub_spoof.publish(spoof)
    await asyncio.sleep(2.0)
    assert spoofer.status[0].backlog > 0
    assert spoofer.status[0].backlog_peak > 0
    assert spoofer.status[111] == old_status[111]
    assert spoofer.status[222] == old_status[222]

    # Finalize.
    spoofer.close()
    target_tr_a.close()
    target_tr_b.close()
    target_tr_c.close()
    dcs_pres.close()
    await asyncio.sleep(1.0)
Esempio n. 3
0
async def _unittest_redundant_input_cyclic() -> None:
    asyncio.get_running_loop().slow_callback_duration = 5.0

    spec = pyuavcan.transport.InputSessionSpecifier(pyuavcan.transport.MessageDataSpecifier(4321), None)
    spec_tx = pyuavcan.transport.OutputSessionSpecifier(spec.data_specifier, None)
    meta = pyuavcan.transport.PayloadMetadata(30)

    ts = Timestamp.now()

    tr_a = LoopbackTransport(111)
    tr_b = LoopbackTransport(111)
    tx_a = tr_a.get_output_session(spec_tx, meta)
    tx_b = tr_b.get_output_session(spec_tx, meta)
    inf_a = tr_a.get_input_session(spec, meta)
    inf_b = tr_b.get_input_session(spec, meta)

    inf_a.transfer_id_timeout = 1.1  # This is used to ensure that the transfer-ID timeout is handled correctly.

    is_retired = False

    def retire() -> None:
        nonlocal is_retired
        is_retired = True

    ses = RedundantInputSession(spec, meta, tid_modulo_provider=lambda: 32, finalizer=retire)  # Like CAN, for example.
    assert not is_retired
    assert ses.specifier is spec
    assert ses.payload_metadata is meta
    assert not ses.inferiors
    assert ses.sample_statistics() == RedundantSessionStatistics()
    assert pytest.approx(0.0) == ses.transfer_id_timeout

    # Empty inferior set reception.
    time_before = asyncio.get_running_loop().time()
    assert not await (ses.receive(asyncio.get_running_loop().time() + 2.0))
    assert (
        1.0 < asyncio.get_running_loop().time() - time_before < 5.0
    ), "The method should have returned in about two seconds."

    # Begin reception, then add an inferior while the reception is in progress.
    assert await (
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"abc")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )

    async def add_inferior(inferior: pyuavcan.transport.InputSession) -> None:
        await asyncio.sleep(1.0)
        ses._add_inferior(inferior)  # pylint: disable=protected-access

    time_before = asyncio.get_running_loop().time()
    tr, _ = await (
        asyncio.gather(
            # Start reception here. It would stall for two seconds because no inferiors.
            ses.receive(asyncio.get_running_loop().time() + 2.0),
            # While the transmission is stalled, add one inferior with a delay.
            add_inferior(inf_a),
        )
    )
    assert (
        0.0 < asyncio.get_running_loop().time() - time_before < 5.0
    ), "The method should have returned in about one second."
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"abc")]
    assert tr.inferior_session == inf_a

    # More inferiors
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    ses._add_inferior(inf_a)  # No change, added above    # pylint: disable=protected-access
    assert ses.inferiors == [inf_a]
    ses._add_inferior(inf_b)  # pylint: disable=protected-access
    assert ses.inferiors == [inf_a, inf_b]
    assert ses.transfer_id_timeout == pytest.approx(1.1)
    assert inf_b.transfer_id_timeout == pytest.approx(1.1)

    # Redundant reception - new transfers accepted because the iface switch timeout is exceeded.
    time.sleep(ses.transfer_id_timeout)  # Just to make sure that it is REALLY exceeded.
    assert await (
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=2,
                fragmented_payload=[memoryview(b"def")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )
    assert await (
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=3,
                fragmented_payload=[memoryview(b"ghi")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )

    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 2
    assert tr.fragmented_payload == [memoryview(b"def")]
    assert tr.inferior_session == inf_b

    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 3
    assert tr.fragmented_payload == [memoryview(b"ghi")]
    assert tr.inferior_session == inf_b

    assert None is await (ses.receive(asyncio.get_running_loop().time() + 1.0))  # Nothing left to read now.

    # This one will be rejected because wrong iface and the switch timeout is not yet exceeded.
    assert await (
        tx_a.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=4,
                fragmented_payload=[memoryview(b"rej")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )
    assert None is await (ses.receive(asyncio.get_running_loop().time() + 0.1))

    # Transfer-ID timeout reconfiguration.
    ses.transfer_id_timeout = 3.0
    with pytest.raises(ValueError):
        ses.transfer_id_timeout = -0.0
    assert ses.transfer_id_timeout == pytest.approx(3.0)
    assert inf_a.transfer_id_timeout == pytest.approx(3.0)
    assert inf_a.transfer_id_timeout == pytest.approx(3.0)

    # Inferior removal resets the state of the deduplicator.
    ses._close_inferior(0)  # pylint: disable=protected-access
    ses._close_inferior(1)  # Out of range, no effect.  # pylint: disable=protected-access
    assert ses.inferiors == [inf_b]

    assert await (
        tx_b.send(
            Transfer(
                timestamp=Timestamp.now(),
                priority=Priority.HIGH,
                transfer_id=1,
                fragmented_payload=[memoryview(b"acc")],
            ),
            asyncio.get_running_loop().time() + 1.0,
        )
    )
    tr = await (ses.receive(asyncio.get_running_loop().time() + 0.1))
    assert isinstance(tr, RedundantTransferFrom)
    assert ts.monotonic <= tr.timestamp.monotonic <= (asyncio.get_running_loop().time() + 1e-3)
    assert tr.priority == Priority.HIGH
    assert tr.transfer_id == 1
    assert tr.fragmented_payload == [memoryview(b"acc")]
    assert tr.inferior_session == inf_b

    # Stats check.
    assert ses.sample_statistics() == RedundantSessionStatistics(
        transfers=4,
        frames=inf_b.sample_statistics().frames,
        payload_bytes=12,
        errors=0,
        drops=0,
        inferiors=[
            inf_b.sample_statistics(),
        ],
    )

    # Closure.
    assert not is_retired
    ses.close()
    assert is_retired
    is_retired = False
    ses.close()
    assert not is_retired
    assert not ses.inferiors
    with pytest.raises(ResourceClosedError):
        await (ses.receive(0))
    tr_a.close()
    tr_b.close()
    inf_a.close()
    inf_b.close()
    await asyncio.sleep(2.0)