5.6 需求追溯——从@req到矩阵的自动化
那条致命的问题
距离SOP(Start of Production,量产启动)还剩四周。安全经理李工在评审会议上问了一个问题:
“需求SR-042,‘若CAN通信连续丢失3帧,系统应在100ms内进入安全降级模式’,这个需求有对应的测试用例吗?”
会议室的空气凝固了几秒。有人在翻DOORS数据库,有人在查TestRail,有人在看邮件记录。“应该有的吧…我记得张工写过…不对,张工去年调岗了…”
十分钟后,答案浮出水面:SR-042在DOORS里确实标记为“已实现“,但它没有对应的单元测试,集成测试只覆盖了2帧丢失的场景,3帧丢失的行为从未被验证。如果SR-042真的失效,车辆在CAN总线松动的情况下可能不会进入安全模式。
幸运的是,这是一个评审中才发现的遗漏。如果在SOP后才发现呢?如果在一次事故调查中被监管部门发现呢?
需求追溯不是官僚主义的文档游戏。它是“我承诺这些功能被测试过“的法律证据。 在ISO 26262体系中,需求追溯到测试的完整性是功能安全评估的硬性要求。没有完整的追溯链,安全档案(Safety Case)不成立。
核心洞察:需求追溯不是官僚主义的文档游戏,而是“我承诺这些功能被测试过“的法律证据。 SR-042的教训说明:DOORS里标着“已实现“并不等于被验证过,3帧丢失的行为从未被测试就差点流到SOP。在ISO 26262体系中,需求追溯到测试的完整性是功能安全评估的硬性要求,没有完整的追溯链,安全档案(Safety Case)不成立。
eng-lite的追溯方案
eng-lite没有DOORS,没有Polarion,没有Reqtify。但它有一条不到100行的Python脚本,一个@req标注规范,和一条CI门禁规则。加起来几百行代码,完成了一个完整的需求追溯闭环。
标注规范:@req tag
在源代码中,每一个实现需求的地方用@req标记:
/** @req SR-001: Sensor shall initialize all internal state to zero */
void sensor_init(void)
{
/* @req SR-001 */
/* Implementation: zero-initialize the sample buffer */
}
在测试文件中,每个测试用例用@covers标记它覆盖的需求:
/** @covers SR-001 */
void test_sensor_init_sets_zero(void)
{
/* Test that sensor_init correctly zeroes state */
}
追溯脚本:scripts/trace.py
#!/usr/bin/env python3
"""
eng-lite Traceability Matrix Generator
Scans source files for @req annotations and test files for @covers,
then generates a CSV mapping requirements to test cases.
"""
import re
import csv
import sys
import os
from pathlib import Path
PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIRS = ['src', 'tests']
REQ_PATTERN = re.compile(r'@req\s+(SR-\d+)')
COVERS_PATTERN = re.compile(r'@covers\s+(SR-\d+)')
def scan_files(directory, pattern):
"""Scan all .c and .h files in directory for pattern matches."""
results = []
for root, _, files in os.walk(directory):
for f in files:
if f.endswith(('.c', '.h')):
filepath = os.path.join(root, f)
with open(filepath, 'r') as fh:
for lineno, line in enumerate(fh, 1):
match = pattern.search(line)
if match:
results.append((match.group(1), filepath, lineno))
return results
def main():
all_reqs = {} # req_id -> list of (file, line) where implemented
all_covers = {} # req_id -> list of (file, line) where covered
for d in SRC_DIRS:
abs_d = str(PROJECT_ROOT / d)
if not os.path.isdir(abs_d):
continue
all_reqs.update(
{r[0]: all_reqs.get(r[0], []) + [r] for r in scan_files(abs_d, REQ_PATTERN)}
)
all_covers.update(
{c[0]: all_covers.get(c[0], []) + [c] for c in scan_files(abs_d, COVERS_PATTERN)}
)
# Generate CSV
outdir = PROJECT_ROOT / 'build'
outdir.mkdir(exist_ok=True)
outpath = outdir / 'traceability.csv'
with open(outpath, 'w', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Requirement', 'Implemented In', 'Test Location', 'Status'])
all_req_ids = set(all_reqs.keys()) | set(all_covers.keys())
uncovered = []
for rid in sorted(all_req_ids):
impl_loc = ' + '.join(f"{f}:{l}" for _, f, l in all_reqs.get(rid, []))
test_loc = ' + '.join(f"{f}:{l}" for _, f, l in all_covers.get(rid, []))
if test_loc:
writer.writerow([rid, impl_loc, test_loc, 'COVERED'])
else:
writer.writerow([rid, impl_loc, 'UNCOVERED', 'UNCOVERED'])
uncovered.append(rid)
print(f"Traceability matrix written to {outpath}")
if uncovered:
print(f"ERROR: {len(uncovered)} requirement(s) lack test coverage:")
for r in uncovered:
print(f" - {r}")
sys.exit(1)
else:
print("All requirements have test coverage.")
sys.exit(0)
if __name__ == '__main__':
main()
生成的CSV输出
Requirement,Implemented In,Test Location,Status
SR-001,sensor.c:23,test_sensor.c:29,COVERED
SR-002,sensor.c:37,test_sensor.c:39 + test_sensor.c:54 + test_sensor.c:70,COVERED
SR-003,sensor.c:94,test_sensor.c:88,COVERED
如果某个需求缺少测试,trace.py会以非零退出码退出,触发CI门禁失败。
CI门禁集成
在.github/workflows/ci.yml中:
- name: Traceability Check
run: make trace
这个阶段在CI流水线中的位置是刻意安排的:它在测试和覆盖率之后运行,因为它的前提是测试文件确实存在。它的语义也很清晰:在所有测试都通过之后,检查是否每个需求都有测试。
核心洞察:一条不到100行的Python脚本、一个
@req标注规范、一条CI门禁规则,就完成了需求追溯闭环。 源码里用@req标实现位置,测试里用@covers标验证位置,trace.py扫描两边生成“需求ID→实现位置→测试位置→状态“的CSV矩阵;一旦有需求缺测试就以非零退出码让CI失败。它把“每个需求都要被测试“从口头承诺变成了可自动执行的检查。
追溯矩阵的层级
在真实的汽车项目中,需求追溯有多个层级:
| 追溯层级 | 从 | 到 | 工具 |
|---|---|---|---|
| 客户需求 → 系统需求 | 客户SOR文档 | 系统需求规约 | DOORS链接 |
| 系统需求 → 软件需求 | 系统需求矩阵 | SWRS | DOORS链接 |
| 软件需求 → 架构设计 | SWRS | 软件架构文档 | 设计追溯 |
| 软件需求 → 代码实现 | @req标注 | 源代码 | 代码审查 |
| 软件需求 → 单元测试 | 测试用例 | 测试报告 | 测试追溯 |
| 软件需求 → 集成测试 | 测试用例 | 测试报告 | 测试追溯 |
eng-lite的追溯脚本覆盖的是最底层的两个环节:需求→代码实现 和 需求→单元测试。这恰好也是工程师最能直接控制的环节。
核心洞察:完整追溯链有六层,从客户需求一路延伸到集成测试,而工程师最能直接控制的是最底层的两环。 需求→代码实现靠
@req标注,需求→单元测试靠@covers标注;上层的系统需求、架构设计等环节由DOORS链接等工具承载。但无论工具多复杂,底层数据结构始终是一张映射表——理解这一点,就不会被上层ALM工具的复杂性吓倒。
工程哲学:不是检查清单,是安全网
追溯矩阵经常被误解为“官样文章“:做给审核员看的,没有实际价值。但真正经历过功能安全审计的工程师都知道:一个好的追溯矩阵是你在审计中最重要的武器。 当审核员问“你怎么保证需求SR-042被正确实现了“时,你不靠记忆力,不靠拍胸脯,你打开追溯矩阵,一行一行地指给他看:
SR-042 → 实现在
src/can_safety.c:142→ 测试用例test_can_loss_3frames()在tests/test_can_safety.c:88→ 覆盖率报告显示该函数100%覆盖 → 测试日志显示该用例通过。
这是完整的证据链。审核员要的就是这个。你不能给他一个“我相信没问题“,你必须给他一个“我可以证明没问题“。
核心洞察:追溯矩阵是你在审计中最重要的武器——你给出的不是“我相信没问题“,而是“我可以证明没问题“。 一条完整的证据链把需求、实现位置、测试用例、覆盖率、测试日志串成可逐行指认的链条。更反直觉的是,它的价值不在写标注的当下,而在三年后那个“完全不记得这段代码为何存在“的时刻——防止你把一个有根有据的安全决策当成过度设计删掉。
三道结业练习
完成以下三项练习,作为Chapter 5的结业任务:
练习一:在你的项目中找三个核心函数。在每个函数上方添加一行@req标注(编号自定,如SR-001)。然后为每个函数写至少一个单元测试,用@covers标记需求。运行make trace,确保三个需求都显示“COVERED“。
练习二:故意删除一个@covers标注,运行make ci。观察CI流水线在哪个阶段失败,以及失败信息是否清晰到你能立刻定位问题。
练习三:设想你的项目需要支持ASIL B认证。回顾Chapter 5的五个环节,列出哪些环节需要增强,以及分别需要增强到什么程度。把答案写下来,不要只在大脑里想,写下来的东西才是真正属于你的。
本篇小结
- 需求追溯体系:本章带你走过了eng-lite的需求追溯体系:从
@req标注规范,到trace.py的Python实现,到生成的CSV追溯矩阵,到CI门禁的集成。 - 完整闭环:它们形成一个完整的闭环:标注需求 → 实现代码 → 编写测试 → 检查覆盖率 → 生成追溯矩阵 → CI验证完整性。
- 基础设施闭环:这是eng-lite Chapter 5的最后一章,也是整个工程基础设施闭环的最后一环。
- 五个环节缺一不可:从Makefile(构建系统)到Cppcheck(静态分析),到Ceedling/Unity(单元测试),到GitHub Actions(CI流水线),到trace.py(需求追溯),五个环节,环环相扣,缺一不可。
【全书结语】
到这里,eng-lite这座“亲手建造的楼“竣工了。回顾一下这个教学项目给你的核心礼物:你不是在学习某一个工具,你是在亲身体验一套完整的嵌入式工程基础设施如何运转。 从构建到测试到CI到追溯,这套方法论不依赖任何特定工具或平台。你今天用Makefile + Cppcheck + Ceedling + GitHub Actions,明天换成CMake + Helix QAC + VectorCAST + Jenkins,核心的工程原则不会变:
- 编译通过只是开始,不是结束
- 静态分析是自动化的代码审查,不是惩罚机制
- 单元测试的价值在三个月后,不在当下
- CI的真正作用是确保没有人能跳过检查
- 需求追溯是你未来的自己写给现在的你的备忘录