4.4 测试基础设施——EmbUnit框架与原生宿主测试
嵌入式测试的原罪
嵌入式测试有一个原罪:代码跑在目标芯片上,测试却想在开发机上跑。CAN驱动需要CAN控制器,Flash驱动需要Flash外设,OS调度器需要真实中断,这些在x86上都没有。于是早期的嵌入式开发者发明了一种“高效“流程:交叉编译→烧录→连调试器→手动观察寄存器→靠经验判断对不对。
Arctic Core的测试基础设施说:不,我们把测试搬到宿主机上。
“宿主测试”(Host-Based Testing)在汽车行业已经有一段时间,但Arctic Core以最小的代价实现了它:不需要QEMU、不需要Simulink、不需要任何仿真平台。只需要一份tests_common.mk,一个叫EmbUnit的轻量框架,和GCC的x86编译模式。
核心洞察:嵌入式测试的原罪在于把“目标硬件“误当成了测试的必要条件。CAN驱动需要CAN控制器、Flash驱动需要Flash外设,这些在x86上都没有——但Arctic Core选择把测试搬回宿主机,且以最小代价实现:一份
tests_common.mk、一个EmbUnit、一个GCC x86编译模式即可。测试验证的是逻辑而非硬件,硬件差异用stub隔离。
宿主测试的核心:重新定义“目标“
tests_common.mk的开篇注释说得很清楚:
###
# This file is supposed to be included from the test-specific makefiles.
# For this reason all paths in this file is relative to the current BDIR.
###
注意:relative to the current BDIR,测试的“构建目录“是测试用例所在的目录,所有路径都从那里计算。这意味着测试工程是自包含的:它有自己的makefile,自己的配置文件,自己的stub(桩代码),自己的工具目录(utils)。
测试工程的目录结构(第26-27行):
VPATH := ../config/$(board_name) ../config ../stubs ../utils $(VPATH)
inc-y := $(inc-pre-y) ../config/$(board_name) ../config ../stubs ../utils $(inc-y)
四层级联的搜索路径:
../config/$(board_name):目标板定制配置(优先级最高)../config:通用测试配置../stubs:桩代码,模拟真实硬件或依赖模块的行为../utils:测试工具函数
这个顺序保证了板级定制配置可以覆盖通用配置,而通用配置为所有测试提供一致的基线。
源文件的自动发现机制同样简洁(第32-34行):
PROJECT_C_FILES=$(notdir $(wildcard ../*.c))
obj-y += $(PROJECT_C_FILES:%.c=%.o)
VPATH += ..
不需要手动列出测试文件,把测试用例放在测试目录下,构建系统自动捡起来编译。如果某个测试需要额外的源文件,手动加到obj-y即可。
核心洞察:宿主测试的起点是“重新定义目标“:测试的构建目录就是测试用例所在目录。所有路径相对它计算,测试工程自包含——自己的makefile、配置、stub、工具目录。config/stubs/utils四层级联搜索路径保证板级定制配置覆盖通用基线,源文件自动发现则消灭了手动清单,只需把测试用例放进目录就会被捡起来编译。
EmbUnit:嵌入式世界的xUnit
EmbUnit是嵌入式C世界的单元测试框架标准。它的API风格借鉴了JUnit,但完全用C实现,没有任何动态内存分配:
# === EmbUnit ===
VPATH += $(PROJECT_DIR)/embUnit/embUnit
inc-y += $(PROJECT_DIR)/embUnit/embUnit
LINT_EXCLUDE_PATHS += $(PROJECT_DIR)/embUnit/embUnit
VPATH += $(PROJECT_DIR)/embUnit/textui
inc-y += $(PROJECT_DIR)/embUnit/textui
LINT_EXCLUDE_PATHS += $(PROJECT_DIR)/embUnit/textui
inc-y += $(PROJECT_DIR)/embUnit
include $(PROJECT_DIR)/embUnit/embUnit/embUnit.mk
include $(PROJECT_DIR)/embUnit/textui/textui.mk
EmbUnit被分为两层:核心层(embUnit库)提供断言宏(TEST_ASSERT_EQUAL_INT、TEST_ASSERT_NULL、TEST_FAIL等)和测试用例注册机制;输出层(textui)提供文本界面的测试结果打印。两层被独立引入,你可以替换textui为XML输出器,不改变核心断言库的任何一行。
第12行和第15行的LINT_EXCLUDE_PATHS值得注意:EmbUnit的源码不运行MISRA检查。这不是偷懒:EmbUnit不是运行在目标芯片上的代码,它是宿主工具,不属于功能安全范围。MISRA资源只应该花在真正进入ECU的代码上。
核心洞察:EmbUnit是零动态内存分配的C版xUnit,分层让可替换性成立。核心层提供断言宏与测试注册机制,输出层textui可被XML输出器整体替换而不动断言库一行——API借鉴JUnit,实现却是纯C、无malloc,以最轻的重量适配嵌入式约束。把它排除在MISRA之外并非偷懒:宿主工具不进入ECU、不承担安全责任,MISRA资源只该花在真正上车的代码上。
CI集成:从文本到XML的一跃
测试框架的核心价值不在于“能跑测试“,而在于“能把测试结果自动喂给CI“。EmbUnit的textui默认输出人类可读的文本,但Arctic Core在这里加了一个关键的编译开关:
ifneq ($(SELECT_TEST_OUTPUT),)
ifeq ($(SELECT_TEST_OUTPUT),xml)
def-y+= XML_OUTPUTTER_SKIP_HEADER
def-y+= XML_OUTPUTTER_HTML_ESCAPE
def-y+= EMBUNIT_XML_OUTPUT
endif
endif
当SELECT_TEST_OUTPUT=xml时,三个宏被定义:
EMBUNIT_XML_OUTPUT:切换输出格式从文本到XML(JUnit兼容格式)XML_OUTPUTTER_SKIP_HEADER:不输出XML声明头(便于多个测试结果文件被简单拼接)XML_OUTPUTTER_HTML_ESCAPE:对测试名称中的特殊字符做HTML转义(避免破坏XML结构)
这三行代码的价值远超出表面:它让EmbUnit的输出可以直接被Jenkins的JUnit插件解析。一条make test SELECT_TEST_OUTPUT=xml命令产生的XML文件,可以在CI仪表盘上生成绿色/红色的测试趋势图。在汽车行业,这意味着每次代码提交都可以自动运行全部单元测试,结果可视化,ISO 26262-6要求的“软件单元验证“有了一层可审计的自动化证据链。
测试公共文件还隐式管理了AUTOSAR模块间的依赖关系:
ifeq (USE_DCM, $(findstring USE_DCM,$(def-y)))
inc-y += $(ROOTDIR)/diagnostic/Dcm/inc
endif
ifeq (USE_COM, $(findstring USE_COM,$(def-y)))
inc-y += $(ROOTDIR)/communication/Com/inc
endif
ifeq (USE_PDUR, $(findstring USE_PDUR,$(def-y)))
inc-y += $(ROOTDIR)/communication/PduR/inc
inc-y += $(ROOTDIR)/communication/Com/inc
endif
这160多行的条件include告诉测试构建系统:如果你要测CanTp模块,你需要CanIf的头文件;如果你要测PduR模块,你不仅需要PduR自己的头文件,还需要Com模块的:因为PduR的路由表引用了Com模块的数据类型。这些隐式依赖在真实ECU上由RTE自动满足,但在宿主测试中必须显式声明。tests_common.mk就是这份“依赖地图“。
核心洞察:测试框架的价值不在“能跑测试“,而在“能被CI自动消化“。三个编译宏(
EMBUNIT_XML_OUTPUT、XML_OUTPUTTER_SKIP_HEADER、XML_OUTPUTTER_HTML_ESCAPE)把文本输出切换为JUnit兼容XML,一条make test SELECT_TEST_OUTPUT=xml即可被Jenkins解析生成红绿趋势图——每次提交自动跑全部单测并可视化,ISO 26262-6的“软件单元验证“由此获得一层可审计的自动化证据链。
覆盖率工具:GCOV与BullseyeCoverage的双轨制
纯跑测试不够,汽车功能安全(尤其是ASIL C/D等级)要求结构覆盖率(Statement Coverage、Branch Coverage、MC/DC)。Arctic Core同时支持两种覆盖率工具:
覆盖工具选择(build_config_bsw.mk第21-29行):
ifneq ($(SELECT_COV_TOOL),)
CFG+=FILE_MEM_PARTITION
CFG+=FS_RAM
ifeq ($(SELECT_COV_TOOL),gcov)
CFG+=GCOV
endif
ifeq ($(SELECT_COV_TOOL),bullseye)
CFG+=BULLSEYE
endif
endif
GCOV是GCC自带的覆盖率工具,通过编译选项注入:
c_common_flags-$(CFG_GCOV) += -fprofile-arcs
c_common_flags-$(CFG_GCOV) += -ftest-coverage
-fprofile-arcs在目标文件中插入计数器代码,-ftest-coverage生成.gcno笔记文件。测试运行后,.gcda数据文件记录每个基本块的执行次数。GCOV的优势是零成本集成:只要编译器是GCC,覆盖率能力就自然存在。
BullseyeCoverage是商用覆盖率工具,通过编译前钩子注入:
CC_PRE=$(COV_BULLSEYE_PATH)/bin/covc -i
covc -i是一个编译器包装器:它截获编译命令,在预处理和编译之间插入自己的插桩代码。BullseyeCoverage的优势是支持MC/DC覆盖率和多编译器,劣势是需要商业许可。两者的并存体现了Arctic Core“不绑架用户到特定工具“的设计哲学。
核心洞察:GCOV与BullseyeCoverage的双轨制是“工具多样性降低系统性风险“的实践。一个在编译器层面注入插桩、一个在预处理层面包装编译器,两套完全不同的技术对同一段代码报告一致的覆盖率时,数据的可信度远高于单一工具——这是对工具链错误(Tool Chain Error)的有效防御,也是ASIL C/D结构覆盖率要求的工程回应。
源码级注释:@req需求追溯
Arctic Core的测试和源码有一种独特的关联方式:@req注释标签。在boards/no_os/config/Fls_Cfg.h中:
/** @req SWS_Fls_00308 */
#include "MemIf_Types.h"
#if defined(USE_FEE)
/** @req SWS_Fls_00262 */
/** @req SWS_Fls_00263 */
#include "Fee_Cbk.h"
#endif
@req后面的SWS_Fls_00308是AUTOSAR标准文档中的需求编号(SWS = Software Specification,Fls = Flash Driver模块,00308 = 第308号需求)。每一条需求都精确地锚定到一行源代码。这不是给编译器看的,是给审计员、给功能安全评估员、给配置管理工具看的。
在Fls_Cfg.h的结构体定义中,追溯密度更高:
typedef struct {
/** @req SWS_Fls_00109 */
/** @req SWS_Fls_00110 */
void (*FlsJobEndNotification)();
void (*FlsJobErrorNotification)();
...
/** @req SWS_Fls_00355 */
const struct Flash *FlsInfo;
...
} Fls_ConfigSetType;
每一个结构体成员都标注了它对应哪条AUTOSAR需求。这种“需求→设计→代码“的双向追溯链(Bidirectional Traceability)是ISO 26262-6第7章对软件架构设计的硬性要求,你必须能证明:代码中每一行都对应了一条需求,需求中每一条都有对应的代码实现。@req标签让这个追溯链在代码中自我文档化,而不需要一份外部的追踪矩阵Excel。
这种追溯链在业务代码中更加密集,例如StbM(同步时间基准管理)模块:
/* @req SWS_StbM_00051 */ /* For types included from required files*/
/* @req SWS_StbM_00124 */ /* data types uint8, uint16 and sint32 used... */
/* @req SWS_StbM_00180 */ /* StbM shall maintain the Local Time Base autonomously... */
/* @req SWS_StbM_00240 */ /* CS service interface - StbM_GlobalTime_Master */
每个模块有数十到数百条@req注释,Arctic Core整个代码库有超过7300处@req引用。它们既不是被编译器忽略的“装饰“,也不是事后补的“文档“,它们就是需求实现本身的一种形式化声明。
核心洞察:7300多处
@req标签让需求追溯在代码中自我文档化。每一条AUTOSAR需求(如SWS_Fls_00308)都精确锚定到一行源码或一个结构体成员,审计员、安全评估员、配置管理工具不必再翻外部Excel矩阵。它们不是给编译器看的装饰,而是“需求→设计→代码“双向追溯链的自我声明,是ISO 26262-6第7章架构设计的硬性证据。
构建比喻:材料试验室
每一批次的建筑材料进入工地之前,必须经过材料试验室的检验:混凝土试块要做抗压测试,钢筋要做拉伸测试。tests_common.mk就是这个试验室的SOP。
在现实工地上,你不需要把整栋楼建起来再测混凝土强度,你只需要浇一小块试块(宿主测试只编译被测试模块+stub),在标准养护条件下测试(x86宿主机环境),然后出报告(XML输出→CI仪表盘)。如果批次不合格,立刻退回材料供应商(修复代码),不需要等到大楼封顶才发现问题。
覆盖率工具则是试验室的“取样率指标“,你不仅要知道试块通过了测试,还要知道试块的每一条纤维都被拉伸到了(每行代码都被执行到)、每一个拐角都被检查了(每个分支都走了两边)、每一组逻辑组合都被验证了(MC/DC)。GCOV给你的报告告诉试验室主任:“第37号配方的那袋水泥,我们只测了上表面,下面的还没测到。”
@req标签则是试验报告和设计图纸之间的交叉索引,第308号需求对应的混凝土配比在第37行代码实现,测试报告可以证明这个配比的试块通过了全部检验。这层追溯链是ISO 26262审计的核心证据。
核心洞察:测试是浇筑前的材料试验室:浇一块试块、标准养护、出报告。不需要建完整栋楼再测混凝土强度——宿主测试只编译被测模块加stub,在x86标准环境养护,产出XML报告上CI仪表盘;批次不合格立刻退回供应商,不必等到大楼封顶才发现问题。覆盖率工具则是试验室的取样率指标:不仅要证明试块通过,还要证明每条纤维(每行代码)都被拉伸、每个拐角(分支)都被检查、每组逻辑组合(MC/DC)都被验证。
本篇小结
- 原生宿主测试:Arctic Core的测试基础设施以
tests_common.mk为枢纽,通过EmbUnit框架实现了嵌入式C代码的原生宿主测试。 - 测试依赖管理:测试目录通过四层级联的config/stubs/utils路径管理测试依赖,自动发现测试源文件。
- CI可解析的XML输出:EmbUnit通过编译宏
EMBUNIT_XML_OUTPUT实现JUnit兼容的XML输出,直接接入Jenkins等CI工具。 - 双轨覆盖率方案:GCOV和BullseyeCoverage的双轨覆盖率方案提供了从statement coverage到MC/DC的完整覆盖率能力。
- @req双向追溯链:超过7300处的
@req标签实现了从AUTOSAR需求到源代码的双向追溯链,为ISO 26262功能安全审计提供了自我文档化的证据材料。
【下集预告】:测试告诉你“逻辑对不对“,但谁告诉你“代码有没有隐患“?混凝土强度达标不代表钢筋没有锈点,编译通过不代表没有MISRA违规。下一节,我们走进
scripts/pclint/目录,看一个1835行的.lnt文件如何把143条MISRA C:2012规则一条一条映射到PC-Lint的诊断号,看gcc.lnt的14行如何定义整个项目的静态分析策略。这是浇筑之前对每一根钢筋的出厂检验。