mtc 新模块:命令式动作脚本(Python DSL)→ .mt 矩阵编译器,非阻塞时间轴 IR(重叠截断/rest 静止值)+ 10ms 栅格化,产物可由 pybot/bot4cj 直接执行;34 测试全绿(含 pybot parser e2e)。 pybot speaker.py:frequency<=0 改为静音节拍(仅 sleep 保时序,不再钳到 min_f 低鸣),对齐仓颉版 sleep 占位语义;mtc Beep 通道 rest=0,段前/段间/段后回落静音,两侧语义闭环。 修复 test_actpred_compatibility 过时断言(99c1bc1 首行分解为重复微步 19→20 帧时漏改,14→15)。 文档三处同步:根 README 回填 memdb/pybot/actpred 并新增 mtc(模块表+端口一览+章节),MODULES/ARCHITECTURE 同步 mtc,新建 mtc/README.md。
128 lines
4.1 KiB
Python
128 lines
4.1 KiB
Python
"""DSL 收集测试:硬件声明 / 时间轴指令 / 校验规则"""
|
|
|
|
import pytest
|
|
|
|
from mtc.dsl import Collector, CompileError, compile_script, make_namespace
|
|
|
|
|
|
def run(source):
|
|
"""exec 脚本并返回 collector"""
|
|
c = Collector()
|
|
exec(compile(source, "<test>", "exec"), make_namespace(c))
|
|
return c
|
|
|
|
|
|
# ---------------- 硬件声明 ----------------
|
|
|
|
def test_hw_blocks_collected_in_order():
|
|
c = run('I2C("i2c-1", "/dev/i2c-1")\n'
|
|
'Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Servo("s2", 180, 0, 0, "pca9685-0", 1)\n')
|
|
assert [b[0] for b in c.blocks] == ["I2C", "Servo", "Servo"]
|
|
assert c.blocks[1][2] == ["s1", "90", "0", "0", "pca9685-0", "0"]
|
|
assert c.device_names() == {"i2c-1", "s1", "s2"}
|
|
|
|
|
|
def test_hw_block_arity_error():
|
|
with pytest.raises(CompileError, match="需要 6 个参数"):
|
|
run('Servo("s1", 90, 0)\n')
|
|
|
|
|
|
def test_register_block_extension():
|
|
c = run('register_block("ADS1115", ["name", "I2C", "addr"])\n'
|
|
'ADS1115("ads0", "i2c-1", "0x48")\n')
|
|
assert c.blocks[0][0] == "ADS1115"
|
|
assert c.blocks[0][1] == ["name", "I2C", "addr"]
|
|
|
|
|
|
def test_register_block_requires_name_first():
|
|
with pytest.raises(CompileError, match="首列必须是 name"):
|
|
run('register_block("Bad", ["addr"])\n')
|
|
|
|
|
|
# ---------------- 时间轴指令 ----------------
|
|
|
|
def test_action_delay_wait_cursor():
|
|
c = run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Servo("s2", 90, 0, 0, "pca9685-0", 1)\n'
|
|
'Action("s1", 10, 100)\n'
|
|
'Delay(50)\n'
|
|
'Action("s2", 20, 50)\n'
|
|
'Wait()\n')
|
|
assert c.cursor == 100.0 # Wait 推进到最晚完成时刻
|
|
assert c.channels["s1.angle"].segments[0].v1 == 10
|
|
assert c.channels["s2.angle"].segments[0].t0 == 50
|
|
|
|
|
|
def test_action_relative_accumulates():
|
|
c = run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Action("s1", 10, 100)\n'
|
|
'Wait()\n'
|
|
'Action("s1", 10, 100)\n')
|
|
segs = c.channels["s1.angle"].segments
|
|
assert (segs[1].v0, segs[1].v1) == (10, 20) # 相对叠加在前段目标值上
|
|
|
|
|
|
def test_setto_absolute():
|
|
c = run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Pose("s1", 45)\n'
|
|
'SetTo("s1", 90, 100)\n')
|
|
seg = c.channels["s1.angle"].segments[0]
|
|
assert (seg.v0, seg.v1) == (45, 90)
|
|
|
|
|
|
def test_beep_registers_frequency_channel():
|
|
c = run('Speaker("spk", 2, 48000, 0.6, 5000, 20)\n'
|
|
'Beep(1000, 50)\n')
|
|
assert "spk.frequency" in c.channels
|
|
assert c.channels["spk.frequency"].segments[0].v1 == 1000
|
|
|
|
|
|
# ---------------- 校验 ----------------
|
|
|
|
def test_action_undeclared_device_rejected():
|
|
with pytest.raises(CompileError, match="未声明"):
|
|
run('Action("s1", 10, 100)\n')
|
|
|
|
|
|
def test_dur_must_be_positive_multiple_of_10():
|
|
with pytest.raises(CompileError, match="10 的倍数"):
|
|
run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Action("s1", 10, 15)\n')
|
|
with pytest.raises(CompileError, match="10 的倍数"):
|
|
run('Delay(5)\n')
|
|
|
|
|
|
def test_delay_column_is_reserved():
|
|
with pytest.raises(CompileError, match="编译器管理"):
|
|
run('Speaker("spk", 2, 48000, 0.6, 5000, 20)\n'
|
|
'Action("spk", 10, 100, param="delay")\n')
|
|
|
|
|
|
def test_pose_after_action_rejected():
|
|
with pytest.raises(CompileError, match="Action 之前"):
|
|
run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'Action("s1", 10, 100)\n'
|
|
'Pose("s1", 45)\n')
|
|
|
|
|
|
def test_empty_script_rejected():
|
|
with pytest.raises(CompileError, match="没有任何动作"):
|
|
compile_script('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n')
|
|
|
|
|
|
def test_python_loop_and_math_work():
|
|
"""Python DSL 红利:for 循环与数学运算直接可用"""
|
|
c = run('Servo("s1", 90, 0, 0, "pca9685-0", 0)\n'
|
|
'for i in range(4):\n'
|
|
' Action("s1", 5 if i % 2 == 0 else -5, 20)\n'
|
|
' Wait()\n')
|
|
segs = c.channels["s1.angle"].segments
|
|
assert len(segs) == 4
|
|
assert [s.v1 for s in segs] == [5, 0, 5, 0]
|
|
|
|
|
|
def test_syntax_error_wrapped():
|
|
with pytest.raises(CompileError, match="SyntaxError"):
|
|
compile_script("def broken(:\n")
|