Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.13 软件测试与MC/DC——每一行代码都要为安全作证

免疫隐喻:测试是软件的免疫系统

人体免疫系统的工作方式堪称工程学奇迹:它不是等到病原体已经大肆破坏后才出手,而是主动巡逻、识别异常、定点清除。T细胞在胸腺中经过“阴性选择“:凡是会攻击自身组织的T细胞,一律淘汰。剩下的,只攻击外来入侵者。这个过程,就是生物版的“测试“。每一颗T细胞都要证明自己不会误伤友军,才能获得“上岗资格“。

软件测试在功能安全中的角色,与免疫系统如出一辙。你写的每一行代码,在装车之前,都必须通过层层测试证明自己的“忠诚“。你不会在某个边界条件下翻转符号位,不会在某个中断嵌套时死锁,不会在某个罕见的时序组合下跳过安全检查。ISO 26262 Part 6 为这个“免疫筛选“过程制定了极其详尽的规则:从软件单元测试到集成测试,再到整车级测试,每一层都有明确的覆盖度要求,而要求的严格程度,与ASIL等级精密挂钩。

接下来,你把自己放进一个真实的测试场景中。

场景:方向盘转角的安全决策

你在测试实验室里盯着一块屏幕,上面是一段看似平淡无奇的C代码:

if ((steering_angle > MAX_ANGLE) && (!calibration_active) && (!emergency_override))
{
    Trigger_Safety_Shutdown();
}

这段代码的含义很直白:当方向盘转角超过最大允许值,且校准未激活、且紧急越权未触发时,执行安全关机。三个条件,一个决策。问题是:你怎么证明你测够了?

一个测试工程师的本能反应是:给一个超限的转角,让另两个条件为真,跑一遍,看到Trigger_Safety_Shutdown()被调用,打勾,完成。这是“语句覆盖“:代码被执行过。满意了吧?

你不满意。因为你知道,只需要一组输入(转角=MAX_ANGLE+1,calibration_active=0,emergency_override=0),这行if和里面的函数调用就都覆盖了:语句覆盖率100%。但你是否验证了任何一个条件失效时,安全关机不会被错误触发?是否验证了校准激活时,即使转角超限也不会误关机?是否验证了紧急越权模式下,驾驶员可以强行超限操作?

没有。100%语句覆盖率,可能只覆盖了全部逻辑行为的12.5%。这就是为什么ISO 26262对不同的ASIL等级要求不同深度的覆盖准则。

从语句覆盖到MC/DC:四个层次的递进

你决定系统地测试这个决策。你坐下来,画出真值表。三个布尔条件:A = (steering_angle > MAX_ANGLE),B = (!calibration_active),C = (!emergency_override)。决策D = A && B && C。

语句覆盖(Statement Coverage) 只需要证明每一行可执行代码至少运行过一次。对于这个if语句,一组输入就够了:A=T, B=T, C=T 。 D=T,进入分支。语句覆盖率:100%。你只证明了一件事情:这段代码“可以运行“。这就像你检查了一辆车的刹车踏板。你踩了一下,它动了。但你不知道它会不会在高速时断裂,不知道ABS会不会在湿滑路面上误触发,不知道在连续下坡热衰减后它还灵不灵。

分支覆盖(Branch Coverage) 要求每个判断的每个可能结果都至少出现过一次。对于这个if,你需要D=T(进入分支)和D=F(跳过分支)各出现一次。你加了一组输入:A=F, B=T, C=T : D=F。现在你证明了两件事:安全关机该触发时会触发,不该触发时不会触发。但你仍然不知道是哪个条件阻止了触发:是转角不超限?还是校准激活?还是紧急越权?你不知道,因为只要任何一个条件为假,D就是假。A、B、C各自的独立作用,被逻辑与运算掩盖了。

条件覆盖(Condition Coverage) 要求每个原子条件取真取假各一次。对于三个条件,你需要A=T和A=F,B=T和B=F,C=T和C=F各出现一次。但你可以用两组输入就满足条件覆盖,而分支覆盖仍然不完整。这引出了MC/DC。

MC/DC(Modified Condition/Decision Coverage,修正条件/决策覆盖) 是你要找的答案。MC/DC的要求是:

  1. 每个决策的每个可能结果至少出现一次;
  2. 每个条件至少一次独立地影响决策的结果;
  3. 每个入口点和出口点至少被调用一次。

“独立影响“是MC/DC的灵魂。它要求你证明:当其他条件固定不变时,改变这一个条件的值,会导致整个决策的结果翻转。这意味着你必须找到那些“这个条件说了算“的测试向量。

你设计测试用例。固定B=T, C=T,让A从F变T:D从F变T。这证明了A独立影响D。固定A=T, C=T,让B从T变F:D从T变F。这证明了B独立影响D。固定A=T, B=T,让C从T变F:D从T变F。这证明了C独立影响D。再加上A=F, B=T, C=T(D=F)覆盖决策的假分支。

四个测试用例:

用例ABCD证明
1TTTT决策为真的基线
2FTTFA独立影响D(A翻转→D翻转)
3TFTFB独立影响D(B翻转→D翻转)
4TTFFC独立影响D(C翻转→D翻转)

N+1个用例(N=条件数),这就是MC/DC的理论最小测试集。你不需要穷举2³=8种组合,但你证明了每个条件都能“一票否决“。这才是软件免疫系统该有的筛查精度。

你再看一眼ISO 26262-6 Table 9(软件单元级结构覆盖度量):

覆盖方法ASIL AASIL BASIL CASIL D
语句覆盖++++++
分支覆盖+++++++
MC/DC+++++

注意这张表的精妙之处:ASIL D要求MC/DC为“++“(强烈推荐),语句覆盖反而降为”+“(推荐)。为什么?因为你做了MC/DC,语句覆盖自然就满足了:前者是后者的严格超集。ASIL A则相反:语句覆盖”++“,MC/DC只是”+“:对于低安全等级的系统,MC/DC的额外成本可能不值得。这是ISO 26262务实的一面:安全投入要与风险等级匹配,不是越严越好。

测试推导方法:从需求到用例的路径

有了覆盖准则,你怎么生成测试用例?ISO 26262-6 Table 7给出了测试推导方法的推荐等级:

方法ASIL AASIL BASIL CASIL D
需求分析++++++++
等价类生成+++++++
边界值分析+++++++
错误推测++++

需求分析是“++“全满贯:不管什么ASIL等级,你都必须从需求出发推导测试。这是安全测试的根基:测试不是为了证明代码“能跑”,而是为了证明代码“满足了安全需求“。如果一条安全需求没有对应的测试用例,那就等于这条需求从未被验证过:安全论证会出现逻辑断裂。

等价类生成和边界值分析在ASIL B/C/D都是“++“。你想象一下转角传感器的测试:输入范围是[-540°, +540°]。等价类划分告诉你,只需要测试有效等价类(正常范围内的一个代表值)和无效等价类(小于-540°、大于+540°)。但边界值分析进一步要求你测试-540°、+540°、-541°、+541°:边界处的行为最容易出错。每一个老工程师都被“off-by-one“的bug折磨过。边界值分析就是专门猎杀这类bug的。

错误推测是唯一在所有ASIL等级都是“+“的方法。它依赖测试者的经验和直觉:“我觉得这里可能会溢出”、“如果中断在这里到来会不会死锁?”。这种方法不可系统化,因此不能作为主要依据,但它的价值不可否认:最好的测试工程师往往能找到需求分析和等价类划分都遗漏的缺陷。

测试层次:单元→集成→架构→整车——层层递进的四重免疫防线

你把上述内容想象成免疫系统的四道防线:

第一道防线:软件单元测试(Part 6, Clause 9)

这是最微观的层面。皮肤和黏膜:阻挡病原体的第一道物理屏障。你把每个函数、每个模块单独拿出来,给它精心构造的输入,检查输出是否完全符合详细设计。这里是你验证MC/DC的地方。这里也是你发现指针越界、除零、整数溢出、死代码等低级但致命的bug的地方。

对于ASIL D的单元测试,你必须:

  • 对每个安全相关的软件单元做MC/DC(++)
  • 对所有安全相关单元做分支覆盖(++)
  • 用需求分析推导测试用例(++)
  • 用等价类和边界值分析推导测试用例(++)

第二道防线:软件集成测试(Part 6, Clause 10)

这是固有免疫层面。巨噬细胞和中性粒细胞。它们不针对特定病原体,但它们能识别“这不正常“。你把测试好的单元组装成子系统,验证它们之间的接口、数据流、时序。这里的目标不是验证函数内部的逻辑,而是验证“模块A传给模块B的数据在每种调度场景下都是正确的“、“任务间的同步不会导致死锁”、“共享内存的访问是互斥的”。

ISO 26262 Table 11给出了集成测试的方法推荐:

方法ASIL AASIL BASIL CASIL D
基于需求的测试++++++++
故障注入++++
资源使用评估++++++++

故障注入是集成测试的特色。你故意让一个模块返回错误码,看另一个模块如何反应:是优雅降级?还是崩溃?还是静默失败(最危险的一种)?对于ASIL C/D,故障注入是“++“。你必须系统性地验证每个安全机制在面对故障输入时的行为。

资源使用评估也是“++“全满贯。在嵌入式系统中,栈溢出、堆耗尽、CPU负载过高等资源问题是最隐蔽的杀手。你必须在集成测试中测量最坏情况下的资源消耗,确保留有安全裕量。

第三道防线:架构级覆盖(Part 6, Clause 10.4.5)

这是适应性免疫层面。T细胞和B细胞。它们针对特定抗原,提供精确打击。架构级覆盖不看函数内部的代码,而是看软件架构的“骨架“是否完整:

方法ASIL AASIL BASIL CASIL D
函数覆盖++++++
调用覆盖++++++

函数覆盖:架构中的每个函数都被调用过吗?调用覆盖:架构中定义的每个函数调用关系都在测试中执行过吗?你可能会觉得奇怪:单元测试和集成测试都做了,函数怎么可能没被调用?答案是:中断服务函数(ISR)、错误处理函数、降级模式下的备选路径。这些“罕见路径“在正常测试中经常被遗漏。架构级覆盖要求你专门验证它们。

第四道防线:整车集成测试(ISO 26262-4, Clause 11)

这是免疫记忆。打过疫苗之后,身体记住了抗原:下一次遭遇,反应更快、更强。整车级测试在最真实的场景下验证安全机制:不是模拟器,不是HIL台架,而是真实的车辆在真实的道路上行驶。

ISO 26262-4 Table 13(整车级功能安全需求验证方法)简练而坚决:

方法ASIL AASIL BASIL CASIL D
基于需求的测试++++++++
故障注入++++++++
长期测试++++++++
用户测试++++++++

四个“++“全满贯,所有ASIL等级一律”++“。整车级测试不接受“推荐”:每一项都是“强烈推荐“。为什么?因为无论你在实验室做了多么充分的模拟,真实世界的复杂性和随机性总会给你“惊喜“:CAN总线上的噪声模式、温度循环导致的水汽凝结、用户以你从未想到的方式操作开关。这些在设计中无法穷举的场景,必须通过整车测试来暴露。

长期测试(耐久性测试)尤其值得单独讨论。它不是在验证“功能是否正确“,而是在验证“在持续的应力下,正确性能维持多久“。一辆车要开15年。15年的振动、温度变化、电磁干扰、软件状态积累。你无法在单元测试中模拟。你必须跑够里程。

本篇小结

  1. 测试深度递进:语句覆盖→分支覆盖→MC/DC,每一层覆盖前一层的盲区
  2. 测试推导有四种方法:等价类划分、边界值分析、原因-效果图、判定表
  3. 从单元测试到整车测试的四道防线,每道有各自的覆盖准则和测试方法
  4. 整车测试的四项全“++“提醒我们:真实道路上的验证不可省略

【下集预告】: MC/DC测试保证了每个软件单元的每一个条件都被验证,但一个现实问题摆在面前:你买不到几颗ASIL D的MCU,交货周期52周,单价200美元。下一节给出另一条路:两颗ASIL B的MCU加起来能到D吗?ISO 26262说可以——但前提是你必须在DFA中证明它们不会同时失效,而且硬件度量指标不能分解。B+B要真的等于D,光靠纸上写是不够的。