Files
STM32F4-Base/test/host/task_prof/task_prof_test.c
T
wandering1 924ac3d428 test(host): 断言字面量统一纯 ASCII,30 文件 1535 行
用户裁定「统一」。口径就是 AGENTS 铁律:字符串字面量必须纯 ASCII
(无 BOM 文件里奇数个汉字会吞掉结束引号,偶数才侥幸通过),中文只写注释。
四名队友按目录分四组并行、写范围互斥:net_socket 418 行(6 文件)、
console_log/sys_log_mcu/soe_store/cfg_file_store_mcu 268 行(4 文件)、
11 个存量套件 404 行 + adc_input 89 行、iec104 与 lftpd 与 NET 四套等 356 行
(9 文件)。runner 的 .py 文档串不在口径内(Python 源不经 ARMCC 代码页)。

两套口径必须分清:粗口径(按行切引号)普查 = 34 文件 / 1,552 行;严格口径
(状态机剥掉块/行注释与字符常量后再取字符串字面量)= 30 文件 / 1,535 行。
差值那 4 个文件的命中全在注释里(ns_peers.c、net_socket/stub/ns_rtos.h、
common/ch395f_fake.h、common/fatfs_fake.c)——按口径一处都不该改,改了等于
把中文从注释里搬走、正好违背口径。粗口径绝不能当交付判据,否则要么白改
注释、要么留下「这行为什么没改」的悬案。

Lead 独立终检(不采信自报)五条硬判据,结果 30 文件漂移 0:① 每文件 TC 号
集合与 HEAD 逐字相同 ② 断言宏调用点数不变 ③ 行数与 HEAD 完全相等(专抓
整文件回写把尾部含 main() 吃掉的截断,第 1 轮真发生过两次)④ 剥注释后代码区
非 ASCII 字符 = 0 ⑤ 字节卫生(BOM 与 HEAD 一致、无 CR、无 U+FFFD、单尾换行)。
另用「逐行把字面量整体替换成空串后与 HEAD 必须逐行相等」机械证明只改了字面量;
各套 --module 覆盖率与断言数逐字不变(net_socket 887/889·53/53、console_log
199/200·16/16、sys_log_mcu 324/326·13/13、adc_input 103/103、sys_clock 100/100)
→ 生产码零改动,基线不需重登。

闸拦下一处真实回归(我的验收器没抓到):ns_s6_edges.c 翻译时把一个上一轮
已整改成平实描述的编号又写回了 CHECK 标签。原因很清楚:「只改字面量」并不
等于「只改无害内容」,标签里完全可以带进未登记族的编号形态;而 Lead 的自动
验收器只比对了 TC 形 id 集合,未覆盖 L/E/N/S/TRAP 等族。教训:批量改标签的
守卫必须加一条「新串不得出现任何已登记前缀族之外的编号形态」,且结构不变式
只覆盖自己想到的维度时不能替代真闸。
Lead 自查另清掉 126 处写坏的反斜杠(JS→Python 转义层数翻倍,把 markdown
反引号写成反斜杠+反引号),恰好发生在记录这条教训的那一节里。

文档:docs/测试说明/主机单测覆盖率口径.md §9 新增「第 2 轮」小节(两套口径对照
表、五条终检判据、六条手法含「转换说明符序列必须逐位相同」取代「禁止裸 %」、
三条协作纪律含先改板再派工与临时文件带组前缀、后续项 3 条)+ 变更记录一行。

后续项(本轮不做):把剥注释词法扫描器收进 scripts 当门禁(需用户点头并先定
test/ 是否纳入 coding-standard);iec104 的简写编号归族(牵 id_registry,
不能顺手做);替换器加「编号形态白名单」校验。

gate: pre-commit run --all-files -> 39 hooks, 37 Passed; failed = mem-numbers +
mem-budget(受管快照与本树 .map 的漂移,属第 1 轮生产码改动的遗留,非本批
增量)。本批只碰 test/host/**,不进 Keil 编译面也不进模拟器 CMake,**RAM/Flash
增量 = 0**;实测水位 RW_IRAM1 57.4%、RW_CCM 87.9%,均低于 92% 红线。
id-check 0 台账外新账(可判定 84.63% > 基线 82.6%);test-coverage 33/33 全绿;
cppcheck / coding-standard / bsp-err / layer-deps / doc-links 全 Passed。
按仓内分支约定 --no-verify 提交,台账 --write 回填留待主工作树合并后。
2026-10-24 21:00:17 +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 valid sample records 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 smaller sample does not change 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 only accumulates glitch (DWT reset defence)");
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 over-upper-bound sample only accumulates 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 out-of-range id returns safely, touches no slot");
}
/* ===================== 时基与 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 first timebase read lazily enables 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 sets in_seg and records the start point");
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 settles the delta and clears 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 exit without enter records nothing (guards fake huge sample from 0 start)");
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 repeated exit does not double count");
}
/* ===================== 上报节流与通道 ===================== */
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 nothing sent before the first 5 s");
g_tick = 5000u;
task_prof_report();
CHECK((g_line_calls == 2) && (g_raw_calls == 0),
"TC-TASKPROF-012 on schedule: sends two lines via the ring buffer");
g_tick = 6999u;
task_prof_report();
CHECK(g_line_calls == 2, "TC-TASKPROF-013 less than 2 s apart sends nothing");
g_tick = 7000u;
task_prof_report();
CHECK(g_line_calls == 4, "TC-TASKPROF-014 after a full 2 s sends two lines again");
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 ignores throttling and uses the raw channel");
}
/* ===================== 行内容与换算 ===================== */
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 the two lines start with [PROF] / [PROF2] respectively");
CHECK((strstr(g_line[0], "\r\n") != NULL) &&
(strstr(g_line[1], "\r\n") != NULL),
"TC-TASKPROF-016b both lines end with CRLF");
CHECK(field_of(0, "uip=") == 0L, "TC-TASKPROF-016c an unsampled slot prints 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: max of 1680 cycles prints as uip=10");
CHECK(field_of(0, "adcm=") == 20L,
"TC-TASKPROF-017b same clock: max of 3360 cycles prints as adcm=20");
SystemCoreClock = 84000000u; /* 改时钟树而忘了同步 → 整表偏移,这里必须显式跟着变 */
g_tick = 7000u;
task_prof_report();
CHECK(field_of(2, "uip=") == 20L,
"TC-TASKPROF-017c at 84 MHz the same cycle counts print as 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= sums the glitch of all slots");
/* 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 inside UI_PERIOD segment: uip= reports the us elapsed so far");
CHECK(field_of(1, "netn=") == 248L,
"TC-TASKPROF-019b inside NET_POLL segment: netn= of [PROF2] is visible too");
CHECK(field_of(0, "adcn=") == 0L,
"TC-TASKPROF-019c ADC_BEAT not in segment prints 0 (no cross-slot leakage)");
CHECK(s_prof[TASK_PROF_UI_PERIOD].in_seg == 1u,
"TC-TASKPROF-019d reporting does not consume segment state (observe only)");
/* 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 reporting does not clear counters (UI_WORK not in uic= field, still cumulative 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;
}