Files
STM32F4-Base/test/run_all_suites.py
T
wandering1 5a9988b557 chore(注释): 清掉源码里不可检索的过程编码
清掉的(读者无从解析、且所指文档已不存在)
- 已删任务文档的编号: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
2026-10-21 11:39:53 +08:00

287 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())