1. 先搞清楚 Fuego 到底是个什么东西
我最早接触 Fuego 是在一个需要给多块不同架构的开发板做回归测试的场景里。当时手头有五六种板子,架构不一,有的跑在本地串口上,有的只能远程登录操作,每次改动一点驱动或者内核配置,就得挨个手工敲命令验证一遍,累且容易漏。后来用了 Fuego,才算把这堆重复劳动收拢成一套可复用、可批量跑的测试集合。
Fuego 是一个面向嵌入式 Linux 设备的自动化测试框架,核心定位是"把测试用例组织起来,统一调度到目标板上执行,并把结果集中收集、比对、展示"。它本身不产出测试内容,而是提供一套跑测试的基础设施:测试用例的组织方式、执行调度、日志采集、结果判定、以及一个基于网页的结果查看界面。你可以把它理解成一个"测试管家"——你告诉它有哪些板子、要跑哪些测试,剩下的分发、执行、收日志、判断通过与否,它来操心。
这套东西解决的问题很明确:嵌入式测试的碎片化。板子多、架构杂、连接方式各异、日志散落各处、结果靠人眼看。Fuego 用统一的目录结构和约定式的脚本接口,把这些差异抹平了一部分,让"写一次测试,到处跑"变得可行。它适合谁呢?我认为主要是三类人:一是维护多款硬件、需要频繁做冒烟和回归的嵌入式测试工程师;二是做 BSP 或系统集成、要验证镜像基本功能的开发人员;三是想把手工验证流程固化成可持续执行的自动化流程的团队。
这篇文章聚焦的是 Fuego 里最容易让人卡住、也最能体现它设计思路的一件事:给框架添加一个新的测试。为什么单独拎这件事出来讲?因为 Fuego 的测试不是随便丢个脚本进去就能跑的,它有一套自己的约定——测试目录里的 spec 文件、测试脚本、解析脚本、判据文件,这几样东西各司其职,少一个或者写错一处,测试就跑不起来或者结果永远是"通过"。我见过太多人第一次加测试时,脚本明明本地能跑,一放进框架就各种报错,问题往往就出在对这套约定的理解不到位上。
所以下面我会按"整体设计思路 → 核心文件逐个拆解 → 完整实操落地 → 踩坑排查"的顺序展开,把我实际加测试时踩过的、以及帮别人排查过的问题都摊开讲。读完你应该能独立地给自己的板子加一个能跑、能判、结果可信的测试用例。
2. 添加测试前必须吃透的设计约定
2.1 为什么 Fuego 要用"目录 + 约定文件"这种笨办法
第一次看 Fuego 的测试目录时,我的反应是:这也太原始了。一个测试就是fuego_test目录下的一个子目录,里面塞着fuego_test.sh、spec.json(老版本是spec)、parser.py、criteria.json这么几个文件,全靠文件名和约定来协作,没有任何强类型的接口定义。这种设计放在今天的眼光看确实"土",但用久了会发现它是有意为之。
嵌入式环境的现实是:目标板上的工具链可能是精简过的 busybox,可能没有 Python,可能连 bash 都是裁剪版。如果框架要求测试必须写成某种特定语言、依赖某个运行时,那很多板子根本跑不了。Fuego 的做法是把"框架侧"和"目标侧"解耦:测试脚本fuego_test.sh是跑在目标板上的,只要求最基本的 shell 环境;而parser.py和判据处理是跑在主机上的,主机环境相对完整,可以用 Python。这个分工是整个框架的基石,理解了这一点,后面所有"为什么这个文件要这么写"就都顺了。
另一个考量是可移植性和可维护性。测试用例以纯文本文件形式存在,意味着它可以被版本管理、被复制、被改造。你想基于现有测试改一个新变体,直接复制目录改名就行,不需要动框架代码。这种"约定优于配置"的思路,牺牲了一部分类型安全和错误提示,换来了极低的接入门槛和极强的可移植性——对嵌入式这种环境差异巨大的领域来说,这笔交易是划算的。
2.2 四个核心文件各自扮演什么角色
在动手之前,得先在心里建立起这张分工表。我把它整理成了下面这个对照,建议加测试时对着看:
| 文件 | 运行位置 | 职责 | 写错的典型后果 |
|---|---|---|---|
spec.json | 主机(框架读取) | 声明测试的元信息:名称、依赖、超时、是否需要重新构建等 | 测试不显示、参数不生效、超时被误判 |
fuego_test.sh | 目标板 | 真正在板子上执行的测试逻辑 | 测试报错、卡死、无输出 |
parser.py | 主机 | 把测试输出解析成结构化结果(测量值/判据) | 结果为空、指标解析不出来 |
criteria.json | 主机 | 定义通过/失败的门槛 | 永远通过或永远失败 |
这里最容易误解的是spec.json和criteria.json的价值。很多人觉得这俩就是走个形式,随便填填。实际上spec.json里的超时设置、依赖声明直接决定了测试在调度层面会不会被误杀;criteria.json里的判据则决定了"跑完了"和"跑对了"之间的区别。我后面会专门讲这两块的细节。
另外要强调一个顺序问题:Fuego 的执行流是"主机调度 → 板子执行脚本 → 输出回传主机 → parser 解析 → criteria 判定"。这个链条意味着,你在fuego_test.sh里echo出来的东西,最终要能被parser.py认出来。两边必须约定好输出格式,这是新手最常断裂的地方。我见过有人脚本里打印了一堆漂亮的中文日志,parser 里却按固定英文关键字去匹配,结果解析出来一片空白,还以为测试没跑。
2.3 三种常见的测试类型,决定你的脚本怎么写
Fuego 里的测试大致可以分成几类,不同类型对应不同的脚本结构,提前想清楚你属于哪类能省很多返工:
第一类是功能验证型,跑一个程序,看它退出码和输出是否符合预期。这类最简单,脚本基本就是调用、检查返回值。
第二类是基准性能型,跑一个负载,采集耗时、吞吐之类的数值,这类必须输出可被 parser 提取的数值,通常还要在criteria.json里设阈值。
第三类是交互/长时型,比如需要串口交互、需要持续跑一段时间采集状态。这类要特别注意超时设置和清理逻辑,否则容易把板子挂住。
你要加的测试属于哪一类,直接决定了fuego_test.sh里要不要做数值输出、parser.py里要不要写提取逻辑、criteria.json要不要设门槛。我个人的建议是:第一次加测试,先挑一个最简单的功能验证型跑通全流程,把整条链路摸熟,再去碰复杂的。别一上来就搞性能采集,出了问题你分不清是脚本错、parser 错还是判据错。
3. 核心文件逐个拆解与实操要点
3.1 spec.json:别把它当成注释文件
spec.json是框架读取测试元信息的地方。它的字段不多,但每一个都有实际作用,我挑几个关键的讲。
testName是这个测试的唯一标识,框架界面和命令行里都用它来引用测试。这里有个坑:名字里别用空格和特殊字符,否则在命令行传参和结果目录生成时容易出问题。我一般用下划线分隔的小写名,比如my_gpio_check。
timeout是单次测试执行的超时秒数。这个值一定要结合你的测试实际耗时来设,宁可略宽也别卡太死。设太短的后果是测试还在跑就被框架强杀,报告里显示超时失败,你会以为是板子问题查半天。设太长的问题相反,测试真挂了框架要等很久才回收,批量跑时特别浪费时间。我的经验值是:本地手工跑三次取最大耗时,再乘以 1.5 到 2 作为超时。
needs_output之类的字段涉及输出处理的开关,具体名称随版本略有差异,加测试时最好直接参考同目录下已有的、能正常工作的测试的 spec 文件,照着改是最稳妥的。新版 Fuego 有spec.json,老版本可能是无扩展名的spec,格式是类似的键值对。动手前先确认你用的 Fuego 版本用的是哪种格式,这个细节不对,测试根本不会被识别。
还有一个容易忽略的点:spec 里可以声明测试依赖,比如是否需要先构建、是否依赖某个工具存在。如果你的测试需要先在主机上编译一个程序再推送到板子,那构建逻辑和依赖声明就得配合好fuego_test.sh里的推送步骤。这块我建议初期直接跳过,先做纯脚本型测试,等熟了再上需要编译的。
注意:修改 spec 后,最好清一下测试的构建缓存再重新跑。Fuego 有时会缓存测试元信息,改了不生效会让人怀疑人生。
3.2 fuego_test.sh:跑在板子上的那部分,要"贫瘠"一点
这个脚本是整个测试的核心执行体,但它跑在目标板上,环境可能非常简陋。写的时候要时刻提醒自己:这里不是你的开发机。具体来说有几条硬性习惯。
第一,脚本开头别假设有 bash。很多精简系统只有 busybox 的 sh,写#!/bin/bash可能直接找不到解释器。稳妥的写法是#!/bin/sh,然后避免使用 bash 独有的语法,比如[[ ]]、数组、local在某些裁剪环境里也可能不可用。我一般写测试脚本时会把语法限制在 POSIX sh 的子集里,这样兼容性最好。
第二,注意工作目录。脚本执行时当前目录不一定是测试目录,所有对测试资源的引用要么用绝对路径,要么在脚本开头显式cd到已知位置。我踩过的坑是脚本里用相对路径引用一个数据文件,本地测好好的,在框架里跑就找不到文件,原因是框架从一个不同的目录启动脚本。
第三,输出要克制且规范。因为输出最终要被parser.py解析,你echo的每一行都要么是给人看的,要么是能被机器认出的结构化信息。我的做法是:给机器看的用固定前缀,比如RESULT: value=xxx,给人看的就正常打印。别把两者混在一起,也别打印一大堆无关信息,否则 parser 的匹配逻辑很容易被误触发。
第四,一定要处理退出码和清理。脚本结束时的退出码框架会看,非零通常意味着执行层面出了问题。如果你的测试中途创建了临时文件、启动了后台进程,记得在脚本末尾清掉,否则残留在板子上会影响下一次测试。我给脚本加清理逻辑的方式是尽量把可能失败的地方包起来,确保清理代码一定能被执行到。
3.3 parser.py:主机侧的翻译官,别写得太脆
parser.py的角色是把板子回传的原始输出"翻译"成框架能理解的结构化数据。它跑在主机上,可以用完整的 Python 环境,这一点比脚本宽松多了。但它有一个隐性的脆弱点:它依赖输出格式。板子上的输出一旦变了(哪怕只是多了个空行、换了个措辞),parser 就可能匹配不上。
所以写 parser 的核心原则是:匹配要宽、提取要稳、缺失要有兜底。具体讲,匹配用正则时别锁死整行,用关键字加宽松的数值模式,比如r"RESULT:\s*value=([0-9.]+)"这种,容许前后有空格、有换行。提取不到值时,不要让它静默返回空,要么给出明确提示,要么用一个合理的默认值并打日志,这样排查时你能立刻知道是"没提取到"而不是"值就是空"。
另一个实践要点是 parser 里处理多组测量值的方式。有的测试一次跑下来会产生好几个指标,parser 要能分别提取并各自命名,这样在结果界面上才能分开显示、分开和判据比较。我建议指标名就用有意义的英文短名,跟criteria.json里的键一一对应,两边名字对不上是最常见的低级错误。
3.4 criteria.json:把"跑完了"升级成"跑对了"
判据文件是决定结果可信度的地方。没有它,测试跑完只能说明"执行没报错",不能说明"功能正确"。criteria.json的本质是给 parser 提取出来的每个指标定一个通过门槛,框架拿实际值和门槛比,给出通过或失败。
写判据时最需要想清楚的是门槛怎么来。比如你测一个操作的耗时,门槛不能拍脑袋定,得基于实测数据留出合理裕量。我的方法是先在几块不同的板子上各跑若干次,记录分布,取一个明显宽松于正常波动、但又能抓住明显劣化的值。门槛定太严,稍微有点系统负载波动就误报失败;定太松,真出问题了也发现不了。这个平衡点要靠数据说话,不能靠感觉。
还有一点要提醒:判据的键名必须和 parser 输出的指标名严格对应。哪怕大小写差一个字母,框架也可能认为该指标没有判据,或者判据没有对应指标,结果要么恒过要么恒失败。这个坑我替别人排查过不止一次,症状就是"测试结果永远是同一个值",最后发现是键名对不上。
4. 从零加一个测试的完整落地过程
4.1 准备工作:先手工跑通再进框架
这一步很多人跳过,然后付出代价。我的铁律是:任何测试,先在目标板上手工完整跑通,确认命令、依赖、输出都符合预期,再考虑往框架里搬。原因很简单,框架会引入额外变量(工作目录、环境变量、超时、输出重定向),如果基础命令本身就有问题,你在框架里排查等于同时面对两个未知数。
具体做法:登录到目标板,手工执行你打算写进fuego_test.sh的那串命令,观察输出、耗时、返回值。把实际输出抄下来,这就是你后面写 parser 匹配规则的依据。同时记下这次的真实耗时,用来定spec.json里的超时。别忘了在不同状态下各跑几次(冷启动、有负载时),看看耗时和输出的稳定性,这直接决定你的判据门槛定多宽。
4.2 建立测试目录与骨架文件
准备工作完成后,到 Fuego 的测试目录下创建你的测试子目录。目录名建议和测试名一致,便于对应。然后在里面放上四个骨架文件。这一步我建议不要从空白开始,而是从同类型、已能正常工作的测试目录复制一份过来改,能省掉大量格式细节上的来回。
复制过来之后按顺序改:先改spec.json的名字和超时;再改fuego_test.sh里的执行逻辑;然后改parser.py的匹配规则和指标名;最后改criteria.json的键名和门槛。改的过程中每改完一个文件,可以先不急着全链路跑,而是在主机上单独验证一下语法,比如python直接执行一下 parser,看有没有语法错误。这种小步验证能避免一次性冒出五六个错误。
4.3 编写执行脚本的关键片段
执行脚本的结构我一般这么组织:开头做环境确认和变量定义,中间是主测试逻辑,最后是清理。下面是一个简化后的骨架,注意它只用 POSIX sh 语法:
#!/bin/sh # 基本环境确认,缺工具就尽早失败,别跑到一半才报错 if ! command -v some_tool >/dev/null 2>&1; then echo "ERROR: some_tool not found" exit 1 fi # 主逻辑 TMPFILE="/tmp/my_test_$$.log" some_tool --run > "$TMPFILE" 2>&1 RC=$? # 输出可被 parser 提取的结构化结果 VALUE=$(grep -o 'metric=[0-9.]*' "$TMPFILE" | head -n 1 | cut -d= -f2) echo "RESULT: value=${VALUE}" # 清理 rm -f "$TMPFILE" exit $RC这段骨架里有几个设计意图值得说。开头查工具是为了让失败尽早暴露,避免脚本跑到后面才因为缺命令莫名失败;用$$做临时文件名后缀是为了避免并发跑多个测试时互相覆盖;把结构化输出单独用固定前缀打印,是为了让 parser 匹配时不容易误伤;最后清临时文件是为了不给板子留垃圾。这几个点看着琐碎,但在批量、并发、长期跑的场景下,每一个都能省掉不少麻烦。
4.4 编写解析脚本与对齐判据
parser 这边,我对应的写法是围绕固定前缀做宽松匹配,并对缺失做兜底:
import re def parse(text): result = {} m = re.search(r"RESULT:\s*value=([0-9.]+)", text) if m: result["value"] = float(m.group(1)) else: # 明确记录缺失,而不是静默返回空 print("WARN: value not found in output") result["value"] = None return result这里的门道在于:re.search而不是re.match,因为输出可能有多行,目标信息不一定在开头;正则里用\s*容许冒号后有多余空格;兜底分支打警告而不是直接崩,这样你在结果日志里能立刻看到是解析失败。判据文件里就对应写value这个键和它的门槛。两边键名严格一致,这一步做完,整条链路的接口就对齐了。
4.5 跑第一次并读结果
第一次跑别急着批量,就单独跑这一个测试,然后重点看三个地方:一是执行日志,确认脚本确实在板子上跑起来了;二是 parser 的输出,确认指标被正确提取;三是判据比对结果,确认通过/失败符合预期。这三处任何一处不对,都回到对应文件去修。我第一次加测试时,就是在这里发现 parser 提取出的值一直是空,回头一看是脚本里 echo 的前缀和 parser 里匹配的前缀差了一个字符。这种错误在框架层面看不出来,只能靠这三处日志逐段核对。
5. 常见问题与排查速查
5.1 那些看起来像板子故障、其实是配置的问题
加测试过程中遇到的大部分"疑难杂症",根子不在硬件,而在配置和格式。我把高频问题整理成了下面这张表,基本覆盖了我自己踩过的和帮别人处理过的情况:
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 测试列表里看不到新测试 | 目录名或 spec 格式不被识别 | 核对 spec 文件名与版本是否匹配,目录名无特殊字符 |
| 测试一启动就超时失败 | 超时设太短或脚本卡在等待输入 | 放宽超时,检查脚本是否误读了标准输入 |
| 结果始终为空 | parser 匹配规则与输出格式不一致 | 对照板子原始输出,放宽正则 |
| 结果恒定通过或恒定失败 | 判据键名与指标名不匹配 | 逐字核对两边键名 |
| 时好时坏,偶发失败 | 门槛过严或存在未清理的残留状态 | 复核门槛裕量,补清理逻辑 |
| 脚本报找不到文件 | 相对路径问题 | 改用绝对路径或在开头显式 cd |
5.2 我总结的几条实操心得
第一条,也是最值钱的一条:永远保留一次完整的原始输出。不管 parser 写得多好,第一次跑的时候都该把板子回传的原始文本完整看一眼。这一步能提前发现 80% 的解析问题,因为你会直观看到输出长什么样,和你的正则想当然以为的样子对不对得上。我后来养成习惯,在脚本里把结合流重定向到一个日志文件,parser 解析的对象就是那份原始日志,出了问题直接翻原始日志,一目了然。
第二条,清理逻辑不是可选项。测试在板子上留下的临时文件、后台进程、占用的设备节点,如果不清,第一次可能没事,批量跑或者第二天再跑就会出各种奇怪的失败。我吃过一次亏:测试里启动的后台进程没杀干净,结果后续测试抢同一个设备节点,全都失败,排查了很久才发现是上一个测试的残留。
第三条,别在第一次就把门槛定死。先用一个非常宽松的判据让全流程跑通,确认链路正常,再逐步收紧门槛到合适值。这样你能区分"链路问题"和"门槛问题",不至于混在一起查。这个顺序听起来简单,但真能省掉大量返工。
第四条,脚本里的每个echo都要有理由。要么给人看,要么给机器解析,别为了"看起来详细"打印一堆无用信息。输出越干净,parser 越不容易误判,日志也越好读。这个习惯在测试数量涨到几十上百个的时候,价值会成倍放大。
5.3 后续扩展的思路
当你把一个最简单的功能测试跑通之后,扩展方向其实很自然:需要采数值就加 parser 提取逻辑和判据门槛,升级成性能型测试;需要多组参数就考虑用循环在脚本里跑多轮并分别输出结构化结果;需要先构建就先补上主机侧的构建步骤和依赖声明。每一步都是在已有骨架上加一块,而不是推倒重来,这也是这套约定式设计真正好用的地方——它把复杂的自动化测试,拆成了几个各管一段、边界清楚的简单文件。
我在实际使用中越来越觉得,Fuego 这类框架的价值不在它有多先进,而在它足够朴素、足够可拆解,让人能在资源受限的嵌入式环境里把自动化测试这件事真正落地。加测试的过程,本质上就是在和这套约定对话:你尊重它的分工,它就给你一个能跑、能判、能复现的结果。