feat(pybot): bot4cj 的 Python 迁移版(.mt 解析 + 硬件驱动 + 插件系统)

- parser: FSM 解析器与仓颉版逐语义对齐,修 S01/S02/S04/Q05
- robot/hardvars: 六类硬件变量工厂,修 r.cj:911 重名 FIXME、Q07 倒序释放
- plugin: Python 插件替代 Lua 桥接,修 S07 三级路径查找
- hd: i2c(每fd锁 Q03/S06)/spi/servo/kalman/mpu6050(S05/Q06)/speaker(S03 实声)
- modules: PCA9685 与 Lua 版逐字节对齐;ADS1115 重写(experimental);ADS1256 空桩
- tests: 42 项 pytest 全绿(Mock 总线 journal 断言)
- CLI: --dry-run / --test roundtrip / --verbose;运行时零第三方依赖
This commit is contained in:
qing committed 2026-08-09 18:57:03 +00:00
1 parent 85eaa2114e
commit bba7ee3eb4
31 files changed
+2155

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
"""解析器测试:FSM / 列数校验(S01) / 占位符语义 / 序列化往返 / 全量脚本"""
import io
import pytest
from pybot.parser import (DoesNotSync, MatrixParser, MatrixSaver,
get_mt, parse_mt)
from tests.conftest import SCRIPTS_DIR
def parse_all(text):
parser = MatrixParser(text)
objects = []
while True:
obj = parser.parse()
if obj is None:
break
objects.append(obj)
return objects
# ---------------- 基本解析 ----------------
def test_parse_simple_block():
objs = parse_all("@Foo\nname val;\na 1;\nb 2;\n@\n")
assert len(objs) == 1
m = objs[0]
assert m.name == "Foo"
assert m.size == 3 and m.col == 2
assert m[0, 0] == "name" and m[2, 1] == "2"
def test_parse_multiple_blocks_and_comments():
text = ("# 头部注释\n"
"@A\nx y;\n1 2;\n@\n"
"@B\nn;\nv; # 行内注释\n@\n")
objs = parse_all(text)
assert [o.name for o in objs] == ["A", "B"]
assert objs[1][1, 0] == "v"
def test_parse_std_mt():
objs = parse_all((SCRIPTS_DIR / "std.mt").read_text(encoding="utf-8"))
names = [o.name for o in objs]
assert names == ["Alias", "I2C", "PCA9685", "MPU6050_BIND",
"Speaker", "Servo", "Action"]
servo = objs[names.index("Servo")]
assert servo.size == 11 and servo.col == 6 # 表头 + 10 舵机
# 注释掉的 MPU6050_BIND 数据行不产生内容
assert objs[names.index("MPU6050_BIND")].size == 1
@pytest.mark.parametrize("script", sorted(SCRIPTS_DIR.glob("*.mt")),
ids=lambda p: p.name)
def test_parse_all_scripts(script):
"""全量 scripts/*.mt 解析:块内每行列数一致且非空"""
objs = parse_all(script.read_text(encoding="utf-8"))
assert objs, f"{script.name}: no blocks parsed"
for o in objs:
assert o.name
for row in o.values:
assert len(row) == o.col
# ---------------- S01:列数校验 ----------------
def test_does_not_sync():
with pytest.raises(DoesNotSync) as ei:
parse_all("@Foo\na b c;\n1 2;\n@\n")
assert ei.value.expected == 3 and ei.value.actual == 2
def test_missing_semicolon_merges_lines():
"""漏写分号导致跨行合并成一行(S04 回归场景),列数不齐即报错"""
with pytest.raises(DoesNotSync):
parse_all("@Foo\na b;\n1 2\n3 4;\n@\n")
# ---------------- 占位符语义 ----------------
def _matrix(text):
return parse_all(text)[0]
def test_placeholder_recursion():
m = _matrix("@T\nname v;\na 5;\nb -;\nc -;\n@\n")
assert get_mt(m, 3, 1) == "5" # 递归两级到首行
assert parse_mt(m, 3, 1) == 5.0
def test_placeholder_first_row():
"""首行 '-':get_mt 返回字面 '-';parse_mt 解析为 typ("0")
⚠️ 与 actpred/mt_parser.py 不同(后者用 @Servo 初始角度填充),不得误统一"""
m = _matrix("@T\nname v;\na -;\n@\n")
assert get_mt(m, 1, 1) == "-"
assert parse_mt(m, 1, 1) == 0.0
assert parse_mt(m, 1, 1, int) == 0
# ---------------- 序列化往返 ----------------
def test_roundtrip():
text = (SCRIPTS_DIR / "std.mt").read_text(encoding="utf-8")
objs = parse_all(text)
out = io.StringIO()
MatrixSaver(out).save(objs)
objs2 = parse_all(out.getvalue())
assert len(objs2) == len(objs)
for a, b in zip(objs, objs2):
assert a.name == b.name
assert a.values == b.values