test:@test 闭环迁移 PowerShell→Python(run_boot_test/verify_log 纯标准库,文档同步)
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify_log.py - 对抓取的 MCU 调试日志做标记校验(纯标准库,2026-09-20 替代 verify_log.ps1)。
|
||||
|
||||
用于 @test 闭环:烧录后从串口助手日志提取的开机日志交给本脚本 grep 标记,
|
||||
输出结构化判定(格式与旧 PowerShell 版逐字一致,下游文档/习惯不受影响)。
|
||||
|
||||
用法:
|
||||
& '.venv\\Scripts\\python.exe' test/verify_log.py --log-path <file>
|
||||
[--required 'marker1;marker2']
|
||||
[--forbidden 'FAIL;ERROR;HardFault;assert']
|
||||
[--show-tail-lines 40]
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(所有 Required 标记找到,Forbidden 零命中)
|
||||
1 = FAIL(缺 Required 或命中 Forbidden)
|
||||
2 = ERROR(日志文件不存在/不可读)
|
||||
|
||||
默认 Required 标记对应 STM32F4-Base 启动序列(见 App/app_main.c + App/task/net_task.c),
|
||||
按测试用例覆盖,例如:
|
||||
--required 'App init OK;net init: OK;PHY: 100M FULL'
|
||||
--forbidden 'FAIL;ERROR;HardFault;assert'
|
||||
|
||||
语义约定(与旧 PowerShell 版对齐):
|
||||
- Required:大小写敏感子串匹配(全文)
|
||||
- Forbidden:大小写**不敏感**子串匹配、逐行计数(旧版 -like 语义;
|
||||
如 Forbidden 'FAIL' 会命中 "write /log/run.log fail (7)" 这类行)
|
||||
- 读取编码:先按 UTF-8,解码失败回退系统 locale 编码(旧版 Get-Content 行为)
|
||||
- 控制台输出重配置为 UTF-8(errors=replace):任何 Windows 控制台代码页
|
||||
都只影响终端显示,不会导致本脚本崩溃
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import locale
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REQUIRED_DEFAULT = "App init OK;Network Init;net init: OK"
|
||||
FORBIDDEN_DEFAULT = "FAIL;ERROR;HardFault;assert"
|
||||
|
||||
|
||||
def _split_list(spec):
|
||||
"""';' 分隔列表 → 去空白、去空项。"""
|
||||
return [p.strip() for p in spec.split(";") if p.strip()]
|
||||
|
||||
|
||||
def _read_lines(path):
|
||||
raw = Path(path).read_bytes()
|
||||
for enc in ("utf-8", locale.getpreferredencoding(False)):
|
||||
try:
|
||||
return raw.decode(enc).splitlines()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return raw.decode("utf-8", errors="replace").splitlines()
|
||||
|
||||
|
||||
def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
show_tail=40, out=print):
|
||||
"""标记校验核心(可被 run_boot_test.py 复用)。返回退出码 0/1/2。"""
|
||||
p = Path(log_path)
|
||||
if not p.is_file():
|
||||
out("VERIFY_ERROR: log file not found: {}".format(log_path))
|
||||
return 2
|
||||
try:
|
||||
lines = _read_lines(log_path)
|
||||
except OSError:
|
||||
out("VERIFY_ERROR: cannot read log file: {}".format(log_path))
|
||||
return 2
|
||||
|
||||
full = "\n".join(lines)
|
||||
req_list = _split_list(required)
|
||||
forb_list = _split_list(forbidden)
|
||||
|
||||
out("=== Log verdict: {} ===".format(log_path))
|
||||
out("Lines: {}, bytes: {}".format(len(lines), p.stat().st_size))
|
||||
|
||||
missing = []
|
||||
for m in req_list:
|
||||
if m in full:
|
||||
out("[OK] required marker found: {}".format(m))
|
||||
else:
|
||||
out("[MISS] required marker NOT found: {}".format(m))
|
||||
missing.append(m)
|
||||
|
||||
hits = []
|
||||
for f in forb_list:
|
||||
fl = f.lower()
|
||||
n = sum(1 for ln in lines if fl in ln.lower())
|
||||
if n > 0:
|
||||
out("[HIT] forbidden pattern: {} ({} line(s))".format(f, n))
|
||||
hits.append(f)
|
||||
else:
|
||||
out("[OK] forbidden pattern absent: {}".format(f))
|
||||
|
||||
if show_tail > 0 and lines:
|
||||
out("")
|
||||
out("=== Last {} line(s) of log ===".format(show_tail))
|
||||
for ln in lines[-show_tail:]:
|
||||
out(ln)
|
||||
|
||||
out("")
|
||||
if missing or hits:
|
||||
out("VERDICT: FAIL")
|
||||
return 1
|
||||
out("VERDICT: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
# 控制台输出固定 UTF-8:Windows 代码页(GBK/UTF-8 终端)均不会让本脚本崩溃
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ap = argparse.ArgumentParser(description="MCU debug log marker check (pure stdlib)")
|
||||
ap.add_argument("--log-path", required=True, help="boot log file to check")
|
||||
ap.add_argument("--required", default=REQUIRED_DEFAULT,
|
||||
help="required markers, ';' separated (case-sensitive)")
|
||||
ap.add_argument("--forbidden", default=FORBIDDEN_DEFAULT,
|
||||
help="forbidden patterns, ';' separated (case-insensitive)")
|
||||
ap.add_argument("--show-tail-lines", type=int, default=40,
|
||||
help="show last N log lines (0 = off)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
return verify(args.log_path, args.required, args.forbidden, args.show_tail_lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user