仿真日志解析 Python 模板:提取失败种子并生成回归汇总
更新时间:2026-10-31。这是芯片设计入门的回归辅助模板,适合已经能跑单个测试、准备批量收集结果的读者。它不绑定任何仿真器,只要求测试结束时写出一行固定格式的结果;测试结果本身仍应由 记分板 SV 模板 这类自检逻辑产生。
首屏答案:让每条用例输出 RESULT test=<名字> seed=<正整数> status=<PASS|FAIL>,脚本只解析这种结果行,普通信息日志原样忽略。最终表格保留行号、用例、种子和状态,另列出 FAILED_SEEDS 与 SUMMARY。默认退出码 0 只表示日志解析成功;传入 --fail-on-test 后,存在失败用例返回 1;文件缺失、没有结果或格式异常返回 2。
本文要回答的问题
- 为什么先约定单行结果格式,比直接全文搜索
FAIL更可靠? - 怎样从日志生成失败用例与种子的可复现清单?
- 解析成功、测试失败和输入损坏为什么要使用不同退出码?
- 如何处理文件缺失、非法种子和完全没有结果行的日志?
一、仿真日志解析 Python 模板先约定结果契约
随机回归日志往往混有编译信息、波形提示、错误堆栈和进度输出。直接数全文中的 PASS,可能把说明文字也算进去;只搜 FAIL,又拿不到对应的用例名和种子。更稳的办法是把「给人看的详细日志」与「给脚本读的结果记录」分开,每条结束用例只写一行结构化结果。
读这张图要区分两类失败:橙色步骤处理的是设计回归中的失败用例,它们仍然属于一份格式正确的日志;紫色步骤则同时关心脚本输入是否可信。种子必须与状态在同一行解析,避免错误段和用例段跨行配对时发生错位。
本文样例是一份手工构造的通用仿真输出,不代表某个仿真器或 UVM 的真实报告。接入现有环境时,只需在测试结束处增加同样的结果行,不必修改其他日志。
[INFO] regression started; only RESULT lines are parsed
RESULT test=reset_smoke seed=104729 status=PASS
RESULT test=fifo_random seed=8675309 status=FAIL
[ERROR] scoreboard mismatch at cycle 418
RESULT test=burst_boundary seed=31337 status=PASS
RESULT test=backpressure seed=424242 status=FAIL
[ERROR] timeout waiting for output valid
RESULT test=idle_recovery seed=9001 status=PASS
[INFO] regression finished| 字段 | 允许形式 | 作用 |
|---|---|---|
test | 字母、数字、下划线、点、冒号、连字符 | 定位测试,作为复现命令的用例参数 |
seed | 不含前导零的正十进制整数 | 重现同一份随机激励 |
status | 只接受 PASS 或 FAIL | 防止未知状态被静默当成通过 |
| 行号 | 由解析器自动记录 | 回到原日志查看邻近错误详情 |
如果环境允许种子为 0,可以把正则中的种子分支改为 0|[1-9][0-9]*,但要让整个回归体系保持一致。这里的严格格式是有意设计:宁可停止并指出坏行,也不要在字段缺失时打印一张看似完整的汇总表。
二、完整可复制的解析模板
下面就是全部实现,只使用 Python 3 标准库。保存为 parse_sim_log.py 后执行 python3 parse_sim_log.py <日志路径> 即可;不需要安装第三方包,也不依赖仿真器 Python 接口。
#!/usr/bin/env python3
"""Parse simulator RESULT lines and summarize test failures and seeds."""
import argparse
import re
import sys
from pathlib import Path
RESULT_RE = re.compile(
r"RESULT test=(?P<test>[A-Za-z0-9_.:-]+) "
r"seed=(?P<seed>[1-9][0-9]*) status=(?P<status>PASS|FAIL)"
)
class LogFormatError(ValueError):
"""The input contains no result or has a malformed RESULT line."""
def parse_log(path):
records = []
with path.open("r", encoding="utf-8") as stream:
for line_number, raw_line in enumerate(stream, start=1):
line = raw_line.strip()
if not line.startswith("RESULT"):
continue
match = RESULT_RE.fullmatch(line)
if match is None:
raise LogFormatError(
"line {}: expected RESULT test=<name> seed=<positive-int> "
"status=<PASS|FAIL>".format(line_number)
)
records.append((line_number, match.groupdict()))
if not records:
raise LogFormatError("no RESULT records found")
return records
def print_summary(records):
print("{:>5} {:<24} {:>10} {}".format("LINE", "TEST", "SEED", "STATUS"))
for line_number, fields in records:
print("{:>5} {:<24} {:>10} {}".format(
line_number, fields["test"], fields["seed"], fields["status"]
))
failures = [item for item in records if item[1]["status"] == "FAIL"]
failed_seeds = ", ".join(
"{}({})".format(item[1]["test"], item[1]["seed"])
for item in failures
) or "-"
print("FAILED_SEEDS: {}".format(failed_seeds))
print("SUMMARY total={} pass={} fail={}".format(
len(records), len(records) - len(failures), len(failures)
))
return len(failures)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("log", type=Path, help="simulator log to parse")
parser.add_argument(
"--fail-on-test",
action="store_true",
help="return 1 when at least one test failed",
)
args = parser.parse_args()
try:
records = parse_log(args.log)
except (OSError, UnicodeError, LogFormatError) as error:
print("INPUT_ERROR {}: {}".format(args.log, error), file=sys.stderr)
return 2
failure_count = print_summary(records)
if args.fail_on_test and failure_count:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())fullmatch 要求整行都符合约定,因此 seed=abc、多出字段或状态拼错都会报错。line.startswith("RESULT") 则保证坏结果行不会被当作普通噪声跳过。逐行读取不需要一次把整个日志放进内存,适合长回归;失败列表只保存匹配后的记录,而不是原始日志全文。
模板按出现顺序保留重复用例。这对「同名测试跑多个种子」很有用,每一行仍是一条独立结果。如果后续要做跨日志趋势统计,可以把 records 中的字典写入 CSV 或数据库,但不要在这里按用例名直接去重,否则会丢掉其他种子的失败。
三、汇总结果与退出码怎么接回归
默认模式面向人工查看:只要日志完整可读,即使里面有失败用例,解析器仍返回 0。持续集成需要让失败阻断流程时加 --fail-on-test,此时返回 1。返回 2 专属于输入问题,调用方可以据此选择重新收集日志,而不是盲目重跑设计。
| 退出码 | 触发条件 | 调用方应做什么 |
|---|---|---|
| 0 | 至少一条合法结果,默认模式解析完成 | 读取表格与失败种子;不表示全部用例通过 |
| 1 | 指定 --fail-on-test 且至少一条 FAIL | 用提取出的用例名和种子复现 |
| 2 | 文件不可读、编码错误、坏结果行或无结果 | 检查仿真是否结束及结果契约是否实现 |
例如失败行给出 fifo_random(8675309),复现时应把这两个值同时交回原来的运行器。具体参数名由你的仿真环境决定,可能是 +TEST=fifo_random +SEED=8675309,也可能是脚本选项;本文不假定某一种命令格式。仅重跑同名测试而不固定种子,随机激励可能改变,原问题不一定再次出现。
还应区分「脚本没有跑到结尾」和「全部通过」。日志完全没有任何 RESULT 时返回 2,就是为了防止空文件被汇总成 pass=0 fail=0 后误放行。若一个回归应固定产生 100 条结果,可在更上层再检查 total=100,模板本身不猜测应有用例数。
四、实际运行:失败种子、统计与异常输入
运行脚本先做 py_compile 语法编译检查,再解析五条样例结果;随后分别验证失败回归、格式异常和文件缺失三种状态。输出中的测试状态由样例日志决定,不调用也不冒仿真器结果。
表格显示 reset_smoke、burst_boundary 和 idle_recovery 通过,fifo_random 与 backpressure 失败。FAILED_SEEDS 把复现需要的两组值收拢到一行,SUMMARY total=5 pass=3 fail=2 则可直接用于回归摘要。注意 SAMPLE_PARSE_EXIT=0 与两个失败用例并不矛盾:前者描述解析任务,后者描述测试结果。
FAILED_SEEDS: fifo_random(8675309), backpressure(424242)
SUMMARY total=5 pass=3 fail=2
SAMPLE_PARSE_EXIT=0
FAIL_ON_TEST_EXIT=1 (expected 1)
INPUT_ERROR malformed.log: line 2: expected RESULT test=<name> seed=<positive-int> status=<PASS|FAIL>
MALFORMED_INPUT_EXIT=2 (expected 2)
INPUT_ERROR missing.log: [Errno 2] No such file or directory: 'missing.log'
MISSING_INPUT_EXIT=2 (expected 2)
RUN_SH_RESULT=PASSmalformed.log 故意把种子写成 abc,解析器指出第 2 行;不存在的 missing.log 由文件异常路径返回同样的输入错误码。最后的 RUN_SH_RESULT=PASS 只说明运行脚本观察到了全部预期退出码,并不表示样例回归全部通过。
实验环境
- 机器/系统:Mac(x86_64)/ Darwin 25.5.0
- 工具版本:Python 3.13.7,仅使用标准库
- 日期:2026-10-31
- 完整命令:
python3 -m py_compile parse_sim_log.py;python3 parse_sim_log.py sample_sim.log;python3 parse_sim_log.py --fail-on-test sample_sim.log;python3 parse_sim_log.py malformed.log;python3 parse_sim_log.py missing.log;bash run.sh
代码获取
完整解析器、正常与异常样例日志、运行脚本位于公开仓 https://github.com/derekzhuo/geek-doc.cn/tree/main/experiments/tpl-python-log-parser。进入该目录执行 bash run.sh 可复现本文结果;接入自己的环境时修改 sample_sim.log 或替换命令中的日志路径即可。
这一篇在知识体系里的位置
本篇解决回归完成后的结果整理,不负责判断单个事务是否正确。事务比对可从 记分板 SV 模板 开始;被测对象若是顺序缓冲,可参考 同步 FIFO Verilog 模板。三层各自负责自检、结果输出和跨用例汇总,失败种子才能成为真正可执行的复现入口。
总结
仿真日志解析 Python 模板的核心是固定 RESULT 单行契约、严格校验整行、保留失败用例与种子,并输出通过失败统计。五条样例结果实跑得到 3 个通过、2 个失败及两组复现信息;默认解析返回 0,--fail-on-test 检出失败返回 1,缺失或格式异常返回 2。把设计失败与输入损坏分开,回归脚本才能给出可信的下一步动作。