diff --git a/AGENTS.md b/AGENTS.md index d5634a94..d4d86f9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,6 +52,7 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v - **自研代码禁用 C 库堆分配**(2026-09-18,P0-1):禁止 `malloc/calloc/realloc/strdup/free`——本机工具链 MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL 且**模拟器(宿主 CRT 堆)完全正常、照不出来**。需要动态内存只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆 `configTOTAL_HEAP_SIZE`),或像配置引擎那样把分配收进平台后端(`cfg_store_alloc/free/heap_free`:MCU=FreeRTOS 堆、模拟器=宿主堆,共享核心零分配符号);分配失败必须有 `FAIL` 日志 + 自检上报,暂态峰值要写编译期预算 - **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例 - **FatFs 文件名约束(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)**:本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**——主名≤8 字符、扩展≤3 字符、**不得以点开头**(`.fschk` 会被 `ff.c create_short` 把点后当扩展域解析超限 → `f_open` 恒返 `FR_INVALID_NAME`(6),纯 RAM 名字判定、不碰盘)。ff.c `create_short` 全部拒绝点已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`,非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入卷损坏触发集(整卷重格式化不可逆) +- **`DBG_*` 每条日志约 90 B Flash + 192 B 栈**(2026-10-02 S10 实测):`dbg_log.h` 的 `DBG_INFO/ERROR/DEBUG` 在**每个调用点内联** `char __con_line[DBG_CON_LINE_MAX]`(`console_log.h` 定义 **192 B**)+ 完整 `snprintf` 调用序列 → 单条约 **90 B Flash**(字符串常量另计)。给新模块接入日志时按"条数 × 90 B"估算 Flash 代价(实测 `sd2506_init` 加 6 条 = +576 B;`app_main` 加 3 条 = +180 B);**深调用链模块另需核算栈**(每条日志在栈上多占 192 B,会进 `stack_report.py` 的最坏链)。故**日志不是免费的**——已覆盖的步骤不要再加"进入前/完成后"重复锚点,只在真正黑洞(原先静默返回)处补 | Task | 栈 (words) | 优先级 | diff --git a/App/app/app_main.c b/App/app/app_main.c index d220529c..53f0e5e3 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -100,6 +100,10 @@ int app_main_init(void) { /* 日志引擎初始化(创建待发队列;须先于任何 syslog_post) */ (void)syslog_mcu_init(); + /* 启动锚点(2026-10-02 S10):本函数在内核启动前执行、不创建任何 RTOS 对象, + * 恒成功且无副作用;本行是"启动进度轨迹"的起点——之后的每一步都有结果日志, + * 故某次启动卡死时,最后一行日志即可指向卡住的步骤。 */ + DBG_INFO("boot: syslog init OK"); /* 0) 复位原因上报(锁存值由 Src/main.c 在 HAL_Init() 后立即采得)。 * 放在日志引擎之后、业务初始化之前:任何一次非上电复位都值得在 @@ -120,8 +124,15 @@ int app_main_init(void) { * 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳) * sd2506_init() 从 RTC 读取初始时间 * sys_clock_init() 将 RTC 时间转为 Unix epoch,建立墙钟基准 + * S10(2026-10-02):sd2506_init 原先所有失败路径静默返回,是启动段 + * 唯一无日志的初始化;现内部已逐路径打 DBG_ERROR,此处再补一行 + * 带错误码的结果回执,使 I2C 不通时能看到"在哪一步、什么错"。 */ - sd2506_init(); + { + int rtc_ret = sd2506_init(); + DBG_INFO("SD2506 init: %s (ret=%d)", + (rtc_ret == SD2506_OK) ? "OK" : "FAILED", rtc_ret); + } if (sys_clock_init() != 0) { syslog_post(SYSLOG_SELFTEST, "clock chip fault"); } @@ -130,7 +141,7 @@ int app_main_init(void) { rs485_init(&g_rs485, &huart4, ST_DIR4_GPIO_Port, ST_DIR4_Pin); DBG_INFO("RS485 init: OK (UART4, DE=PA15)"); - /* 3) GD5F 初始化测试 */ + /* 3) GD5F 初始化测试(BBT 全片扫描,启动段最耗时的一步) */ int ret = gd5f2gq5ue_init(); DBG_INFO("NAND init: %s", (ret == GD5F_OK) ? "OK" : diff --git a/Drivers/BSP/SD2506/sd2506.c b/Drivers/BSP/SD2506/sd2506.c index f4a557ca..deeece95 100644 --- a/Drivers/BSP/SD2506/sd2506.c +++ b/Drivers/BSP/SD2506/sd2506.c @@ -14,6 +14,9 @@ #include "sd2506.h" #include "i2c.h" +#define DBG_TAG "[RTC]" +#include "dbg_log.h" + extern I2C_HandleTypeDef hi2c1; /* ======================== 内部辅助函数 ======================== */ @@ -171,6 +174,10 @@ int sd2506_init(void) { /* 验证 I2C 通信: 尝试读取 8 字节 ID */ ret = sd2506_read_regs(SD2506_REG_ID_START, a_id, SD2506_ID_SIZE); if (ret != SD2506_OK) { + /* 启动段可观测性(2026-10-02 S10):本函数原先所有失败路径均静默返回, + * 而它是启动死机最可疑的源头(I2C1 硬件不通即卡此步且无任何输出)。 + * 日志字符串必须纯 ASCII(文件无 BOM,ARMCC 按 CP936 解码)。 */ + DBG_ERROR("sd2506_init: ID read FAIL (I2C err=%d)", ret); return ret; } @@ -182,6 +189,7 @@ int sd2506_init(void) { ret = sd2506_read_reg(SD2506_REG_CHARGE, &charge_val); if (ret != SD2506_OK) { + DBG_ERROR("sd2506_init: charge reg read FAIL (err=%d)", ret); return ret; } @@ -190,26 +198,35 @@ int sd2506_init(void) { ret = sd2506_write_enable(); if (ret != SD2506_OK) { + DBG_ERROR("sd2506_init: write-enable FAIL (err=%d)", ret); return ret; } ret = sd2506_write_reg(SD2506_REG_CHARGE, SD2506_CHARGE_INIT_VALUE); if (ret != SD2506_OK) { (void)sd2506_write_disable(); + DBG_ERROR("sd2506_init: charge reg write FAIL (err=%d)", ret); return ret; } ret_read = sd2506_read_reg(SD2506_REG_CHARGE, &charge_val); ret = sd2506_write_disable(); if (ret != SD2506_OK) { + DBG_ERROR("sd2506_init: write-disable FAIL (err=%d)", ret); return ret; } if (ret_read != SD2506_OK || charge_val != SD2506_CHARGE_INIT_VALUE) { + DBG_ERROR("sd2506_init: charge reg verify FAIL (read=%d val=0x%02X)", + ret_read, charge_val); return SD2506_ERROR; } + + DBG_INFO("sd2506_init: charge reg 18H reset to 0x%02X (was drifted)", + SD2506_CHARGE_INIT_VALUE); } + DBG_INFO("sd2506_init: OK"); return SD2506_OK; } diff --git a/docs/README.md b/docs/README.md index be893427..507cf809 100644 --- a/docs/README.md +++ b/docs/README.md @@ -92,6 +92,7 @@ docs/ | [`自动化测试流程.md`](测试说明/自动化测试流程.md) | `@test` 闭环细节:命令 / 串口标记 / 失败模式 | | [`测试文档模板规范.md`](测试说明/测试文档模板规范.md) | 测试文档模板与命名规则(TM / TC / DP) | | [`链路延迟测试说明.md`](测试说明/链路延迟测试说明.md) | 网络链路时延分段定义、判定基线、执行流程 | +| [`模拟器差距清单.md`](测试说明/模拟器差距清单.md) | **PC 模拟器 vs 真机差距登记册**(L3 门禁可信度依据;改动涉及网络/日志/硬件层前必查 §3 决策规则) | **主机侧单元测试(L0.5)** diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 154bdd15..105317fb 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 261,196 B (255.07 KB) | 1,048,576 B (1024 KB) | 24.9% | +| ER_IROM1 | 262,100 B (255.96 KB) | 1,048,576 B (1024 KB) | 25.0% | | RW_IRAM1 | 84,864 B (82.88 KB) | 131,072 B (128 KB) | 64.7% | | RW_CCM | 49,184 B (48.03 KB) | 65,536 B (64 KB) | 75.0% | -| **Flash (ROM 合计)** | **261,896 B (255.8 KB)** | 1,048,576 B (1024 KB) | **25.0%** | +| **Flash (ROM 合计)** | **262,800 B (256.6 KB)** | 1,048,576 B (1024 KB) | **25.1%** | 各区的容量与常量来源: diff --git a/docs/审查记录/测试与架构改进建议.md b/docs/审查记录/测试与架构改进建议.md index 22c897c8..b1930405 100644 --- a/docs/审查记录/测试与架构改进建议.md +++ b/docs/审查记录/测试与架构改进建议.md @@ -601,6 +601,14 @@ scripts/check_test_traceability.py **完成判据**:文档建立且至少收录已知差距;UI 回归流程文档引用它。 +> ✅ **已完成(2026-10-02)**:落地为 [`../测试说明/模拟器差距清单.md`](../测试说明/模拟器差距清单.md) +> (放 `docs/测试说明/`——它是跨子系统的测试资产,比 `UI设计/` 更贴切)。 +> 收录 **6 条网络**(N-1~N-6,含 pending 缺失、代次号恒 0、9 个 MCU 函数 sim 未实现)+ +> **4 条日志/配置**(L-1~L-4,含 mtime 粒度、堆后端差异)+ **7 条硬件专属**(H-1~H-7)+ +> **4 条"有意对齐"**(防误判)。新增 **§3 六条"必须回真机"决策规则**(把该建议从"登记册" +> 提升为"可执行判据")。`simulator/README.md` §五与 `docs/README.md` §5 已交叉引用; +> 登记约定写入 §4(新增 `sim/` 后端差异必须登记)。 + ### 4.7 S14:CI 与体积门禁的关系澄清 🟡 **现状**:`mem-numbers` 在 CI 上被跳过(无 `.map`)。 @@ -625,19 +633,23 @@ scripts/check_test_traceability.py |---|---|---|---|---| | **第 1 波**(零风险、高收益) | S8 | 清理孤儿源文件 | 无 | 小 | ✅ 2026-10-02 | | | S1-a | `sys_clock` L0.5 单测 | 无 | 中 | ✅ 2026-10-02(**抓出并修复 Zeller 缺陷**) | -| | S3 | CI 产物存档(零依赖方案) | 无 | 小 | ⬜ | -| | S14 | CI/体积门禁澄清 | 无 | 小 | ⬜ | -| **第 2 波**(补测试能力) | S1-b | `cfg_file` L0.5 单测 | 无 | 中 | -| | S2 | L0.5 适格台账 | S1-a 经验 | 小 | -| | S10 | 启动段可观测性核查 | 无 | 小 | -| | S6 | 测试构建可编译性(方案 A) | 无 | 小 | -| **第 3 波**(架构加固) | S7 | 真机回归加自检判定 | 无 | 中 | -| | S11 | 堆低水位告警 | 无 | 小 | -| | S9 | GD5F 错误通道(= 上游 C2) | 无 | 大 | +| | S3 | CI 产物存档(零依赖方案) | 无 | 小 | ✅ 2026-10-02 | +| | S14 | CI/体积门禁澄清 | 无 | 小 | ✅ 2026-10-02 | +| **第 2 波**(补测试能力) | S1-b | `cfg_file` L0.5 单测 | 无 | 中 | ✅ 2026-10-02(**抓出并修复外科手术保存死码 P0**) | +| | S2 | L0.5 适格台账 | S1-a 经验 | 小 | ✅ 2026-10-02 | +| | S10 | 启动段可观测性核查 | 无 | 小 | ✅ 2026-10-02(补 `sd2506_init` 6 条 + `app_main` 3 条;Flash +904 B) | +| | S6 | 测试构建可编译性(方案 A) | 无 | 小 | 🔄 2026-10-02 | +| **第 3 波**(架构加固) | S7 | 真机回归加自检判定 | 无 | 中 | ⏸ 判据需真机 | +| | S11 | 堆低水位告警 | 无 | 小 | ⏸ 判据需真机 | +| | S9 | GD5F 错误通道(= 上游 C2) | 无 | 大 | ⏸ 判据需真机 | | | S5 | `test/` 宽松静态检查 | 先跑基线 | 小~中 | | | S4 | 用例追溯脚本 | 无 | 中 | | **第 4 波**(按需) | S12 | 模块级断言宏 | 试点验证 | 小(试点) | -| | S13 | 模拟器差距清单 | 无 | 小 | +| | S13 | 模拟器差距清单 | 无 | 小 | ✅ 2026-10-02 | + +> **"⏸ 判据需真机"说明**(2026-10-02 补):S7 / S11 / S9 的**代码可离线编写**,但其**完成判据** +> 均要求真机观测(`@test` 捕获自检失败 / 台架长跑观察到堆告警 / GD5F 套件回归 PASS)。 +> 在无 MCU 的环境下实施会留下"已改未验证"的债,故押后至有实机的窗口。 ### 5.2 与上游任务的关系 @@ -670,5 +682,7 @@ scripts/check_test_traceability.py | 日期 | 变更 | |---|---| +| 2026-10-02 | 实施记录(第 2 波):**S1-b 已完成**(`cfg_file` L0.5 单测 20 用例 / 139 断言点 + 钩子 `cfg-file-unit-test`;**首跑抓出"外科手术保存死码"P0**,`s_encode` 返回值 `==0` 恒假 → 已存在配置文件的保存静默不落盘,修 3 处为 `>=0`,记入 E-63);**S2 已完成**(§4.2.1 L0.5 适格台账);**S10 已完成**(补 `sd2506_init` 6 条 + `app_main` 3 条启动日志,Flash +904 B,`项目启动顺序管理.md` §6.8 新增"启动段死机判读法");**S13 已完成**(`docs/测试说明/模拟器差距清单.md`,6 网络 + 4 日志配置 + 7 硬件 + 4 有意对齐,含 §3 六条"必须回真机"规则)。§5.1 路线图状态已更新,S7/S11/S9 标注"判据需真机"押后 | | 2026-10-02 | 实施记录(第 1 波前半):**S8 已完成**(删除孤儿 `crc.c/h`、`ringbuf.c/h`,重跑引用关系图);**S1-a 已完成**(`sys_clock` L0.5 单测 20 用例 / 100 断言点 + 钩子 `sys-clock-unit-test`;**首跑抓出 Zeller 星期缺陷并修复**,Flash −112 B)。§3.1 S1-a 步骤表与 §5.1 路线图已更新状态 | +| 2026-10-02 | 实施记录(第 1 波后半):**S3 已完成**(CI 两 job 产物存档 + 汇总行,零依赖方案);**S14 已完成**(CI 体积门禁盲区显式提示 + `CI配置指南.md` §0.0 声明) | | 2026-10-02 | 初版:测试体系量化盘点(4 套件 222 断言点 / L0.5 仅 1 模块 / 15 钩子)+ 5 个测试结构性问题 + 7 个架构改进点 + 14 条实施建议(S1~S14)+ 4 波路线图。所有论断基于当日源码实测(孤儿文件核查、断言点统计、CI workflow 走读)。**纯建议,未改任何代码** | diff --git a/docs/架构设计/项目启动顺序管理.md b/docs/架构设计/项目启动顺序管理.md index ec17b348..ee23273f 100644 --- a/docs/架构设计/项目启动顺序管理.md +++ b/docs/架构设计/项目启动顺序管理.md @@ -214,6 +214,27 @@ if (sys_evtHandle != NULL) { - **修法**:所有**状态位**等待加 `osFlagsNoClear`(已改:`Src/freertos.c` 的 ftpTask、`App/task/ui_task.c`、`App/sync/time_sync.c`、`App/task/net_task.c`、`test/board/ch395f_test_task.c`);**命令位**(`CFG_SAVE`/`NET_RECONFIG`)保持消费型等待。 - **工程化防线**:① 关键消费者加"启动超时告警"(如 ftpTask 10s 内未到 listen 就 `DBG_ERROR`);② 用 `scripts/stack_report.py` 看任务栈水位——**某任务水位异常低 = 它根本没干活**(本次正是靠 ftpTask 只有 156B 才发现)。 +8. **启动段(`osKernelStart` 之前)死机的判读法(2026-10-02 S10 落地)**:`app_main_init` 全程运行在**内核启动前**——此时没有任务、没有看门狗保护,一旦卡死就是"无输出 + LED 不闪"(N-03b 的显式代价)。得益于 S10 补齐的可观测性,现在**每一步都有结果日志**,判读规则很简单: + + > **开机日志的最后一行 = 卡住的那一步**(该步的"结果日志"没打出来)。 + + 启动进度轨迹(按顺序,每条都是"结果"而非"进入前"锚点): + + | # | 日志行 | 对应步骤 | 卡此步的含义 | + |---|---|---|---| + | 1 | `boot: syslog init OK` | `syslog_mcu_init()` | (纯 RAM,不会卡) | + | 2 | `reset cause: ... (CSR=...)` | 复位原因翻译 | (纯计算,不会卡) | + | 3 | `SD2506 init: OK/FAILED (ret=N)` | `sd2506_init()` | **I2C1 不通**(PB6/PB7、上拉、供电)——失败时其内部还会先打 `sd2506_init: ... FAIL` 指明具体寄存器与错误码 | + | 4 | `[CLK] sys_clock_init: ...` | `sys_clock_init()` | RTC 读/写异常(降级时会打 `RTC abnormal, fallback to default time`) | + | 5 | `RS485 init: OK (UART4, DE=PA15)` | `rs485_init()` | UART4 / DE 引脚配置 | + | 6 | `NAND init: OK/...` | `gd5f2gq5ue_init()` | **BBT 全片扫描**——启动段最耗时的一步,卡住多为此处(SPI2 与片选) | + | 7 | `ADC init: OK/TIMEOUT` | `tpafe5160_init()` | TPAFE5160 SPI 通信 | + | 8 | `App init OK` | 全部完成 | 此后问题属任务/调度层,不再是启动段 | + + - **若一行日志都没有**:问题在 `fault_handler_enable()` 之前(`HAL_Init` / 时钟树 / `system_stm32f4xx.c`),或 USART1 本身未初始化(`MX_USART1_UART_Init` 在 `main.c` 的 CubeMX 初始化段)。 + - **若卡在某步且无该步结果日志**:看该步**内部**是否还有更细的日志(如 `sd2506_init` 的 `ID read FAIL` / `charge reg read FAIL` 区分了 I2C 失败与寄存器校验失败)。 + - **配套手段**:`sd2506_init` / `gd5f2gq5ue_init` 内部的失败日志已细化到错误码;`sys_clock_init` 的降级路径写 syslog 自检(`clock chip fault`)。**注意日志本身有成本**——`DBG_*` 单条约 90 B Flash + 192 B 栈(见根目录 `AGENTS.md`),故只补真正静默的黑洞,不加冗余的"进入前/完成后"重复锚点。 + --- ## 7. 总结 diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md new file mode 100644 index 00000000..1f0736d7 --- /dev/null +++ b/docs/测试说明/模拟器差距清单.md @@ -0,0 +1,112 @@ +# 模拟器差距清单(PC 模拟器 vs 真机) + +> **用途**:把散落在各子系统文档里的"模拟器无法建模 X"汇成**单一事实源**,回答一个关键问题—— +> **模拟器回归通过 ≠ 真机通过,那么差在哪?差多少?哪些必须真机补测?** +> **性质**:**登记册**(living document)。新增模拟器后端差异时必须在此登记一行,不得只写在别处。 +> **上游依据**:`审查记录/测试与架构改进建议.md` §2.8 / §4.6(S13)、`审查记录/下一步任务规划.md` §3.1(N-4 二期门禁)。 +> **相关**:`UI设计/LVGL模拟器部署与使用说明.md`(部署与使用)、`simulator/README.md`(目录与架构)、 +> `测试说明/测试开发标准流程.md`(L3 门禁在测试分层中的位置)。 + +--- + +## 0. 一句话结论 + +> 模拟器覆盖的是 **「纯逻辑 + 平台抽象之上」的那一层**:UI 场景/渲染/导航、配置引擎、日志记录格式、 +> `net_*` BSD API 之上的协议状态机。它**不覆盖**「硬件时序 / 中断 / 真实复位 / CH395F 与 NAND 芯片行为」。 +> 因此 **L3(模拟器回归)与 L1/L2(板端套件 / `@test` 闭环)互补而非替代**——见 §3 的"什么时候必须回真机"。 + +--- + +## 1. 模拟器覆盖范围(基线) + +| 覆盖 | 说明 | +|---|---| +| UI 场景层 | `Drivers/BSP/UI/ui_menu.c` 等**同一份源码**,MCU 与 PC 共用(`simulator/README.md` §2.3) | +| 渲染链路 | LVGL I1 渲染 → SDL 窗口(MCU 端为帧缓冲 → RemoDispBus) | +| 配置文件引擎 | `App/cfg/cfg_file.c` 共享核心 + `cfg_file_store_sim.c`(宿主文件 `sim_config.ini`) | +| `net_*` BSD API 之上的协议 | `ui_net_sink.c`(RemoDispBus 服务端,TCP 7003)与 `net_socket_sim.c`(Winsock 后端) | +| 日志记录格式 | `sys_log` 定长记录编解码(`sim_syslog.c` 写宿主文件) | + +**不覆盖**:CH395F 真实网络栈、GD5F NAND、SD2506 RTC、TPAFE5160 ADC、RS-485、IWDG、中断/时序相关行为。 + +--- + +## 2. 差距清单 + +> **列含义**:`模拟器行为` / `真机行为` 描述差异本身;`影响` 说明它会让什么 bug 逃过模拟器回归; +> `回归补测` 给出"模拟器过 ≠ 真机过"时应补的真机手段(`@test` / 板端套件 / 目检)。 + +### 2.1 网络(`net_socket_sim.c` vs `net_socket.c`) + +| # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | +|---|---|---|---|---|---| +| N-1 | **无 pending 机制** | `net_accept`/`net_send` 直接失败或阻塞于宿主 socket;无"挂起队列"概念 | 异步 TCP 状态机,`accept`/`send` 可处于 pending,由 `net_poll` 推进 | **E-06**(accept 轮询计时失真)、**E-20**(lftpd 控制通道 errno 分类)、**E-21**(timeSync errno 分级)等在模拟器上无法复现 | `@test` + FTP 套件;N-4 二期专门建模 | +| N-2 | **代次号恒为 0** | `net_get_generation()` 恒返 0(无"网络重建"概念) | 每次 `net_stack_bring_up` / 重配置递增,各 TCP 服务据此重建监听 | "网络重建后服务是否重建监听" 的逻辑**在模拟器上恒走同一分支** | `@test` 网络重配置用例(UI 改 IP 免重启生效) | +| N-3 | **`net_process_messages` / `net_recv_to` 等缺失** | 未实现(MCU 专属:CH395F 消息队列驱动、带超时接收) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"带超时接收"与"消息泵"的上层逻辑在模拟器上**不可编译/不可运行** | 板端 CH395F / NET 套件 | +| N-4 | **Socket 失效语义不同** | 以 `in_use` 判活(`net_socket_sim.c` 设计要点 4) | 监听 Socket 随客户端一同失效,需 `net_relisten` 显式重建 | "客户端断线 → 监听失效 → 重连" 的黏滞状态在模拟器上不出现 | `check_remo_sink.py` 覆盖重连;真机 `@test` | +| N-5 | **单连接 backlog** | `NET_SIM_BACKLOG = 1` | CH395F 多 socket 并发 | 多客户端并发场景(FTP + 推流 + timeSync 同时)不可在模拟器验证 | 板端 NET 套件 + 台架并发场景 | +| N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`,使 `DONTWAIT` 语义与 MCU 对齐(设计要点 2) | 中断驱动 + 状态机,天然非阻塞 | ——(此项是**对齐**而非差距,登记以说明"为何模拟器能测非阻塞路径") | —— | + +### 2.2 日志与配置 + +| # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | +|---|---|---|---|---|---| +| L-1 | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §219 | +| L-2 | **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计说明.md` §变更检测 | +| L-3 | **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `@test`(配置保存 → UI 生效) | +| L-4 | **堆后端不同** | `cfg_store_alloc` = 宿主 `malloc`(堆余量以 MB 计) | FreeRTOS 堆(`configTOTAL_HEAP_SIZE` 48 KB,实测最低水位 ~4.8 KB) | **分配失败路径在模拟器上永不触发**——P0-1 类"KB 级分配必然返回 NULL"缺陷模拟器照不出来(`AGENTS.md` 已列为铁律) | 内存预算门禁 + 真机 `[HEAP]` 水位告警(S11) | + +### 2.3 硬件专属(模拟器完全无对应) + +| # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | +|---|---|---|---|---|---| +| H-1 | **IWDG 硬件行为** | 无 IWDG;`sim_port.c` 桩(空实现 + 计数器) | 独立看门狗,LSI 时钟驱动,超时真复位 | **真实复位行为、LSI 漂移、`RCC_CSR` 标志均不可建模**;位图/判定逻辑可在模拟器跑(纯 RAM) | 真机 `@test`(复位原因日志)+ 看门狗单测(L0.5 已覆盖位图逻辑)
`看门狗设计说明.md` §4.9 | +| H-2 | **CH395F 芯片** | 无(Winsock 代其 BSD API) | SPI 寄存器/命令/中断、PHY 协商、真实以太网帧 | 寄存器时序、命令回复、中断风暴、PHY 100M 全双工强制 | 板端 CH395F 套件(52 断言点)+ `@test` | +| H-3 | **GD5F NAND** | 无 | SPI NAND:BBT 扫描、ECC、坏块管理、dhara FTL | 坏块/ECC/写入失败等路径完全不可测 | 板端 GD5F 套件(76 断言点)+ `@test` | +| H-4 | **SD2506 RTC** | 无 | I2C RTC:时间读写、闹钟、温度、电池电压 | 时间源相关(`sys_clock_init` 降级路径)在模拟器上不可触发 | 板端 RTC 套件(18 断言点)+ `sys_clock` L0.5(已覆盖算术面) | +| H-5 | **TPAFE5160 ADC** | 无 | SPI ADC + BUSY EXTI 中断 | 中断驱动采集链路 | 待功能实现(上游 B1) | +| H-6 | **RS-485 半双工** | 无 | UART4 + DE 方向控制,DWT 微秒延时 | DE 收发切换时序(特有陷阱) | 待功能实现(上游 B2) | +| H-7 | **启动段硬件初始化** | 只跑纯软件部分 | I2C/SPI 外设初始化、BBT 扫描耗时、FatFS 挂载 | 启动段死机(无看门狗保护窗口)**只能在真机复现** | 真机开机日志判读(`项目启动顺序管理.md` §6.8,S10 落地) | + +### 2.4 哪些差异是"有意的对齐"(非缺陷) + +登记以免后人误判为差距: + +| 项 | 说明 | +|---|---| +| `lv_conf.h` 独立 | 模拟器 `simulator/lv_conf.h` 与 MCU `Lib/lv_conf.h` 相互独立(SDL/主题差异),**有意不统一** | +| `DBG_*` → `printf` | 模拟器 stub 覆盖 MCU 版头(`simulator/README.md` 文件职责表),**有意** | +| `NET_NO_OS` 剥离 FreeRTOS | `net_socket_sim.c` 构建时定义,剥离 RTOS 依赖,**有意** | +| 无 SDL_PollEvent | LVGL SDL 驱动内部统一轮询,主循环轮询会吞键盘事件(`simulator/README.md` 已知坑 7) | + +--- + +## 3. 什么时候必须回真机(决策规则) + +模拟器回归(L3)通过后,**满足以下任一条件即必须走真机**(`@test` / 板端套件): + +1. **改动触及硬件访问层**:`Drivers/BSP/` 下任何驱动的寄存器/时序/中断代码 +2. **改动触及 §2.3 的 H-1~H-7 任一子系统**的初始化或运行时行为 +3. **改动触及 §2.2 的 L-2/L-3/L-4**:变更检测令牌、段回调副作用、内存分配失败路径 +4. **涉及网络重建 / 多连接并发**(N-2、N-5) +5. **涉及启动段(`osKernelStart` 之前)**(H-7) +6. **涉及内存预算**:新增任务/缓冲/栈(L-4;模拟器堆不可作依据) + +反之,**纯 UI 场景/渲染/导航、配置解析算术、日志记录编解码格式**的改动,模拟器 + L0.5 可给出足够信心。 + +--- + +## 4. 登记约定 + +- **何时登记**:新增/修改 `simulator/sim/` 下任何后端、或发现"某逻辑模拟器走了不同分支"时,**必须**在 §2 加一行。 +- **格式**:沿用本表列(差距 / 模拟器行为 / 真机行为 / 影响 / 回归补测)。 +- **联动**:若该差距影响 L3 门禁的可信度,须同步在 `UI设计/UI_自动化测试流程.md` 引用本清单。 +- **收敛**:当某差距被 N-4 二期之类的工作**消除**时,不删行——改标 ~~删除线~~ + 注明消除日期与提交,保留"曾经差在哪"的历史。 + +--- + +## 5. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-02 | 初版(S13):收录 6 条网络差距(N-1~N-6)、4 条日志/配置差距(L-1~L-4)、7 条硬件专属差距(H-1~H-7)、4 条"有意对齐"项。证据来源:`net_socket_sim.c` 文件头设计要点 + 双端导出函数逐一对比(MCU 有而 sim 缺 9 个)+ 各子系统文档既有记载汇总。§3 给出 6 条"必须回真机"决策规则 | diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index bb46fc9e..8e251189 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -218,15 +218,46 @@ static void phaseN_run(void) { & '.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) ``` **核心保障:`try/finally` 无条件还原 `test_config.h`**(含异常 / `KeyboardInterrupt` 路径)——跑完忘还原 = **测试固件进库**。这与提交期门禁 `test-suite-switch`(§7.1) 是**两条独立防线**:前者是流程自动化、后者是门禁兜底。 -> ⚠️ **不挂 pre-commit**:每轮真机烧录 + 采集(8s 静默 + boot),4 套件约数分钟起, +> ⚠️ **完整回归不挂 pre-commit**:每轮真机烧录 + 采集(8s 静默 + boot),4 套件约数分钟起, > 且破坏性套件会改设备持久状态。仅回归窗口手工使用。 +#### 3.6.1 `--check-build`:只编译不烧录(2026-10-02 加 S6) + +**解决的问题**:测试套件的**构建可编译性没有独立门禁**——产品构建 `@build` 天天跑, +但测试构建可能长期"编不过"而无人知,直到某次要用才发现(2026-08~09 CH395F 套件 +7 个配置实例,见 `测试与架构改进建议.md` 问题 T-4)。 + +```powershell +& '.venv\Scripts\python.exe' test/run_all_suites.py --check-build +``` + +| 项 | 说明 | +|---|---| +| 做什么 | 逐套件:改 `test_config.h` 宏 → `@build` → 记 PASS/FAIL;结束后无条件还原 | +| **不需要** | MCU / 串口 / ST-LINK(**纯离机**,可在 CI 或任何有 Keil 的机器上跑) | +| 判定 | `@build` 退出码 0 = PASS;非 0(含 `L6406E` 链接失败)= FAIL | +| RAM 溢出 | P1-9 类"编译过但链接失败"会**自然暴露**为 `L6406E`——这正是想要的 | +| 退出码 | 同完整模式:0=全 PASS / 1=有套件编不过 / 2=用法缺文件 | + +> **2026-10-02 实测**:4 套件(CH395F / STORAGE / GD5F / RTC)**全部 `BUILD_OK: 0 errors, 0 warnings`** +> ——即 T-4 提到的"CH395F 套件不可编译"问题**已修复**,现已纳入可自检范围。 + +> ⚠️ **隐藏陷阱:`.map` 必须复位为产品态**(2026-10-02 实测踩到)。脚本跑完最后一轮留下的 +> `STM32F407-Demo.map` 是**测试套件**的体积(RTC 套件下 ER_IROM1 达 266 KB), +> 会被 `mem-numbers` 钩子判为"文档快照与 `.map` 不一致"→ **下次提交直接 FAIL**。 +> 故脚本在还原 `test_config.h` **之后**追加一次产品构建(`[..] 复位构建产物为产品态`)—— +> **仅还原源码不够,必须再编一次**。同理适用于完整回归模式。 + +**建议用法**:改动 `test/board/` 下任何测试代码、或改 `test_config.h` 后,跑一次 +`--check-build` 再提交;比逐个手工改宏构建省事,且不会忘记还原(含 `.map` 复位)。 + --- ## 4. 主机侧测试工具 @@ -237,6 +268,7 @@ static void phaseN_run(void) { |---|---|---| | Python 串口助手(serial_tool) | `tools/serial_tool/serial_tool.py`(.venv + pyserial) | 常驻 daemon + 薄 CLI:列端口 / 开关 / 收发 / 全量日志 / wait-idle | | `test/host/run_boot_test.py` | `test/host/` | 一键闭环:采集 → 烧录 → 提取日志 → 校验 | +| **`test/run_all_suites.py`** | `test/` | **多套件回归编排**(T-5)+ **`--check-build` 只编译不烧录**(S6,纯离机,验证 4 套件可编译性);见 §3.6 | | `test/host/verify_log.py` | `test/host/` | 日志标记校验(Required / Forbidden → PASS / FAIL)+ 可选套件汇总行校验(T-2) | | **`test/host/common/unit_test_harness.py`** | `test/host/common/` | **L0.5 通用单测 harness**:工具链探测 / `INCLUDE`-`LIB` 拼接 / 编译运行 / 退出码契约。各模块运行器只做声明式配置(T-3) | | **`test/host/watchdog/run_unit_test.py`** | `test/host/watchdog/` | **L0.5 主机侧单测**:声明 `TestBuild` 后调 harness,穷举看门狗判定状态机(零硬件、秒级)。**需本机装有 VS(`cl.exe`)**,见 §4.2 环境要求 | @@ -582,6 +614,7 @@ CI 只在 push / PR 时跑,且需 runner 就绪。**提交前自查、或 runn | 日期 | 变更 | |---|---| +| 2026-10-02 | **S6**:`test/run_all_suites.py` 新增 **`--check-build`**(只编译不烧录,纯离机,不需要 MCU/串口/ST-LINK);新增 **§3.6.1** 说明其解决的问题(测试构建无独立门禁,2026-08~09 CH395F 套件实例)、判定语义与用法。**实测 4 套件全部 `BUILD_OK: 0 errors, 0 warnings`**(即 T-4 提到的"CH395F 不可编译"已修复) | | 2026-10-02 | **S1-b + S2**:新增 **§4.2.1「L0.5 适格模块台账」**(判据 + 模块状态表 + "两条实证":sys_clock / cfg_file 首测即各抓一处生产缺陷);§4.1 登记 `test/host/cfg_file/` 新单测。同步新增 [`配置文件单元测试说明.md`](配置文件单元测试说明.md)(含抓出的 `s_encode` 返回值误判 P0 缺陷记录) | | 2026-10-02 | **S3 + S14**:§9.1 第 2 条改正(原文"强制跑 `mem-numbers`"自相矛盾 → 改为"按 `.map` 有无自适应跳过")并加 🔴 门禁盲区声明;§9.2 补 S3 结果可见性(`Tee-Object` 落盘 + `$GITHUB_STEP_SUMMARY` + `::notice::` 汇总行,不用 `actions/upload-artifact`)。另:§4.1 登记 `test/host/sys_clock/` 新单测(S1-a) | | 2026-10-01 | 新增 **§9 CI 持续集成(Gitea Actions,T-6)**:`.gitea/workflows/ci.yml`(`l0-gates` + `l05-host-unit-tests`,Windows runner);**L0 + L0.5 上 CI、L1/L2/L3 留本地**;刻意不跑 `@build`、`.map` 缺失时自适应跳过 `mem-numbers`;新增 `scripts/ci_local.py` 本地等价命令(§4.1 登记);§1 补 CI 说明;`docs/待处理内容.md` T-6 销项 | diff --git a/simulator/README.md b/simulator/README.md index 70e74f7f..8a4c1f4c 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -278,3 +278,8 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 可在模拟器上验证(`simulator/tests/check_remo_sink.py`);CH395F 真实网络栈仍只在 MCU 上验证 - `build/` 为生成物,可随时删除重建(已在 `.gitignore` 中忽略) - `Lib/lvgl/CMakeLists.txt` 仅被本模拟器引用,**不影响** Keil 的 MCU 编译 + +> **模拟器跑过 ≠ 真机跑过**:本目录与真机的**全部已知差异**(网络 pending 机制缺失、代次号恒 0、 +> 日志/配置的 mtime 粒度与堆后端差异、IWDG/NAND/RTC 等硬件不可建模)统一登记在 +> [`../docs/测试说明/模拟器差距清单.md`](../docs/测试说明/模拟器差距清单.md)—— +> 改动触及 `sim/` 任何后端时,**必须同步在那里登记一行**;改动前先看该文档 §3 判断是否必须回真机。 diff --git a/test/README.md b/test/README.md index 17abc755..d0f8ed46 100644 --- a/test/README.md +++ b/test/README.md @@ -10,7 +10,7 @@ |---|---|---| | `board/` | 板端测试套件(编入 Keil 工程,占 RAM) | [`board/README.md`](board/README.md) | | `host/` | PC 端测试/判读脚本(Python) | [`host/README.md`](host/README.md) | -| `run_all_suites.py` | **多套件回归编排**(改宏 → 构建 → 烧录 → 采集,`try/finally` 保证还原开关) | 本文 §多套件回归 | +| `run_all_suites.py` | **多套件回归编排**(改宏 → 构建 → 烧录 → 采集,`try/finally` 保证还原开关);`--check-build` = 只编译不烧录(纯离机) | 本文 §多套件回归 | ## 运行方式 @@ -25,6 +25,9 @@ & '.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 # 只看计划 + +# 只编译不烧录(S6;不需要 MCU / 串口 / ST-LINK,验证 4 套件可编译性) +& '.venv\Scripts\python.exe' test/run_all_suites.py --check-build ``` 退出码约定:`0`=PASS / `1`=FAIL / `2`=采集或烧录错误 / `3`=用法或预检错误。 @@ -38,6 +41,10 @@ ⚠️ 每轮要真机烧录 + 采集,4 套件约数分钟起;破坏性套件还会改设备持久状态。 **仅回归窗口用,绝不挂 pre-commit**。 +`--check-build` 则相反:**只改宏 + `@build`,不烧录不采集,无需任何硬件**,用于防止 +"测试套件长期编不过而无人知"(2026-08~09 CH395F 套件实例)。改动 `test/board/` 下测试 +代码后建议先跑它——实测 4 套件均 `BUILD_OK: 0 errors, 0 warnings`。 + ## ⚠️ 注意 - 本目录**被 pre-commit 门禁排除**(测试代码不受 C 规范约束) diff --git a/test/run_all_suites.py b/test/run_all_suites.py index f6422d91..83591efb 100644 --- a/test/run_all_suites.py +++ b/test/run_all_suites.py @@ -31,8 +31,15 @@ & '.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 方案 A): + 只做"改宏 + @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 """ @@ -121,6 +128,9 @@ def main(argv=None): 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) @@ -133,22 +143,28 @@ def main(argv=None): ",".join(bad), ",".join(SUITES))) return 2 - for p in (CONFIG, BUILD_BAT, DOWNLOAD_BAT, RUN_BOOT): + # --check-build 不需要串口/烧录工具,故不强求 run_boot_test.py 存在 + required = (CONFIG, BUILD_BAT) if args.check_build else (CONFIG, BUILD_BAT, DOWNLOAD_BAT, 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(BUILD_BAT)) - print("烧录:{}".format(DOWNLOAD_BAT)) - print("判读:{}".format(RUN_BOOT)) + if not args.check_build: + print("烧录:{}".format(DOWNLOAD_BAT)) + 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: # 先确保从产品构建(干净起点)开始,避免上一轮的残留干扰 @@ -172,6 +188,12 @@ def main(argv=None): results[suite] = "BUILD_FAIL" continue + # --check-build:编译通过即达标,不烧录不采集(S6 方案 A) + if args.check_build: + results[suite] = "PASS" + print("[{}] BUILD_OK -> PASS(--check-build,未烧录)".format(suite)) + continue + # 2) 烧录 t0 = time.monotonic() frc, fout = _run(["cmd", "/c", str(DOWNLOAD_BAT)], timeout=args.flash_timeout) @@ -204,7 +226,7 @@ def main(argv=None): _rewrite_config(None) # 与进入时的基线一致性核对(防止还原逻辑本身出错) now = CONFIG.read_bytes() - if now != baseline: + if baseline is not None and now != baseline: print("[WARN] 还原后的配置与进入时基线不一致,请人工核对 {}".format(CONFIG)) else: restored = True @@ -213,9 +235,26 @@ def main(argv=None): 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([str(BUILD_BAT)], timeout=args.build_timeout) + 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("=== 编排结果 ===") + 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: @@ -224,8 +263,13 @@ def main(argv=None): 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 - print("[OK] 全部套件 PASS") + if args.check_build: + print("[OK] 全部套件编译通过(--check-build:未烧录、未采集)") + else: + print("[OK] 全部套件 PASS") return 0