Files
mcbebop/tests/test_live_findings.py
Ryan Malloy 41a6cb107f Correct the one-controller claim: the drone takes over, it does not refuse
Tested on the aircraft. A second ARSDK handshake is accepted and telemetry is
redirected to it; the first session's frames stop while it still reports
connected = True. So the claim inherited from pyparrot's error text, which had
reached our error messages, tool descriptions, simulator behaviour and a test
name, was wrong in the most misleading direction: a refusal would be loud, and
this is silent.

The simulator now models the takeover by default; refusal stays available
because a client must handle a non-zero status anyway.
2026-10-02 03:44:05 -06:00

74 lines
3.2 KiB
Python

"""Regressions for three bugs that only a real aircraft exposed.
Each of these passed every simulator test while being wrong against the drone,
which is why they have their own file: the simulator was written from the same
assumptions as the code, so it agreed with it.
"""
from mcbebop.arsdk.types import BufferId, DataType, Frame
from mcbebop.protocol import codec, xml_index
from mcbebop.protocol.types import Direction
def test_argument_less_events_are_recorded():
"""Eight events carry no arguments; their arrival IS the message.
AllStatesChanged and AllSettingsChanged are how the drone says a state dump
is finished. Keying only by argument meant they decoded to an empty dict and
disappeared, so nothing could wait for the end of a burst.
"""
for name in ("common.CommonState.AllStatesChanged", "common.SettingsState.AllSettingsChanged"):
spec = xml_index.get(name)
assert spec.args == (), "this test is about events that have no arguments"
_, values = codec.decode_event(codec.encode_command(spec, {}))
assert values == {spec.name: True}
bare = [c for c in xml_index.all_commands() if c.direction is Direction.FROM_DRONE and not c.args]
assert len(bare) == 8
def test_every_bare_event_decodes_to_something():
for spec in xml_index.all_commands():
if spec.direction is Direction.FROM_DRONE and not spec.args:
_, values = codec.decode_event(codec.encode_command(spec, {}))
assert values, f"{spec.full_name} decoded to nothing"
def test_acknowledgement_is_decided_by_data_type_not_buffer():
"""A live Bebop 2 sends DATA_WITH_ACK on buffer 126, not 127.
The naming says 127 is the drone's acknowledged buffer, and requiring both
the type and the buffer to agree meant no frame was ever acknowledged. The
drone then resent its state rather than continuing, and most of it never
arrived: 35 keys instead of 192.
"""
import inspect
from mcbebop.arsdk import connection as conn_mod
source = inspect.getsource(conn_mod.Connection._handle)
ack_line = next(line for line in source.splitlines() if "DataType.DATA_WITH_ACK" in line)
assert "buffer_id" not in ack_line, "the ack decision must not depend on which buffer it arrived on"
# Both buffers must round-trip to a valid ack target.
for buffer_id in (BufferId.D2C_NON_ACK, BufferId.D2C_ACK):
frame = Frame(DataType.DATA_WITH_ACK, buffer_id, 9, b"\x00\x05\x00\x00")
assert Frame.decode_all(frame.encode())[0].buffer_id == buffer_id
assert 0 <= BufferId.ack_for(buffer_id) <= 255
def test_simulator_models_takeover_not_refusal_by_default():
"""The aircraft accepts a second controller; it does not refuse one.
Tested on the real drone 2026-10-02: session A's frames froze at 264 while
session B took over, and A went on reporting connected = True. The
simulator defaulted to refusing, which is the same unverified assumption
the client carried, so no test could catch the difference.
"""
import dataclasses
from mcbebop.sim import FakeBebop
field = next(f for f in dataclasses.fields(FakeBebop) if f.name == "single_controller")
assert field.default is False, "the default must model the aircraft, not the folklore"