5a9988b557
清掉的(读者无从解析、且所指文档已不存在) - 已删任务文档的编号:Task #32、其内部 §5 / §4.3、P1-5、P0-2、P1-4a、 "方案 a"、"Lead 批准 2026-10-12" - 正文里当代词用的文件版本号:"(v2.7 起为 AboveNormal)""已随 v2.0 事件 驱动改造删除""(v2.0:DMA 消费)""见文件头 v1.3 说明"等 → 改写成陈述现状 - 测试与脚本同类:adc_input 桩头"v2.0:DMA 模型"、stub/adc.h 的 v2.0、 check_cubemx_survival.py 段头的"2026-10-10 v2.8"、run_all_suites.py 的"方案 A" 刻意保留 - 可检索登记簿锚点:P-AUDIT 真条目(P0-1/P1-8/P2-1/P2-9/P3-11)、S6/S11、 E-31/E-44、N-18/N-23/N-32、B1/B5/B7/B8、L1-x~L5-x 功能号 - 文件头修改记录块内的 vN.M(规范 V1.0 §2.1/§2.2 要求保留该块;块内编号 在本文件内自洽可读) 顺带纠正注释与代码已经不一致的过时事实 - 仍把 TIM6 写成现行节拍源:Src/main.c 的 include 注释与回调注释、 Src/freertos.c 的 adcTask 块 → 改 TIM3;adc_input.h 修订条目里"ADC1 只认 TIM2/TIM3/TIM8 的 TRGO"不准确 → 换成指向判据(ADC采样节拍设计说明 §3.2) - App/algo/phasor.c 双区注释称"唯一消费者是 adcTask、为后续保护任务预留", 实际 protectTask 已跨上下文读(prot_elm_run -> phasor_get)→ 改为"读者跨 adcTask 心跳与 protectTask 判据两个上下文,勿改加锁、勿取消双区",并同步 修正 docs/算法/采集与算法层设计说明.md 同一句 - adc_tick_irq_notify() 的"拒绝 ISR 内计算"理由仍写"内含 2 ms 忙等"(DMA 化 后已无忙等)→ 换成三条真实理由:任务级共享数据 / 计算量 / 生成区回调 - 裸 §5(指代不明)与"规划 §1.6 / 规划 §4"昵称式 → 写成完整路径紧邻式 字节卫生(Edit 会静默剥 BOM) - App/task/ui_task.c 与 Drivers/BSP/UI/ui_net_sink.c 被剥掉 BOM 两次,均按 3 字节 EF BB BF 前缀补回并复验;全部 20 个文件 CR=0、U+FFFD=0、无行尾空白 门禁 - @build 0 错误 0 警告,且 Program Size 与清理前逐项相同 (Code=213356 RO=18580 RW=1184 ZI=131480)→ 纯注释改动、中文未被误重编码 - ui-golden 首跑红:新工作树缺被 gitignore 的黄金参考 simulator/tests/ref/ (不入库、只在主工作树生成过);从主工作树复制该基线后复跑通过 → 注释改动 零像素影响 - 全套 pre-commit 在这 20 个文件上 Passed(coding-standard / cppcheck / 6 个主机单测 / doc-links / task-table / mem-numbers / cubemx-survival / registration-check / mem-budget / font-charset / ui-golden / id-check) - id-check 可判定比例 82.71% -> 82.88%(删掉的是"形似 P-AUDIT 但查无定义"的 假锚点),悬空引用 863 -> 854
287 lines
14 KiB
Python
287 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""run_all_suites.py - 多套件回归编排(纯标准库)。
|
||
|
||
做什么(2026-10-01 测试架构评审 T-5):
|
||
套件互斥(`test_config.h` 的 `#error`)+ 编译期开关 → 跑 4 个套件的回归
|
||
本来要手工循环 4 轮(改宏 → 构建 → 烧录 → 采集 → 判读)。本脚本把它编排成一条命令:
|
||
|
||
对每个套件:
|
||
1. 改 test_config.h 启用该套件(其它全注释)
|
||
2. @build(0 错 0 警,否则跳过该套件)
|
||
3. 烧录(stlink-flash skill:stlink_flash.py flash,Reset & Run)
|
||
4. @test 采集 + 判读(.dsh/skills/serial-boot-test 的 verify_log.py,含套件汇总行校验 T-2)
|
||
结束后:
|
||
5. **无条件还原 test_config.h**(try/finally,含异常/KeyboardInterrupt 路径)
|
||
|
||
为什么要还原(这是本脚本存在的核心理由):
|
||
跑完一轮忘了还原 = **测试固件进库**(与 T-1 同源风险)。T-1 是"提交期门禁"兜底,
|
||
本脚本是"流程自动化"——两条防线各自独立。
|
||
|
||
⚠️ 代价提醒:
|
||
每轮要真机烧录 + 采集(8s 静默 + boot),4 套件 ≈ 数分钟起;破坏性套件还会改设备
|
||
持久状态。**仅回归窗口用,绝不挂 pre-commit。**
|
||
|
||
退出码:
|
||
0 = 全部套件 PASS
|
||
1 = 有套件 FAIL(构建失败 / 采集错误 / 标记判定失败)
|
||
2 = 环境/用法错误(缺文件、无法还原配置等)
|
||
|
||
用法(仓库根目录):
|
||
& '.venv\\Scripts\\python.exe' test/run_all_suites.py
|
||
& '.venv\\Scripts\\python.exe' test/run_all_suites.py --suites STORAGE,GD5F
|
||
& '.venv\\Scripts\\python.exe' test/run_all_suites.py --dry-run # 只打印计划
|
||
& '.venv\\Scripts\\python.exe' test/run_all_suites.py --check-build # 只编译不烧录(S6,无需 MCU)
|
||
& '.venv\\Scripts\\python.exe' test/run_all_suites.py --keep-config # 不还原(调试用,慎)
|
||
|
||
--check-build(S6 / 上游 P1-10):
|
||
只做"改宏 + @build",不烧录不采集,故**不需要 MCU / 串口 / ST-LINK**。
|
||
用途:① 改动测试固件或 test_config.h 后自检 4 套件仍可编译;
|
||
② 防止"某套件长期编不过而无人知"(2026-08~09 CH395F 套件实例)。
|
||
注意 RAM 溢出(P1-9)会自然暴露为链接错误 L6406E——这正是想要的。
|
||
|
||
设计依据:docs/任务规划/登记簿.md T-5;docs/测试说明/测试开发标准流程.md §3.1
|
||
创建日期:2026-10-01
|
||
"""
|
||
|
||
import argparse
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||
CONFIG = REPO_ROOT / "test" / "board" / "test_config.h"
|
||
KEIL_BUILD = REPO_ROOT / ".dsh" / "skills" / "keil-build" / "scripts" / "keil_build.py"
|
||
STLINK_FLASH = REPO_ROOT / ".dsh" / "skills" / "stlink-flash" / "scripts" / "stlink_flash.py"
|
||
RUN_BOOT = REPO_ROOT / ".dsh" / "skills" / "serial-boot-test" / "scripts" / "run_boot_test.py"
|
||
|
||
# 套件 -> (宏名, verify_log 需要的汇总行名称, 该套件的 Required 标记, --forbidden 覆盖)
|
||
# 汇总行名称与套件 `XXX_TEST_REPORT("...")` 里的字符串一致(见各模块 Test_Guide)。
|
||
# forbidden 覆盖:套件含"预期 [ERR]"的守卫负路径时须覆盖(空 = 用默认 `HardFault;assert;[ERR]`)。
|
||
# GD5F:TC-GD5F-704 故意用非 DMA 可达缓冲触发 `[ERR][NAND] RX DMA buffer not DMA-reachable`
|
||
# 回落轮询(用例 PASS)——按 verify_log.py 的说明去掉 [ERR],保留 HardFault/assert。
|
||
SUITES = {
|
||
"CH395F": ("TEST_SUITE_CH395F", None, "", ""),
|
||
"STORAGE": ("TEST_SUITE_STORAGE", "Storage Tests", "=== Storage Tests:", ""),
|
||
"GD5F": ("TEST_SUITE_GD5F", "GD5F2GQ5UE Tests", "=== GD5F2GQ5UE Tests:", "HardFault;assert"),
|
||
"RTC": ("TEST_SUITE_RTC", "RTC Tests", "=== RTC Tests:", ""),
|
||
}
|
||
|
||
# 套件名 -> 汇总行里的名称可能因模块而异,故上面的 summary 值仅为默认;实际以
|
||
# `--required` 给出的前缀做 Required 匹配,`--require-suite-summary` 做严格解析。
|
||
_ALL_MACROS = [m for m, _, _, _ in SUITES.values()]
|
||
|
||
|
||
def _rewrite_config(enable_macro):
|
||
"""把 test_config.h 里所有 TEST_SUITE_* 开关设为只启用 enable_macro(None = 全注释)。
|
||
|
||
只动"选择开关"行(`#define TEST_SUITE_<NAME>` / `//#define TEST_SUITE_<NAME>`),
|
||
不碰派生的 TEST_SUITE_SELECTED / TEST_SUITE_NAME。
|
||
按原换行符与 BOM 状态回写(仓库编码铁律)。
|
||
"""
|
||
raw = CONFIG.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
text = raw.decode("utf-8-sig") if bom else raw.decode("utf-8")
|
||
newline = "\r\n" if "\r\n" in text else "\n"
|
||
lines = text.split(newline)
|
||
|
||
switch_re = re.compile(r"^\s*(//)?\s*#\s*define\s+(TEST_SUITE_\w+)\s*$")
|
||
out = []
|
||
for ln in lines:
|
||
m = switch_re.match(ln)
|
||
if m and m.group(2) in _ALL_MACROS:
|
||
name = m.group(2)
|
||
if enable_macro is not None and name == enable_macro:
|
||
out.append("#define {}".format(name))
|
||
else:
|
||
out.append("//#define {}".format(name))
|
||
else:
|
||
out.append(ln)
|
||
|
||
data = newline.join(out).encode("utf-8")
|
||
if bom:
|
||
data = b"\xef\xbb\xbf" + data
|
||
CONFIG.write_bytes(data)
|
||
|
||
|
||
def _run(cmd, timeout=None):
|
||
"""跑子进程,返回 (rc, 输出文本)。输出按字节捕获后 UTF-8 兜底解码。"""
|
||
try:
|
||
cp = subprocess.run(cmd, capture_output=True, timeout=timeout,
|
||
cwd=str(REPO_ROOT), shell=isinstance(cmd, str))
|
||
except subprocess.TimeoutExpired as e:
|
||
blob = (e.stdout or b"") + (e.stderr or b"")
|
||
return -1, blob.decode("utf-8", errors="replace")
|
||
blob = (cp.stdout or b"") + (cp.stderr or b"")
|
||
return cp.returncode, blob.decode("utf-8", errors="replace")
|
||
|
||
|
||
def main(argv=None):
|
||
for stream in (sys.stdout, sys.stderr):
|
||
try:
|
||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
pass
|
||
|
||
ap = argparse.ArgumentParser(description="多套件回归编排(T-5,纯标准库)")
|
||
ap.add_argument("--suites", default=",".join(SUITES.keys()),
|
||
help="要跑的套件,逗号分隔(默认全部:{})".format(",".join(SUITES)))
|
||
ap.add_argument("--build-timeout", type=int, default=300)
|
||
ap.add_argument("--flash-timeout", type=int, default=300)
|
||
ap.add_argument("--dry-run", action="store_true", help="只打印计划,不改配置不构建不烧录")
|
||
ap.add_argument("--check-build", action="store_true",
|
||
help="只编译不烧录(S6/P1-10):逐套件改宏 + @build,验证 4 套件可编译性;"
|
||
"不需要 MCU / 串口 / ST-LINK,可用于 CI 或改动测试固件后自检")
|
||
ap.add_argument("--keep-config", action="store_true",
|
||
help="结束后不还原 test_config.h(调试用;⚠️ 会留下测试固件配置)")
|
||
args = ap.parse_args(argv)
|
||
|
||
py = sys.executable
|
||
picked = [s.strip().upper() for s in args.suites.split(",") if s.strip()]
|
||
bad = [s for s in picked if s not in SUITES]
|
||
if bad:
|
||
print("[ERROR] 未知套件:{}(可选:{})".format(
|
||
",".join(bad), ",".join(SUITES)))
|
||
return 2
|
||
|
||
# --check-build 不需要串口/烧录工具,故不强求 run_boot_test.py 存在
|
||
required = (CONFIG, KEIL_BUILD) if args.check_build else (CONFIG, KEIL_BUILD, STLINK_FLASH, RUN_BOOT)
|
||
for p in required:
|
||
if not p.is_file():
|
||
print("[ERROR] 缺文件:{}".format(p))
|
||
return 2
|
||
|
||
mode = "只编译(--check-build,不烧录)" if args.check_build else "完整回归(编译+烧录+采集)"
|
||
print("=== 多套件回归编排 ===")
|
||
print("模式:{}".format(mode))
|
||
print("套件:{}".format(", ".join(picked)))
|
||
print("构建:{}".format(KEIL_BUILD))
|
||
if not args.check_build:
|
||
print("烧录:{}".format(STLINK_FLASH))
|
||
print("判读:{}".format(RUN_BOOT))
|
||
if args.dry_run:
|
||
print("[DRY-RUN] 仅打印计划,未改动任何文件。")
|
||
return 0
|
||
|
||
results = {} # 套件 -> "PASS"/"FAIL"/"BUILD_FAIL"
|
||
restored = False
|
||
baseline = None # 进入时的 test_config.h 基线(try 内赋值;finally 依赖它,故先置 None)
|
||
|
||
try:
|
||
# 先确保从产品构建(干净起点)开始,避免上一轮的残留干扰
|
||
_rewrite_config(None)
|
||
baseline = CONFIG.read_bytes()
|
||
|
||
for suite in picked:
|
||
macro, summary_name, required, forbid = SUITES[suite]
|
||
print("")
|
||
print("################ {} ################".format(suite))
|
||
_rewrite_config(macro)
|
||
|
||
# 1) 构建(@build = keil-build skill;脚本内超时先于 _run 超时触发,
|
||
# 超时会返回 rc=1 + BUILD_FAIL 文案,而不是被 _run 杀掉)
|
||
rc, out = _run([py, str(KEIL_BUILD), "--timeout", str(args.build_timeout)],
|
||
timeout=args.build_timeout + 60)
|
||
tail = "\n".join(out.strip().splitlines()[-6:])
|
||
print("[BUILD] rc={}".format(rc))
|
||
if tail:
|
||
print(tail)
|
||
if rc != 0:
|
||
print("[{}] BUILD_FAIL -> 跳过本套件".format(suite))
|
||
results[suite] = "BUILD_FAIL"
|
||
continue
|
||
|
||
# --check-build:编译通过即达标,不烧录不采集(S6)
|
||
if args.check_build:
|
||
results[suite] = "PASS"
|
||
print("[{}] BUILD_OK -> PASS(--check-build,未烧录)".format(suite))
|
||
continue
|
||
|
||
# 2) 烧录
|
||
t0 = time.monotonic()
|
||
frc, fout = _run([py, str(STLINK_FLASH), "flash"], timeout=args.flash_timeout)
|
||
print("[FLASH] rc={} elapsed={:.1f}s".format(frc, time.monotonic() - t0))
|
||
if frc != 0:
|
||
print("\n".join(fout.strip().splitlines()[-6:]))
|
||
# 烧录失败不直接判死:板子可能已复位,交由采集+判读决定(与 @test 一致)
|
||
print("[WARN] 烧录 rc={}(继续:板子可能已复位)".format(frc))
|
||
|
||
# 3) 采集 + 判读(含套件汇总行校验 T-2)
|
||
# 烧录统一走 stlink-flash skill(与 .serialtest/config.json 的 flash_cmd 同源)
|
||
flash_cmd = '"{}" "{}" flash'.format(py, STLINK_FLASH)
|
||
cmd = [py, str(RUN_BOOT), "--flash-cmd", flash_cmd]
|
||
if required:
|
||
cmd += ["--required", required]
|
||
if summary_name:
|
||
cmd += ["--require-suite-summary", summary_name]
|
||
# 套件构建预期可能出现 [ERR](守卫负路径)——由套件自身的 forbid 覆盖决定
|
||
# (空 = 默认 HardFault;assert;[ERR];GD5F = HardFault;assert,见 SUITES 注释)
|
||
cmd += ["--forbidden", forbid or "HardFault;assert;[ERR]"]
|
||
rc, out = _run(cmd, timeout=args.flash_timeout + 120)
|
||
print("\n".join(out.strip().splitlines()[-8:]))
|
||
results[suite] = "PASS" if rc == 0 else "FAIL"
|
||
print("[{}] -> {}".format(suite, results[suite]))
|
||
|
||
finally:
|
||
# 4) 无条件还原(本脚本存在的核心理由)
|
||
if args.keep_config:
|
||
print("")
|
||
print("[WARN] --keep-config:未还原 test_config.h(⚠️ 可能留下测试固件配置)")
|
||
else:
|
||
try:
|
||
_rewrite_config(None)
|
||
# 与进入时的基线一致性核对(防止还原逻辑本身出错)
|
||
now = CONFIG.read_bytes()
|
||
if baseline is not None and now != baseline:
|
||
print("[WARN] 还原后的配置与进入时基线不一致,请人工核对 {}".format(CONFIG))
|
||
else:
|
||
restored = True
|
||
print("")
|
||
print("[OK] 已还原 test_config.h 为产品构建(全部 TEST_SUITE_* 注释)")
|
||
except Exception as e: # noqa: BLE001 - 还原失败必须显式报出
|
||
print("[ERROR] 还原 test_config.h 失败:{}".format(e), file=sys.stderr)
|
||
|
||
# 5) 复位构建产物为产品态(2026-10-02):最后一轮构建留下的是**测试套件**的
|
||
# .map/.axf,会污染 mem-numbers 体积门禁(下次提交即 FAIL)。仅还原源码不够
|
||
# ——必须再编一次产品固件,使 .map 与 test_config.h 一起回到产品态。
|
||
# --check-build 也要做:它每轮都 @build,同样会留下测试态 .map。
|
||
if restored and not args.keep_config:
|
||
print("")
|
||
print("[..] 复位构建产物为产品态(重建一次产品固件,避免 .map 停留在测试套件态)...")
|
||
rc, out = _run([py, str(KEIL_BUILD), "--timeout", str(args.build_timeout)],
|
||
timeout=args.build_timeout + 60)
|
||
tail = "\n".join(out.strip().splitlines()[-4:])
|
||
if rc == 0:
|
||
print("[OK] 产品固件已重建(.map/.axf 复位)")
|
||
else:
|
||
# 不判死:配置已还原,仅提示手工重建
|
||
print("[WARN] 产品固件重建失败(rc={}),请手工跑 @build 复位 .map".format(rc))
|
||
if tail:
|
||
print(tail)
|
||
|
||
# 汇总
|
||
print("")
|
||
print("=== 编排结果{} ===".format("(--check-build)" if args.check_build else ""))
|
||
for s in picked:
|
||
print(" {:<8} {}".format(s, results.get(s, "SKIPPED")))
|
||
if not restored and not args.keep_config:
|
||
return 2
|
||
|
||
failed = [s for s, r in results.items() if r != "PASS"]
|
||
if failed:
|
||
print("[FAIL] 未通过:{}".format(", ".join(failed)))
|
||
if args.check_build:
|
||
print(" (--check-build:失败 = 该套件编译不过,需修测试固件代码)")
|
||
return 1
|
||
if args.check_build:
|
||
print("[OK] 全部套件编译通过(--check-build:未烧录、未采集)")
|
||
else:
|
||
print("[OK] 全部套件 PASS")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|