"""Wire-format tests. The byte counts here are the whole point. Two of them exist because pyparrot gets them wrong and pyparrot is the obvious thing to check against: the command id is 16 bits, and an enum is 32. """ import struct import pytest from mcbebop.protocol import codec, xml_index from mcbebop.protocol.types import ArgSpec, CommandSpec, EnumSpec def test_header_is_four_bytes_with_a_16_bit_command_id(): assert codec.HEADER.size == 4 assert codec.HEADER.pack(1, 0, 2) == b"\x01\x00\x02\x00" # An id above 255 must survive, even though no current command has one. assert codec.HEADER.unpack(codec.HEADER.pack(1, 4, 300)) == (1, 4, 300) def test_encode_command_is_header_plus_args(): spec = xml_index.get("ardrone3.MediaStreaming.VideoEnable") assert codec.encode_command(spec, {"enable": 1}) == b"\x01\x15\x00\x00\x01" def test_no_arg_command_encodes_to_nothing(): assert codec.encode_args(xml_index.get("ardrone3.Piloting.TakeOff")) == b"" assert codec.encode_args(xml_index.get("ardrone3.Piloting.FlatTrim"), {}) == b"" def test_enum_is_four_bytes_little_endian(): spec = xml_index.get("ardrone3.Network.WifiScan") assert codec.encode_args(spec, {"band": "5ghz"}) == b"\x01\x00\x00\x00" assert codec.encode_args(spec, {"band": "2_4ghz"}) == b"\x00\x00\x00\x00" # An int position is accepted too, for replaying captured traffic. assert codec.encode_args(spec, {"band": 2}) == b"\x02\x00\x00\x00" def test_enum_decodes_back_to_the_member_name(): spec = xml_index.get("ardrone3.Network.WifiScan") assert codec.decode_args(spec, b"\x01\x00\x00\x00") == {"WifiScan_band": "5ghz"} def test_unknown_enum_position_decodes_to_the_number(): # A firmware member this XML snapshot predates must not raise. spec = xml_index.get("ardrone3.Network.WifiScan") assert codec.decode_args(spec, b"\x63\x00\x00\x00") == {"WifiScan_band": 99} def test_string_is_nul_terminated(): spec = xml_index.get("common.Settings.ProductName") assert codec.encode_args(spec, {"name": "Bebop"}) == b"Bebop\x00" assert codec.decode_args(spec, b"Bebop\x00") == {"ProductName_name": "Bebop"} def test_string_arguments_can_be_followed_by_more_arguments(): # A variable-length field mid-payload is where a fixed-width read goes # wrong: everything after the string shifts. spec = xml_index.get("ardrone3.NetworkState.WifiScanListChanged") assert [a.type for a in spec.args] == ["string", "i16", "enum", "u8"] payload = b"MyNet\x00" + struct.pack(" CommandSpec: """Parrot's other projects use bitfields; these two files do not.""" arg = ArgSpec( name="flags", type=f"bitfield:{width}:Flags", members=(EnumSpec("first", 0), EnumSpec("second", 1), EnumSpec("third", 2)), ) return CommandSpec(project="x", klass="Y", name="Z", ids=(9, 9, 9), args=(arg,)) def test_bitfield_takes_member_names_or_a_mask(): spec = _bitfield_spec("u8") assert codec.encode_args(spec, {"flags": ["first", "third"]}) == b"\x05" assert codec.encode_args(spec, {"flags": "second"}) == b"\x02" assert codec.encode_args(spec, {"flags": 0xFF}) == b"\xff" assert codec.decode_args(spec, b"\x05") == {"Z_flags": 5} def test_bitfield_width_sets_the_wire_size(): assert len(codec.encode_args(_bitfield_spec("u16"), {"flags": 1})) == 2 assert len(codec.encode_args(_bitfield_spec("u32"), {"flags": 1})) == 4 with pytest.raises(ValueError, match="unknown bitfield width 'u7'"): codec.encode_args(_bitfield_spec("u7"), {"flags": 1}) def test_bitfield_rejects_an_unknown_member(): with pytest.raises(ValueError, match="no member named 'fourth'"): codec.encode_args(_bitfield_spec("u8"), {"flags": ["fourth"]}) def test_unsupported_type_is_reported(): blob = ArgSpec(name="a", type="blob") spec = CommandSpec(project="x", klass="Y", name="Z", ids=(9, 9, 8), args=(blob,)) with pytest.raises(ValueError, match="unsupported type 'blob'"): codec.encode_args(spec, {"a": 1}) def test_every_type_in_the_xml_is_encodable(): declared = {a.type for c in xml_index.all_commands() for a in c.args} for type_name in declared: if type_name in ("string", "enum"): continue assert struct.calcsize(codec._FORMATS[type_name]) > 0