Files
STM32F4-Base/test/host/task_prof/task_prof_test.c
T
wandering1 b1291f1308 test(NETCFG,CFG): P1①四个主机单测模块,并修 net_cfg 的 WAIT 契约与定长错记
按 docs/测试说明/主机单测覆盖率口径.md §9 的清单吃 P1 第①批「配置与工具」四块,
新增 4 个主机单测模块共 104 条断言,全部 MSVC 编译通过并 PASS:
- test/host/net_cfg/(45 条):出厂默认可用、注册幂等与内容、load 报 -1 仍返回 0
  (RAM 恒有效契约)、跨任务保存交接状态机 IDLE/WAIT/OK/FAIL、MAC 三态优先级、
  IPv4 定长格式化。
- test/host/ftp_cfg/(16 条):[ftp] 段注册(**p_cb 必须为 NULL**,口令下次登录才
  生效)、默认口令判定大小写不敏感、NULL 一律判不等(fail-closed)。
- test/host/prot_set_store/(16 条):blob 返回码分流 1 无档 / 0 成功 / **2 换代
  (只打 INFO)** / -1 损坏;写盘失败必须回置脏标志(否则定值静默丢失)。
- test/host/task_prof/(27 条):无效样本闸门(防 DWT 复位把 max 抬到 2^32)、
  未 enter 就 exit 不记账、首次 5s + 每 2s 节流、环形缓冲与裸发两通道、
  周期→微秒换算随 SystemCoreClock 变。

覆盖率收益(已 --update-baseline 登记):套件 16 → 20 全绿;files_tested 19 → 23;
未触及 125 → 121 个文件(.c 47 → 43、9,657 → 9,228 行);分母 2,893 → 3,163 插桩
行;line=2811/3163(88.9%)、func=225/240(93.8%)。§9 的恒等式同步更新且仍咬合:
43 = 9(P1 剩) + 5(P2) + 29(P3)、9,228 = 2,188 + 2,226 + 4,814。

新单测照出并修掉两处真问题(都在 net_cfg,不是测试写错):
1. 实现违反自己的文档契约:net_cfg.h 明写"属主任务写盘中显示 WAIT",但
   net_cfg_save_run 为了不丢新请求会先清 pending 再写盘 → 写盘那几百毫秒里 UI
   保存行退回上一轮的 OK/IDLE(第一次保存按 ENTER 后屏幕上还是 ENTER),用户只
   会再按一次。补 s_save_busy 标志,TC-NETCFG-028/026/030 钉死(含失败路径必须
   清 busy,否则 UI 永久卡 WAIT)。
2. "定长 11 字符"是错的:%03u.%03u.%03u.%03u = 4 段×3 位 + 3 个点 = **15**。这个数
   错了 4 处(net_cfg.h、net_cfg.c、网络配置说明.md 两处)——是我照抄它才撞上
   FAIL,错在文档不是代码。已全部更正,并确认 len ≥ 16 的要求与 UI_BUF_STRLEN=32
   本来就没问题(无截断风险)。

桩的口径(沿用既有模块,不另起一套):真实头优先——cfg_file.h / cfg_file_store.h /
console_log.h / prot_set.h 全用真头(接口一漂移就编译期报错),只有 main.h 必须桩
(真实 main.h 拉整棵 HAL 头树,PC 上编不动;桩里字段名与位掩码宏名与 CMSIS 一致)。
prot_set_store 连真实 prot_set.c 一起编:blob 的编解码与脏标志在它里面,只有拿真
serialize 产出的 blob 才能造出"换代"与"损坏"两种真实形态,手搓等于测自己的桩。
配置引擎本身(解析/写盘/沉降)归 test/host/cfg_file 的 103 条,本批不重复测。

编号门禁:按 2026-10-21 批 3 的 B 层口径补建 4 族 TC-HOST-*(NETCFG 游标 056 /
FTPCFG 014 / PROTSTOR 016 / TASKPROF 020,代码即属主登记处)。这一步不是走形式:
建族之前文档里引用 TC-NETCFG-028 被判「台账之外新账」直接卡提交,建族后才通过;
编号规范 §8.4 变更记录加一行说明族数 9 → 13。id_registry.json 只做增量插入(+88 行,
无重排)。

自查(提交时 pre-commit run --all-files,**仅 3 条失败且都与本分支改动无关**):
- mem-numbers / mem-budget:本 worktree 因需验证代码改动而 @build 过,新生成的 .map
  与 docs/内存管理 受管快照漂移。实测增量 ER_IROM1 +12 B、RW_IRAM1 +8 B、Flash
  +16 B、**CCM 0**,水位百分比全不变(改动 = 1 个静态字节 + 几条指令)。按并行分支
  约定**不在分支里 --write**,台账由 Lead 合并后统一回填。
- ui-golden:simulator/tests/ref/*.bin 写在 .gitignore 里(黄金参考是本地生成物),
  新建的 worktree 天生没有,报的是"缺黄金参考"而非像素差异;本分支零 UI 文件改动。
- 其余全部 Passed,含 @build 0 错误 0 警告、test-cppcheck(它抓到过我一条 p!=NULL
  后又解引用的写法,已改为断言+显式守卫)、coding-standard、doc-links(35 处=存量)、
  id-check --strict、mojibake 与 BOM/CR/U+FFFD/行尾逐项卫生。
- 自审修掉自己写的两条恒真断言(形如 x==0?1:1 的假绿)与一个重复编号标签。
- 4 个新模块**尚未挂 pre-commit 钩子**:.pre-commit-config.yaml 是单写文件,任务工作
  树内不改;覆盖率脚本按 test/host/**/run_unit_test.py 自动发现,已实跑 20/20。
- 未跑 @test:改动仅 1 个静态字节 + UI 保存行的显示时序,无硬件语义变化。
2026-10-23 23:44:47 +08:00

377 lines
14 KiB
C
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.
/*
* 模块名称:任务耗时探针(task_prof)主机侧单元测试
* 模块功能:在 PC 上离线验证 App/util/task_prof.c 的**样本有效性闸门、enter/exit
* 配对、上报节流与通道选择、周期→微秒换算、glitch 汇总**。
*
* 被测对象:真实源文件 ../../../App/util/task_prof.c(#include 原样纳入)。桩:
* - stub/main.h:DWT/CoreDebug/SystemCoreClock/HAL_GetTick(本目录,
* 真实 main.h 拉整棵 HAL 头树无法在 PC 上编);
* - console_log.h 用**真实头**(App/log),两个出口函数
* dbg_console_write_line / dbg_console_write_raw 由本文件实现并**留档
* 内容**——报告行是要被人读的,所以断言直接盯字符串本身。
*
* 为什么要主机侧单测(而不是只靠真机日志):
* 这个模块是"性能问题的唯一现场证据",它自己错了所有 PROF 行就都是假的:
* 1. **无效样本闸门**(cyc==0 或 ≥ SystemCoreClock×10):漏了它,一次 DWT 复位
* 就把 max 抬到 ~2^32 周期,之后所有"最坏耗时"永久失真(真机上没人会去翻
* gl= 计数发现)。这条只能构造样本测。
* 2. **未 enter 就 exit 必须直接返回**:周期测量写成"先 exit 再 enter"时,首轮
* 会把 0 当起点,记下一次几秒的虚假超大样本——真机上表现为"偶发一条离谱
* PROF 行",查半年。
* 3. 节流契约(首次 5 s、之后每 2 s)与**两条通道的选择**(report 走环形缓冲、
* report_raw 走裸发):错了要么刷屏挤掉正事,要么停摆现场那一行发不出去。
* 4. 周期→微秒换算依赖 SystemCoreClock,改时钟树(比如 168→180 MHz)忘了同步
* 就会整表偏移;这条在真机上看不出来(没人对着示波器量 PROF)。
*
* 覆盖点:
* TC-TASKPROF-001 有效样本:count+1 / last=样本 / max=样本
* TC-TASKPROF-002 更小样本不改 max(max 只增不减)
* TC-TASKPROF-003 cyc=0 只累加 glitch,不动 count/last/max(计数器复位防线)
* TC-TASKPROF-004 cyc ≥ SystemCoreClock×10 同样只累加 glitch
* TC-TASKPROF-005 id 越界(=COUNT 与 +1)安全返回,不碰任何槽
* TC-TASKPROF-006 首次取时基即惰性使能 DWT(TRCENA + CYCCNTENA 读回)
* TC-TASKPROF-007 enter 置 in_seg 并记下起点
* TC-TASKPROF-008 exit 结算 Δ、清 in_seg、count+1
* TC-TASKPROF-009 **未 enter 直接 exit 不记账**(防首轮 0 起点造出假超大样本)
* TC-TASKPROF-010 重复 exit 不重复记账
* TC-TASKPROF-011 首次 5 s 之前一条都不发
* TC-TASKPROF-012 到点后发两行([PROF] + [PROF2]),走环形缓冲通道
* TC-TASKPROF-013 之后不足 2 s 不发(节流)
* TC-TASKPROF-014 满 2 s 再发两行
* TC-TASKPROF-015 report_raw 无视节流,恒发两行且走**裸发**通道
* TC-TASKPROF-016 行格式:以 [PROF]/[PROF2] 开头、以 \r\n 结尾
* TC-TASKPROF-017 周期→微秒换算按 SystemCoreClock(168 MHz 下 1680 周期 = 10 us)
* TC-TASKPROF-018 gl= 是所有槽 glitch 的汇总,不是单槽
* TC-TASKPROF-019 段内进行时的 n= 字段非零(此刻仍在段内的可视性)
* TC-TASKPROF-020 count 是累计值,上报不清零
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-23
*/
#include <stdio.h>
#include <string.h>
#include "console_log.h" /* 真实头:DBG_CON_LINE_MAX + 两个出口 */
#include "task_prof.h"
#include "task_prof.c"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* ===================== 桩实体 ===================== */
static tp_coredebug_t g_coredebug;
static tp_dwt_t g_dwt;
tp_coredebug_t *const CoreDebug = &g_coredebug;
tp_dwt_t *const DWT = &g_dwt;
uint32_t SystemCoreClock = 168000000u; /* 真机口径:168 MHz */
static uint32_t g_tick; /* HAL_GetTick 的可控值 */
uint32_t HAL_GetTick(void)
{
return g_tick;
}
/* 出口留档:两条通道分别计数;**每一行都单独留档**(一次上报会连发
* [PROF] 与 [PROF2] 两行,只存"最后一行"就没法断言第一行的字段) */
#define CAP_MAX 8
static int g_line_calls;
static int g_raw_calls;
static char g_line[CAP_MAX][DBG_CON_LINE_MAX];
static void cap(int slot, const char *src, unsigned len)
{
unsigned i;
if (slot >= CAP_MAX) {
return;
}
for (i = 0u; (i < len) && (i + 1u < DBG_CON_LINE_MAX); i++) {
g_line[slot][i] = src[i];
}
g_line[slot][i] = '\0';
}
int dbg_console_write_line(const char *line, unsigned len)
{
cap(g_line_calls + g_raw_calls, line, len);
g_line_calls++;
return (int)len;
}
void dbg_console_write_raw(const char *buf, unsigned len)
{
cap(g_line_calls + g_raw_calls, buf, len);
g_raw_calls++;
}
void dbg_console_writer_attach(void) { }
uint8_t dbg_console_writer_ready(void) { return 1u; }
/* ===================== 辅助 ===================== */
static void reset_all(void)
{
(void)memset(s_prof, 0, sizeof(s_prof));
(void)memset(&g_coredebug, 0, sizeof(g_coredebug));
(void)memset(&g_dwt, 0, sizeof(g_dwt));
s_dwt_ok = 0u;
s_prof_reported = 0u;
s_prof_last = 0u;
g_tick = 0u;
g_line_calls = 0;
g_raw_calls = 0;
(void)memset(g_line, 0, sizeof(g_line));
}
static void set_cyc(uint32_t v)
{
g_dwt.CYCCNT = v;
}
/* 从指定行的键上取整数值;键不存在或其后不是数字则返回 -1(断言因此会响)。
* 手写解析而不用 sscanf:后者在 MSVC 下触发 C4996(弃用告警),门禁输出要干净 */
static long field_of(int slot, const char *p_key)
{
const char *p;
long v = 0;
int digits = 0;
if (slot >= CAP_MAX) {
return -1L;
}
p = strstr(g_line[slot], p_key);
if (p == NULL) {
return -1L;
}
p += strlen(p_key);
while ((*p >= '0') && (*p <= '9')) {
v = (v * 10) + (long)(*p - '0');
p++;
digits++;
}
return (digits > 0) ? v : -1L;
}
/* ===================== 样本闸门 ===================== */
static void tc_sample_gate(void)
{
printf("-- TC-TASKPROF-001..005 sample gate --\n");
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 500u);
CHECK((s_prof[TASK_PROF_UI_PERIOD].count == 1u) &&
(s_prof[TASK_PROF_UI_PERIOD].last == 500u) &&
(s_prof[TASK_PROF_UI_PERIOD].max == 500u),
"TC-TASKPROF-001 有效样本记 count/last/max");
task_prof_sample(TASK_PROF_UI_PERIOD, 300u);
CHECK((s_prof[TASK_PROF_UI_PERIOD].max == 500u) &&
(s_prof[TASK_PROF_UI_PERIOD].last == 300u) &&
(s_prof[TASK_PROF_UI_PERIOD].count == 2u),
"TC-TASKPROF-002 更小样本不改 max");
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
(s_prof[TASK_PROF_UI_PERIOD].count == 0u) &&
(s_prof[TASK_PROF_UI_PERIOD].max == 0u),
"TC-TASKPROF-003 cyc=0 只累加 glitch(DWT 复位防线)");
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, SystemCoreClock * 10u);
CHECK((s_prof[TASK_PROF_UI_PERIOD].glitch == 1u) &&
(s_prof[TASK_PROF_UI_PERIOD].count == 0u),
"TC-TASKPROF-004 超上界样本只累加 glitch");
reset_all();
task_prof_sample((task_prof_id_t)TASK_PROF_ID_COUNT, 100u);
task_prof_sample((task_prof_id_t)(TASK_PROF_ID_COUNT + 1), 100u);
task_prof_enter((task_prof_id_t)TASK_PROF_ID_COUNT);
task_prof_exit((task_prof_id_t)TASK_PROF_ID_COUNT);
CHECK((s_prof[0].count == 0u) && (s_prof[TASK_PROF_ID_COUNT - 1].count == 0u),
"TC-TASKPROF-005 id 越界安全返回,不碰任何槽");
}
/* ===================== 时基与 enter/exit 配对 ===================== */
static void tc_enter_exit(void)
{
printf("-- TC-TASKPROF-006..010 dwt enable + enter/exit --\n");
reset_all();
set_cyc(1000u);
(void)task_prof_cyc();
CHECK(((g_coredebug.DEMCR & CoreDebug_DEMCR_TRCENA_Msk) != 0u) &&
((g_dwt.CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) &&
(s_dwt_ok == 1u),
"TC-TASKPROF-006 首次取时基即惰性使能 DWT");
reset_all();
set_cyc(2000u);
task_prof_enter(TASK_PROF_UI_WORK);
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 1u) &&
(s_prof[TASK_PROF_UI_WORK].t_start == 2000u),
"TC-TASKPROF-007 enter 置 in_seg 并记起点");
set_cyc(2640u);
task_prof_exit(TASK_PROF_UI_WORK);
CHECK((s_prof[TASK_PROF_UI_WORK].in_seg == 0u) &&
(s_prof[TASK_PROF_UI_WORK].last == 640u) &&
(s_prof[TASK_PROF_UI_WORK].count == 1u),
"TC-TASKPROF-008 exit 结算 Δ 并清 in_seg");
reset_all();
task_prof_exit(TASK_PROF_ADC_BEAT);
CHECK((s_prof[TASK_PROF_ADC_BEAT].count == 0u) &&
(s_prof[TASK_PROF_ADC_BEAT].glitch == 0u),
"TC-TASKPROF-009 未 enter 直接 exit 不记账(防 0 起点假超大样本)");
reset_all();
set_cyc(100u);
task_prof_enter(TASK_PROF_NET_POLL);
set_cyc(150u);
task_prof_exit(TASK_PROF_NET_POLL);
task_prof_exit(TASK_PROF_NET_POLL);
CHECK(s_prof[TASK_PROF_NET_POLL].count == 1u, "TC-TASKPROF-010 重复 exit 不重复记账");
}
/* ===================== 上报节流与通道 ===================== */
static void tc_report_throttle(void)
{
printf("-- TC-TASKPROF-011..015 report throttle + channels --\n");
reset_all();
g_tick = 4999u;
task_prof_report();
CHECK((g_line_calls == 0) && (g_raw_calls == 0), "TC-TASKPROF-011 首 5 s 前不发");
g_tick = 5000u;
task_prof_report();
CHECK((g_line_calls == 2) && (g_raw_calls == 0),
"TC-TASKPROF-012 到点发两行走环形缓冲");
g_tick = 6999u;
task_prof_report();
CHECK(g_line_calls == 2, "TC-TASKPROF-013 不足 2 s 不发");
g_tick = 7000u;
task_prof_report();
CHECK(g_line_calls == 4, "TC-TASKPROF-014 满 2 s 再发两行");
g_line_calls = 0;
g_raw_calls = 0;
task_prof_report_raw();
CHECK((g_raw_calls == 2) && (g_line_calls == 0),
"TC-TASKPROF-015 report_raw 无视节流且走裸发通道");
}
/* ===================== 行内容与换算 ===================== */
static void tc_line_format(void)
{
printf("-- TC-TASKPROF-016..020 line content --\n");
/* 016:一次上报连发两行;行首前缀是采集脚本的抓取依据,行尾必须是 CRLF */
reset_all();
g_tick = 5000u;
task_prof_report();
CHECK((g_line_calls == 2) &&
(strncmp(g_line[0], "[PROF] ", 7) == 0) &&
(strncmp(g_line[1], "[PROF2] ", 8) == 0),
"TC-TASKPROF-016 两行行首分别是 [PROF] / [PROF2]");
CHECK((strstr(g_line[0], "\r\n") != NULL) &&
(strstr(g_line[1], "\r\n") != NULL),
"TC-TASKPROF-016b 两行都以 CRLF 结尾");
CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c 未采样的槽打印 0");
/* 017:打印的是**微秒**,分母是 SystemCoreClock —— 断言落在打印字段上 */
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 1680u); /* 168 MHz → 10 us */
task_prof_sample(TASK_PROF_ADC_BEAT, 3360u); /* 168 MHz → 20 us */
g_tick = 5000u;
task_prof_report();
CHECK(field_of(0, "uip=") == 10L,
"TC-TASKPROF-017 168 MHz:1680 周期的 max 打印成 uip=10");
CHECK(field_of(0, "adcm=") == 20L,
"TC-TASKPROF-017b 同频:3360 周期的 max 打印成 adcm=20");
SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */
g_tick = 7000u;
task_prof_report();
CHECK(field_of(2, "uip=") == 20L,
"TC-TASKPROF-017c 84 MHz 下同一批周期数打印成 20");
SystemCoreClock = 168000000u;
/* 018:gl= 是**所有槽**的 glitch 汇总,不是当前槽 */
reset_all();
task_prof_sample(TASK_PROF_UI_PERIOD, 0u);
task_prof_sample(TASK_PROF_ADC_BEAT, 0u);
task_prof_sample(TASK_PROF_NET_POLL, 0u);
g_tick = 5000u;
task_prof_report();
CHECK(field_of(0, "gl=") == 3L, "TC-TASKPROF-018 gl= 汇总所有槽的 glitch");
/* 019:段未结算时,"此刻已在段内多久"要看得见(uipn / netn 两个字段) */
/* 字段口径 = 上报时刻的 CYCCNT 减去各自的进段时刻(**不是**样本值):
* UI_PERIOD 进段 @1000、NET_POLL 进段 @10000,上报时 CYCCNT=51680
* uipn = (51680-1000)/168 = 301 netn = (51680-10000)/168 = 248 */
reset_all();
set_cyc(1000u);
task_prof_enter(TASK_PROF_UI_PERIOD);
set_cyc(10000u);
task_prof_enter(TASK_PROF_NET_POLL);
set_cyc(51680u);
g_tick = 5000u;
task_prof_report();
CHECK(field_of(0, "uipn=") == 301L,
"TC-TASKPROF-019 UI_PERIOD 段内:uipn= 上报此刻已停留的 us");
CHECK(field_of(1, "netn=") == 248L,
"TC-TASKPROF-019b NET_POLL 段内:[PROF2] 的 netn= 同步可见");
CHECK(field_of(0, "adcn=") == 0L,
"TC-TASKPROF-019c 未进段的 ADC_BEAT 打印 0(不串用别的槽)");
CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u,
"TC-TASKPROF-019d 上报不消费段状态(只观测)");
/* 020:count 是累计量,两轮上报之间不能清零 */
reset_all();
task_prof_sample(TASK_PROF_UI_WORK, 100u);
g_tick = 5000u;
task_prof_report();
g_tick = 7000u;
task_prof_report();
CHECK((s_prof[TASK_PROF_UI_WORK].count == 1u) && (field_of(2, "uic=") == 0L) &&
(field_of(0, "uic=") == 0L),
"TC-TASKPROF-020 上报不清零(UI_WORK 不在 uic= 字段,仍为累计 1)");
}
/* ===================== 入口 ===================== */
int main(void)
{
printf("=== Task Prof Tests ===\n");
tc_sample_gate();
tc_enter_exit();
tc_report_throttle();
tc_line_format();
printf("\n=== Task Prof Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}