❄️ 我的个人专栏: 
《智能软件工程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升级模块的可靠性与安全性,降低设备在真实环境中的升级失败风险。

更多推荐