Files
mcbebop/tests/test_expectations.py
T
rsp2k ef8c4658e0 Protocol layer: XML index, codec, safety tiers, expectations
Parse Parrot's common.xml and ardrone3.xml into CommandSpec objects rather
than transcribing 264 commands by hand. Direction is derived from
comment result=/triggered= with a class-name-suffix fallback for the 31
commands that state neither, giving 101 to-drone and 163 from-drone.
ardrone3.Piloting.FlatTrim is injected: the firmware accepts it but neither
the vendored snapshot nor upstream defines it.

codec encodes the <BBH header (the command id is 16 bits, not 8) and reads
enums as i32 and strings as NUL-terminated, both of which pyparrot gets
wrong. safety assigns the arming tier by class, with Mavlink and Calibration
counted as motion because they move the aircraft. expectations parses the
undocumented #p-c-m grammar including | alternatives and this.<arg> echoes.

90 tests, counts asserted so a bad parse fails loudly.
2026-10-02 00:07:43 -06:00

114 lines
4.3 KiB
Python

"""The expectations grammar is undocumented, so the examples are the spec."""
import xml.etree.ElementTree as ET
import pytest
from mcbebop.protocol import xml_index
from mcbebop.protocol.expectations import expectations_for, parse_expectation_text
from mcbebop.protocol.types import Direction
def test_every_block_is_parsed_and_only_sendable_commands_carry_one():
with_expectations = [c for c in xml_index.all_commands() if c.expectations]
assert len(with_expectations) == 82
assert all(c.direction is Direction.TO_DRONE for c in with_expectations)
def test_delayed_blocks():
delayed = [c.full_name for c in xml_index.all_commands() if any(e.delayed for e in c.expectations)]
assert sorted(delayed) == ["ardrone3.Network.WifiScan", "ardrone3.Network.WifiAuthChannel"][::-1]
def test_bare_reference():
(expectation,) = parse_expectation_text("#0-3-0")
assert expectation.ids == (0, 3, 0)
assert expectation.fields == {}
assert expectation.alternatives == ()
assert expectation.delayed is False
def test_this_dot_argname_means_the_drone_echoes_what_was_sent():
(expectation,) = xml_index.get("common.Settings.ProductName").expectations
assert expectation.ids == (0, 3, 2)
assert expectation.fields == {"name": "this.name"}
def test_whitespace_separated_references_are_all_expected():
expectations = xml_index.get("common.Settings.AutoCountry").expectations
assert len(expectations) == 2
assert expectations[0].ids == (0, 3, 7)
assert expectations[0].fields == {"automatic": "this.automatic"}
assert expectations[1].ids == (0, 3, 6)
assert all(e.alternatives == () for e in expectations)
def test_pipe_prefixed_references_are_alternatives():
(expectation,) = xml_index.get("ardrone3.Piloting.NavigateHome").expectations
assert expectation.ids == (1, 4, 3)
assert expectation.fields == {"state": "inProgress", "reason": "userRequest"}
assert len(expectation.alternatives) == 2
assert [a.fields["state"] for a in expectation.alternatives] == ["pending", "available"]
assert all(a.ids == (1, 4, 3) for a in expectation.alternatives)
def test_alternatives_can_name_different_commands():
# StopPilotedPOI is confirmed by whichever POI event version is in play.
(expectation,) = xml_index.get("ardrone3.Piloting.StopPilotedPOI").expectations
assert expectation.ids == (1, 4, 14)
assert [a.ids for a in expectation.alternatives] == [(1, 4, 22)]
def test_literal_enum_member_values():
(expectation,) = xml_index.get("ardrone3.Piloting.StartPilotedPOI").expectations
assert expectation.fields["status"] == "RUNNING"
assert expectation.fields["latitude"] == "this.latitude"
assert expectation.alternatives[0].fields["status"] == "PENDING"
def test_takeoff_expects_two_states_not_one_of_two():
expectations = xml_index.get("ardrone3.Piloting.TakeOff").expectations
assert [e.fields["state"] for e in expectations] == ["motor_ramping", "takingoff"]
def test_delayed_flag_reaches_the_alternatives_too():
parsed = parse_expectation_text("#1-14-0 |#1-14-1", delayed=True)
assert parsed[0].delayed is True
assert parsed[0].alternatives[0].delayed is True
def test_spaced_pipe_still_parses():
(expectation,) = parse_expectation_text("#0-1-2 | #0-1-3")
assert [a.ids for a in expectation.alternatives] == [(0, 1, 3)]
def test_empty_block_is_empty_not_an_error():
assert parse_expectation_text("\n\t\t\t") == ()
def test_leading_alternative_is_rejected():
with pytest.raises(ValueError, match="starts with an alternative"):
parse_expectation_text("|#0-1-2")
def test_text_without_a_reference_is_rejected():
with pytest.raises(ValueError, match="no #p-c-m reference"):
parse_expectation_text("ProductNameChanged")
def test_malformed_field_is_rejected():
with pytest.raises(ValueError, match="is not 'name: value'"):
parse_expectation_text("#0-3-2(name)")
def test_unknown_expectations_child_is_rejected():
cmd = ET.fromstring(
"<cmd name='X' id='0'><expectations><eventually>#0-0-0</eventually></expectations></cmd>"
)
with pytest.raises(ValueError, match="unknown expectations child"):
expectations_for(cmd)
def test_commands_without_a_block_have_an_empty_tuple():
assert xml_index.get("ardrone3.Piloting.PCMD").expectations == ()