cocotb TestFailed / AssertionError:断言失败报错读法速查
更新时间:2026-10-26。FAQ 速查页,属于 EDA 排错系列。cocotb 测试里最常见的失败形态就是「assert 一句期望值,回归表打出一个 FAIL」——报文不长,但新手常被三件事卡住:报头到底是
TestFailed还是AssertionError、traceback 里的got和expect哪个是 DUT 真实值、以及「这次失败下次还一样吗」。本篇按「报错原文 → 三要素读法 → 复现」组织。超时类失败(报头SimTimeoutError)见 cocotb TimeoutError,回归一个用例都没跑起来的见 cocotb NO_TEST_ERROR。
首屏答案:cocotb 断言失败读三件事就够了——① 倒数第一个 traceback 帧指向你自己 testbench 里 assert 那一行,AssertionError: ... got X expect Y 里 got 是 int(dut.信号.value) 读出的 DUT 实际值,expect 是你写死的期望;② 报头在 cocotb 1.x 显示为 TestFailed(断言被包了一层),2.x 直接原样打出 AssertionError,措辞不同、含义完全相同;③ 只要 RANDOM_SEED 相同,激励序列逐比特一致,失败必然原地复现——改代码前先用同 seed 重跑一次确认现象稳定,再动手。
本文要回答的问题
TestFailed和AssertionError两个报头是什么关系,哪个版本是哪个?- traceback 里
got 1 expect 7,这个 1 是 DUT 算出来的还是我期望的? - 失败带随机激励时,怎么保证「这次看到的 bug 下次还在」?
- 回归汇总表
TESTS=1 PASS=0 FAIL=1与 make 退出码该信哪个?
一、报错原文与现象
最小复现:一个 4 bit 计数器 dut_counter,复位释放后每拍加 1;testbench 在两个上升沿后读 q,故意断言它等于 7:
@cocotb.test()
async def counter_wrong_expect(dut):
cocotb.start_soon(Clock(dut.clk, 10, 'ns').start())
dut.rst_n.value = 0
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
dut.rst_n.value = 1
await RisingEdge(dut.clk)
await RisingEdge(dut.clk)
got = int(dut.q.value)
dut._log.info("q=%d, expecting 7 (deliberately wrong)", got)
assert got == 7, "counter mismatch: got %d expect 7" % gotmake RANDOM_SEED=1234 一跑,断言如期爆炸:
读这张终端卡抓三个要点。第一,失败时间戳是 30.00ns——复位在 20ns 释放,之后两个上升沿 q 从 0 计到 1(非阻塞赋值在沿后才更新,第二沿读出的是 1),时间轴和波形完全对得上,说明 DUT 没疯,是期望值写错了。第二,traceback 最后一帧就是 testbench 第 16 行那句 assert got == 7,下面一行 AssertionError: counter mismatch: got 1 expect 7 把你格式化进消息里的两个数原样打出——断言消息是自己写给自己的现场笔录,写消息时务必把 got 和 expect 都格式化进去,否则只剩一句干巴巴的 AssertionError,等于没报。第三,末尾 make: *** [results.xml] Error 1:cocotb 把 FAIL 写进 results.xml 后让 make 非零退出,CI 靠这个退出码挡板子。
二、三要素读法:dut 值、期望值、种子
每次断言失败,沿着下面这条链从左到右对一遍,答案一定在链上某一环:
图中蓝色是观测与激励的入口,红色是判定动作(断言这一刀切下去的位置),橙色是期望值这一侧——新手最常见的错不是 DUT 算错,而是橙色这一环错了:手算期望时漏算非阻塞赋值慢半拍、参考模型和 DUT 用的复位策略不一致。绿色是比对通过后(或失败后分析出)的归属结论。排查顺序建议固定:先信 got 是真实的(它是仿真器里读出来的硬件值,几乎不会骗你),再质疑 expect 的来源,最后才怀疑 DUT。
两个高频误读顺带点掉。其一,got 读早一拍:时序逻辑里 await RisingEdge(clk) 返回的瞬间沿刚刚发生、非阻塞赋值还没生效,紧接着 int(dut.q.value) 读到的是上一拍的值——本文实验里 30ns 读到 1 而不是 2 正是这个原因,想要「沿后稳定值」就在 RisingEdge 后再补一个 await ReadOnly() 或 await Timer(1, 'ns')。其二,got 里混着 X/Z:int() 遇到未知态直接抛 ValueError,报头会变成 ValueError 而不是 AssertionError——这其实是更早期的信号(复位没做、信号没驱动),按未驱动方向查,别当成断言失败查。
三、报头版本差异:TestFailed 与 AssertionError
同一句 assert,不同 cocotb 版本的报头长相不同,含义却是同一件事:
| 版本 | 失败行报头 | 说明 |
|---|---|---|
| cocotb 1.8 及更早 | TestFailed: counter mismatch: got 1 expect 7 | 断言异常被包装成 TestFailed 再抛 |
| cocotb 1.9 起 / 2.x | AssertionError: counter mismatch: got 1 expect 7 | 原样透出 Python 原生异常类型 |
搜老资料时看到 TestFailed 不要慌——它不是另一种病,只是旧版给 AssertionError 套的外套。判断依据只有一条:traceback 最底下一行的异常类型名 + 你 assert 消息里的 got/expect 数字,报头外壳不影响读法。真正把 FAIL 分成两类的是异常类型:AssertionError(值不对,本篇)与 SimTimeoutError(等不到事件,见 cocotb TimeoutError),两者的排查方向完全不同,先看异常类型再选路。
四、RANDOM_SEED 复现:同 seed 两跑对照
带随机激励的测试最怕「这次挂、下次过」——cocotb 的设计是:不指定 seed 时每次随机选一个并打在日志头部(Seeding Python random module with 1234 这一行),指定 RANDOM_SEED=<n> 则激励序列逐比特锁定。验证方法极朴素:同 seed 连跑两次,断言失败的输出应当逐行一致。本文实验就是这么做的,第二次运行的关键段:
0.00ns INFO cocotb.initialize Seeding Python random module with 1234
0.00ns INFO cocotb.regression running test_assert.counter_wrong_expect (1/1)
30.00ns INFO cocotb.dut_counter q=1, expecting 7 (deliberately wrong)
30.00ns WARNING cocotb.regression test_assert.counter_wrong_expect failed
AssertionError: counter mismatch: got 1 expect 7
** TESTS=1 PASS=0 FAIL=1 SKIP=0 30.00 0.00 15706.30 **与 run1 逐行比对:失败时刻 30.00ns、got 1 expect 7、FAIL 计数完全一致,唯一差别的数字是 RATIO (ns/s)——那是墙钟速率统计,与仿真语义无关。工程用法:CI 挂了的用例,先把日志头部的 seed 抄下来,本地 make RANDOM_SEED=<那个数> 原地复现;复现不了(同 seed 结果漂移)说明测试里藏着 seed 之外的非确定源——random 模块以外的熵(如 os.urandom)、多协程调度时序依赖、或 DUT 里的 X 态传播,这本身就是一条重要线索。修复后别急着删 seed 固定:先用原 seed 验证「原来挂的现在过」,再放开随机跑一批确认没引入新问题。
调试动作按出现频率收敛成一张清单卡:
这张卡对应正文主线:前两步把「错在哪一侧」分开——异常类型决定排查方向,got/expect 对账决定怀疑对象;第三步对齐时间轴,时序逻辑读数差一拍是新手踩得最多的坑;第四、五步把随机性管起来,seed 先固定复现、修完再放开,整个过程可重复、可回归。照着走一遍,断言失败基本不会过夜。
五、预防与纪律
- assert 消息必带 got 与 expect:
assert got == exp, f"xxx: got {got} expect {exp}",没消息的断言等于没装仪表盘。 - 期望值尽量机器生成:参考模型或记分板算 expect,手算常量只用于冒烟;手算时把推导注释写在断言上一行。
- 读数在 ReadOnly 阶段做:
await ReadOnly()后再int(dut.x.value),根除「沿前/沿后」歧义。 - CI 记录 seed:回归脚本把每次的 seed 打进工件名,挂了能一键复现;修复验证先跑原 seed 再放开随机。
- FAIL 以回归表为准:
TESTS=n PASS=x FAIL=y是权威结论,make 退出码只是它的搬运工,别只看退出码 0/1。
实验环境
- 机器/系统:Mac mini M4 / macOS 15
- 工具版本:cocotb 2.1.0 / Icarus Verilog 13.0 (stable) / Python 3.13
- 日期:2026-10-26
- 完整命令:
make RANDOM_SEED=1234(同 seed 连跑两次,输出逐行比对)
代码获取:实验源码(dut.sv / test_assert.py / Makefile)与两次实跑的完整日志在公开仓 experiments/ts-cocotb-testfailed-assertion,装好 cocotb 与 iverilog 后 make RANDOM_SEED=1234 即可复现。
这一篇在知识体系里的位置
本篇是 EDA 排错系列 中「cocotb 验证平台」节点的 FAQ 页,与同系列 cocotb TimeoutError(等不到事件的超时失败)、cocotb NO_TEST_ERROR(用例根本没被收集到)构成 cocotb 三类高频失败的对照组——本篇处理「用例跑了、值不对」这一类,三者可按回归表里的异常类型直接分流。
一句话总结
cocotb 断言失败(旧版报头 TestFailed、新版直出 AssertionError)的读法就三要素:traceback 最后一帧的 got 是 dut.value 读出的 DUT 真实值、先信它,expect 来自你的参考模型或手算、先质疑它,再用日志头部的 RANDOM_SEED 固定激励原地复现——同 seed 两跑输出逐行一致即为确定性失败,修完先跑原 seed 确认由挂转过、再放开随机防回潮。