嵌入式软件单元测试(四十九)——物联网OTA升级模块单元测试:断点续传与签名校验的逻辑覆盖
❄️ 我的个人专栏:
《智能软件工程AI4SE》
《嵌入式面试总结》
《嵌入式处理器架构解析》
《嵌入式与虚拟化》
《嵌入式软件测试》
🌟 Simplicity is the ultimate sophistication
摘要:本文围绕物联网OTA升级模块的断点续传与签名校验两个核心功能,系统分析其逻辑分支并设计单元测试用例。断点续传部分重点覆盖进度记录的有效性判断、分片写入的边界条件与恢复流程;签名校验部分重点覆盖输入参数校验、篡改检测以及校验结果与升级流程的联动。文中给出基于CUnit的测试用例矩阵与C语言测试代码示例,并结合覆盖率工具提出改进建议,帮助提升OTA升级模块的可靠性与安全性。
1. 引言
物联网设备在长期运行过程中,固件升级是保障功能迭代与安全修复的关键环节。OTA(Over-The-Air)升级模块作为设备与云端之间的桥梁,其可靠性与安全性直接关系到设备的稳定运行。在OTA升级过程中,断点续传与签名校验是两个核心功能点:前者决定了弱网环境下升级能否顺利完成,后者决定了固件包是否可信、是否被篡改。本文围绕这两个功能点,讨论如何设计单元测试用例,实现逻辑覆盖。
2. 被测模块概述
本文讨论的OTA升级模块采用C语言实现,主要包含以下核心函数:
- ota_download_start:启动一次固件下载任务,初始化下载上下文。
- ota_download_chunk:接收并写入一个数据分片,支持断点续传。
- ota_download_finish:完成下载,触发签名校验。
- ota_verify_signature:对固件包进行签名校验,返回校验结果。
- ota_upgrade_apply:校验通过后,执行固件写入与重启。
模块内部维护一个下载上下文结构体,记录已接收字节数、总字节数、分片序号、临时文件句柄等状态信息。
3. 断点续传的逻辑覆盖
断点续传的核心逻辑在于:当网络中断或设备重启后,升级任务能够从上次中断的位置继续下载,而不是从头开始。这要求模块能够正确记录和恢复下载进度。
3.1 断点续传的状态记录
下载进度通常记录在持久化存储中,例如Flash或文件系统。单元测试需要验证以下状态字段的正确读写:
- 已接收字节数(received_bytes)
- 固件总字节数(total_bytes)
- 当前分片序号(chunk_index)
- 临时文件偏移量(file_offset)
测试用例应覆盖状态保存成功、状态保存失败、状态读取成功、状态读取失败(文件不存在或校验和错误)等分支。
3.2 断点续传的恢复流程
当模块重新启动下载任务时,应首先读取持久化的进度信息。测试需要覆盖以下场景:
- 存在有效的断点记录,从断点处继续下载。
- 不存在断点记录,从头开始下载。
- 断点记录中的总字节数与当前固件包不一致,判定为无效断点,重新下载。
- 断点记录损坏(校验和错误),判定为无效断点,重新下载。
3.3 分片写入的边界条件
分片写入是断点续传的基础操作。单元测试需要覆盖以下边界条件:
- 分片长度为零,应返回参数错误。
- 分片写入位置超出固件总长度,应返回越界错误。
- 分片写入位置与当前偏移量不连续(中间有空洞),应返回顺序错误。
- 最后一个分片恰好填满固件总长度,应标记下载完成。
- 分片写入后,已接收字节数正确累加。
4. 签名校验的逻辑覆盖
签名校验是OTA升级的安全防线。固件包在云端使用私钥签名,设备端使用预置公钥验证签名。只有校验通过的固件包才能被写入系统分区。
4.1 签名校验的输入条件
签名校验函数通常接收以下输入:
- 固件包数据缓冲区
- 固件包长度
- 签名数据缓冲区
- 签名长度
- 公钥索引或公钥缓冲区
测试用例需要覆盖输入参数为空、长度为零、长度超限等异常分支。
4.2 签名校验的分支覆盖
签名校验的核心分支包括:
- 签名长度与算法要求不匹配,返回校验失败。
- 公钥无效或公钥索引越界,返回校验失败。
- 固件包数据被篡改(任意字节翻转),返回校验失败。
- 签名数据被篡改,返回校验失败。
- 固件包与签名均正确,返回校验成功。
- 校验算法内部错误(如内存不足),返回错误码而非校验失败。
4.3 签名校验与升级流程的联动
签名校验结果直接影响后续升级流程。测试需要覆盖:
- 校验成功,允许执行固件写入。
- 校验失败,拒绝写入并清理临时文件。
- 校验失败,记录错误日志并上报云端。
- 校验失败后,设备保持原有固件正常运行。
5. 单元测试用例设计
基于上述逻辑分析,设计以下测试用例矩阵。测试框架采用CUnit,桩函数用于模拟存储读写、网络接收和加密库调用。
5.1 断点续传测试用例
| 用例编号 | 测试场景 | 输入条件 | 预期结果 |
|---|---|---|---|
| TC-RS-001 | 无断点记录,全新下载 | 存储中无进度记录 | 从偏移量0开始下载 |
| TC-RS-002 | 有效断点,继续下载 | 断点记录:received=1024,total=4096 | 从偏移量1024开始下载 |
| TC-RS-003 | 断点总字节数不匹配 | 断点记录total=2048,新固件total=4096 | 判定无效,重新下载 |
| TC-RS-004 | 断点记录校验和错误 | 断点记录CRC校验失败 | 判定无效,重新下载 |
| TC-RS-005 | 分片长度为零 | chunk_len=0 | 返回参数错误 |
| TC-RS-006 | 分片越界 | offset+chunk_len > total_bytes | 返回越界错误 |
| TC-RS-007 | 分片顺序不连续 | 当前offset=1024,新分片offset=2048 | 返回顺序错误 |
| TC-RS-008 | 最后一个分片填满 | offset+chunk_len == total_bytes | 标记下载完成 |
5.2 签名校验测试用例
| 用例编号 | 测试场景 | 输入条件 | 预期结果 |
|---|---|---|---|
| TC-SV-001 | 固件包为空 | firmware=NULL,fw_len=0 | 返回参数错误 |
| TC-SV-002 | 签名为空 | signature=NULL,sig_len=0 | 返回参数错误 |
| TC-SV-003 | 签名长度不匹配 | sig_len与算法要求不符 | 返回校验失败 |
| TC-SV-004 | 公钥索引越界 | key_index超出公钥表范围 | 返回校验失败 |
| TC-SV-005 | 固件包被篡改 | 固件包中翻转一个字节 | 返回校验失败 |
| TC-SV-006 | 签名被篡改 | 签名数据翻转一个字节 | 返回校验失败 |
| TC-SV-007 | 固件与签名均正确 | 使用测试密钥对生成合法签名 | 返回校验成功 |
| TC-SV-008 | 校验算法内部错误 | 桩函数返回内存不足错误 | 返回错误码,不进入升级流程 |
6. 测试代码示例
下面给出断点续传恢复流程的测试代码示例,演示如何通过桩函数模拟存储读取。
#include <stdio.h>
#include <string.h>
#include "CUnit/Basic.h"
#include "ota_module.h"
/* 桩函数:模拟从存储读取断点记录 */
static int stub_load_progress(ota_progress_t *progress)
{
/* 模拟读取到有效断点:已接收1024字节,总长4096字节 */
progress->received_bytes = 1024;
progress->total_bytes = 4096;
progress->chunk_index = 4;
progress->file_offset = 1024;
return OTA_OK;
}
static int stub_load_progress_not_found(ota_progress_t *progress)
{
(void)progress;
return OTA_ERR_NO_RECORD;
}
static int stub_load_progress_corrupt(ota_progress_t *progress)
{
(void)progress;
return OTA_ERR_CRC_MISMATCH;
}
/* 用例:无断点记录,从头下载 */
static void test_resume_no_record(void)
{
ota_context_t ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.load_progress = stub_load_progress_not_found;
int ret = ota_download_start(&ctx, 4096);
CU_ASSERT_EQUAL(ret, OTA_OK);
CU_ASSERT_EQUAL(ctx.file_offset, 0);
}
/* 用例:有效断点,从断点处继续 */
static void test_resume_valid_record(void)
{
ota_context_t ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.load_progress = stub_load_progress;
int ret = ota_download_start(&ctx, 4096);
CU_ASSERT_EQUAL(ret, OTA_OK);
CU_ASSERT_EQUAL(ctx.file_offset, 1024);
}
/* 用例:断点记录损坏,重新下载 */
static void test_resume_corrupt_record(void)
{
ota_context_t ctx;
memset(&ctx, 0, sizeof(ctx));
ctx.load_progress = stub_load_progress_corrupt;
int ret = ota_download_start(&ctx, 4096);
CU_ASSERT_EQUAL(ret, OTA_OK);
CU_ASSERT_EQUAL(ctx.file_offset, 0);
}
/* 注册测试套件 */
void register_resume_tests(void)
{
CU_pSuite suite = CU_add_suite("OTA_Resume_Suite", NULL, NULL);
CU_add_test(suite, "resume_no_record", test_resume_no_record);
CU_add_test(suite, "resume_valid_record", test_resume_valid_record);
CU_add_test(suite, "resume_corrupt_record", test_resume_corrupt_record);
}
7. 覆盖率分析与改进
通过上述测试用例,可以覆盖断点续传与签名校验的主要逻辑分支。在实际项目中,建议结合覆盖率工具(如gcov、LCOV)统计行覆盖、分支覆盖和条件覆盖,重点关注以下薄弱环节:
- 错误处理路径:存储读写失败、加密库异常等分支容易被遗漏。
- 边界条件:分片恰好填满、签名长度恰好等于算法要求等临界值。
- 状态机转换:下载中、下载完成、校验中、校验失败、升级中、升级完成等状态之间的非法转换。
对于覆盖率未达标的模块,应补充针对性的测试用例,确保关键逻辑分支全部被执行。
8. 总结
本文围绕物联网OTA升级模块的断点续传与签名校验两个核心功能,分析了其逻辑分支,并设计了对应的单元测试用例。断点续传测试重点覆盖进度记录的有效性判断、分片写入的边界条件和恢复流程;签名校验测试重点覆盖输入参数校验、篡改检测和校验结果与升级流程的联动。通过系统的逻辑覆盖,可以有效提升OTA升级模块的可靠性与安全性,降低设备在真实环境中的升级失败风险。
更多推荐

所有评论(0)