test_codec.py 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """codec 层验证 — 编码向量来自 backup pcap 实测, 帧布局来自 cq2_pure_enter.py。"""
  2. import random
  3. import struct
  4. from pathlib import Path
  5. import pytest
  6. from pbot.codec.bitbuf import decode6, default_table_path, encode6
  7. from pbot.codec.frame import Frame, FrameSplitter, build_frame, parse_frame
  8. from pbot.errors import CodecError
  9. from pbot.types import pack_point
  10. # pcap 心跳帧向量(backup self-test 100% 匹配): 密文如下, 明文构造见 _plain_heartbeat()
  11. _HEARTBEAT_ENC = bytes.fromhex(
  12. "5d5452415d4c67445d545078495e55525d5452415d5452415d54507a5d5452415d5452415d5452415d54527b5d545241")
  13. def _plain_heartbeat() -> bytes:
  14. return struct.pack("<8i", 0, 60000, 1789709744, 0, 0, 149, 0, 0) + bytes.fromhex("70000000")
  15. def test_table_exists() -> None:
  16. """Given: 仓库资产 When: 按默认路径找替换表 Then: 存在且 512B。"""
  17. path = default_table_path()
  18. assert Path(path).exists(), f"替换表不在 {path}"
  19. assert Path(path).stat().st_size >= 512
  20. def test_encode6_matches_pcap_vector() -> None:
  21. """Given: 实测心跳明文 When: encode6 Then: 与 pcap 抓包字节完全一致。"""
  22. assert encode6(_plain_heartbeat()) == _HEARTBEAT_ENC
  23. def test_decode6_roundtrip_pcap_vector() -> None:
  24. """Given: pcap 密文 When: decode6 Then: 还原心跳明文。"""
  25. assert decode6(_HEARTBEAT_ENC) == _plain_heartbeat()
  26. def test_roundtrip_random_bytes() -> None:
  27. """Given: 任意字节流 When: encode→decode Then: 恒等。"""
  28. rng = random.Random(20260923) # noqa: S311 - 非密码用途, 固定种子
  29. src = bytes(rng.randrange(256) for _ in range(1000))
  30. assert decode6(encode6(src)) == src
  31. def test_build_and_parse_frame_roundtrip() -> None:
  32. """Given: CS3011 走路帧参数 When: build→parse Then: 全字段还原。"""
  33. wire = build_frame(3011, pack_point(23, 27), 0, 5, pack_point(23, 27),
  34. body="等待地图".encode(), tail=7)
  35. f = parse_frame(wire)
  36. assert f.msg_id == 3011
  37. assert f.p1 == 0 and f.p2 == 5 and f.tail == 7
  38. assert f.body == "等待地图".encode()
  39. assert f.pre == 0
  40. def test_splitter_handles_tcp_fragmentation() -> None:
  41. """Given: 两帧粘包并被任意切碎 When: 逐块 feed Then: 恰好产出两帧。"""
  42. f1 = build_frame(10, 0, 1, 2, 3, b"x" * 10, tail=1)
  43. f2 = build_frame(51, 0, 4, 5, 6, b"", tail=2)
  44. blob = f1 + f2
  45. splitter = FrameSplitter()
  46. frames: list[Frame] = []
  47. for i in range(len(blob)):
  48. frames += splitter.feed(blob[i:i + 1])
  49. assert [(f.msg_id, f.tail) for f in frames] == [(10, 1), (51, 2)]
  50. def test_splitter_resyncs_on_garbage() -> None:
  51. """Given: 帧前混入垃圾字节 When: feed Then: 垃圾被跳过, 帧仍解析出。"""
  52. wire = build_frame(60000, 0, 0, 0, 0, b"", tail=3)
  53. splitter = FrameSplitter()
  54. frames = splitter.feed(b"\x00garbage" + wire)
  55. assert [fr.msg_id for fr in frames] == [60000]
  56. assert frames[0].tail == 3
  57. def test_parse_frame_rejects_short_input() -> None:
  58. """Given: 不足 48 字符 When: parse_frame Then: CodecError。"""
  59. with pytest.raises(CodecError):
  60. _ = parse_frame(b"ab")