CAPL 编程系列第三十一期-第三十四期回顾答案与分析

第 31 期:字符串处理 (Part 2) - 比较、查找与子串提取

  1. strncmp("TestData", "TestDone", 4) 返回值: 0。
    • 分析: strncmp 比较前 n 个字符。这里 n=4,两个字符串的前 4 个字符都是 "Test",完全相同,因此返回 0。
  2. 查找逗号索引: 使用 strchr
    • 分析: strchr(data, ',') 返回指向第一个逗号的指针。用该指针减去字符串起始地址 data 即可得到索引。需要检查返回值是否为 NULL (0) 以确定是否找到。代码片段: char *pos = strchr(info, ';'); long index = -1; if (pos != NULL) index = pos - info;
  3. 提取 "Data" 子串:
    • 分析:
      1. 确定起始指针: char *startPtr = msg + 6;
      2. 准备目标缓冲区: char buffer[5]; (长度 4 + 1 个 \0 位置)
      3. 复制子串: strncpy(buffer, startPtr, 4);
      4. 手动添加 null 终止符: buffer[4] = '\0'; (这是 strncpy 的关键点,当源长度不小于n时不自动添加)。
  4. strstr("Hello World", "World"):
    • 作用: 在第一个字符串中查找第二个字符串(子串)首次出现的位置。
    • 返回类型: char * (字符指针)。
    • 获取索引: char *ptr = strstr("Hello World", "World"); long index = -1; if(ptr != NULL) index = ptr - "Hello World";
  5. 长度计算 index2 - index1 - 1:
    • 分析: index2 - index1 计算的是从第一个逗号(不含)到第二个逗号(包含)之间的字符数,也就是 100km/h, 这部分的长度。为了只提取 100km/h,需要去掉末尾的逗号,因此长度再减 1。
  6. strncmp 返回值:
    • 0: 前 n 个字符相等。
    • 正数: 在第一个不匹配的字符处,s1 的字符大于 s2 的字符。
    • 负数: 在第一个不匹配的字符处,s1 的字符小于 s2 的字符。
  7. strchr 返回 NULL (0): 表示在 logLine 字符串中没有找到字符 'E'。
  8. 逻辑推理 (解析 "$CMD,PARAM1,PARAM2*CS"):
    • 使用 strchr(cmdString, ',') 找到第一个逗号的位置 (pos1)。
    • 使用 strncpy 提取从索引 1 (跳过 '$') 到 pos1 - 1 的字符作为 CMD
    • pos1 + 1 开始,使用 strchr 找到第二个逗号的位置 (pos2)。
    • 使用 strncpy 提取从 pos1 + 1pos2 - 1 的字符作为 PARAM1
    • pos2 + 1 开始,使用 strchr 找到星号 * 的位置 (pos3)。
    • 使用 strncpy 提取从 pos2 + 1pos3 - 1 的字符作为 PARAM2
    • (提取校验和 CS 可以类似地从 pos3 + 1 提取到字符串末尾,可能需要 strlen)
    • 注意: 每次 strncpy 后都要手动添加 \0

第 32 期:时间处理函数

  1. timeNow(): 返回自 CANoe/CANalyzer 测量开始 经过的时长。单位是 10 微秒 (10 µs)。返回类型是 dword
  2. timeNowNS() vs timeNow(): timeNowNS 单位是 纳秒 (ns),返回类型是 double (或 float);timeNow 单位是 10 微秒 (10 µs),返回类型是 dwordtimeNowNS 精度更高。
  3. timeNowNS() 转毫秒 (ms): double ms = timeNowNS_value / 1000000.0; (1 毫秒 = 1,000,000 纳秒)。
  4. getLocalTime(long timeArray[]): 获取运行脚本的计算机的当前系统日期和时间(绝对时间)。它不直接返回值,而是将时间的各个部分(年、月、日、时、分、秒等)填充到传入的 long 数组 timeArray
  5. tm[2]tm[3]: tm[2] 代表 小时 (0-23)tm[3] 代表 一个月中的第几天 (1-31)
  6. tm[4] (月份): 代表月份,取值范围是 0 (一月) 到 11 (十二月)。需要 加 1 得到实际月份。
  7. tm[5] (年份): 代表 自 1900 年经过的年数。需要 加上 1900 得到实际公元年份。
  8. 逻辑推理 (带时间间隔和时间戳的日志):
    • 定义静态变量 dword gLastMsgATime = 0;
    • on message MsgA 事件中:
      1. dword currentTime = timeNow(); (获取当前测量时间)
      2. double intervalMs = 0; if (gLastMsgATime != 0) intervalMs = (double)(currentTime - gLastMsgATime) / 100.0; (计算间隔,单位转为 ms,处理第一次接收情况)
      3. if (intervalMs > 500 || gLastMsgATime == 0) (判断间隔是否超 500ms 或是否为第一次)
      4. long tm[9]; getLocalTime(tm); (获取本地时间)
      5. int year=tm[5]+1900, month=tm[4]+1, day=tm[3], hr=tm[2], min=tm[1], sec=tm[0]; (提取并调整时间分量)
      6. write("%d-%02d-%02d %02d:%02d:%02d: MsgA received (Interval: %.0f ms)", year, month, day, hr, min, sec, intervalMs); (打印日志)
      7. gLastMsgATime = currentTime; (更新上次接收时间)

第 33 期:测试模块 (Test Module) 入门

  1. 关键字: testcase
    • 区别: testcase 明确标识该函数是一个可被 Test Module 框架识别和执行的测试用例,而普通函数(如 void MyHelper())是辅助逻辑,不直接作为测试用例执行。
  2. MainTest 作用: 测试模块的入口点,负责组织和调用该模块中需要执行的 testcase 函数,决定了测试的执行流程和顺序。
    • 规定: 名称必须是 MainTest,返回类型必须是 void
  3. MainTest 为空: 执行该测试模块时,MainTest 会被调用,但由于其内部没有调用任何 testcase 函数,因此不会有任何测试用例被执行。报告可能会显示模块已执行但包含 0 个测试用例。
  4. TestStepPass(...) 作用: 在测试报告中记录一个成功的测试步骤,包含步骤 ID ("Step 1.1") 和描述信息 ("Init OK")。
    • 调用位置: 通常在 testcase 函数内部,用于对某个检查点或操作结果进行断言。
  5. TestStepFail: 通常在测试检查未通过或预期条件未满足时调用。
    • 共同点: 与 TestStepPass 一样,都需要提供步骤 ID (字符串) 和描述信息 (字符串,可带格式化参数)。
  6. 监控面板: Test Module execution panel (测试模块执行面板)。
    • 信息: 显示执行的测试用例列表、每条用例的 Verdict (判定结果,如 Pass/Fail 图标)、执行耗时 (Duration)。
  7. 执行前提: CANoe 测量 (Measurement) 必须处于运行状态
  8. 详细结果: 在自动生成的测试报告 (Test Report) 文件中,使用 Vector CANoe Test Report Viewer 工具打开查看。
  9. 逻辑推理 (TestCase_CheckVoltage):
    Code snippet
    testcase TestCase_CheckVoltage()
    {
      double voltage = getSysVar(SysVar_Voltage); // 假设获取变量值
      if (voltage >= 4.8 && voltage <= 5.2) {
        TestStepPass("1.1", "Voltage check passed. Value: %.2fV", voltage);
      } else {
        TestStepFail("1.1", "Voltage check failed. Value: %.2fV (Expected: 4.8V-5.2V)", voltage);
      }
    }
    

第 34 期:测试模块 (Test Module) 进阶 - Test Setup 环境

  1. Test Setup 优点: 提供更好的组织性(层级结构)、集中管理测试资产、实现关注点分离(测试配置与仿真配置分开),更适合大型、规范化的测试项目。
  2. 访问 Test Setup: CANoe 菜单栏 -> Test -> Test Setup
  3. 创建步骤: 在 Test Setup 窗口中 (通常先创建 Test Environment),右键 -> Insert Test Module -> 配置属性(名称、关联 .can 文件、报告路径等)。
  4. 启动执行: 从 Test Setup 窗口Test Execution 窗口(也在 Test 菜单下)启动。
  5. 层级结构: Test Environment -> Test Module -> Test Group
  6. 修改生效: 保存 .can 文件后,通常需要重新编译 (Compile) 该 Test Module(可以通过 CANoe 工具栏按钮或 Test Setup 中的选项完成)。运行时 CANoe 也可能自动编译。
  7. 自动化测试价值: 体现了回归测试的价值,即在修改代码(修复 Bug)后,能够快速、自动地重新运行现有测试,以确保修复有效且没有引入新的问题。
  8. 执行方式不同: Test Setup 提供更灵活的执行控制(可选模块/组/用例),是专门的测试管理入口。Simulation Setup 中的节点运行通常是执行该节点关联的整个模块,方式较为简单直接。
  9. 逻辑推理 (项目组织):
    • 创建一个顶层 Test Environment,命名为 "ECU Integration Tests"。
    • 在该 Environment 下创建三个 Test Modules: "Gateway_Tests", "Cluster_Tests", "Infotainment_Tests"。
    • 分别为这三个 Module 关联不同的 .can 脚本文件 (如 Gateway.can, Cluster.can, Infotainment.can),存放在 Test Modules 文件夹中。
    • 根据需要在每个 .can 文件的 MainTest 中使用 TestGroupBegin/End 组织内部用例 (例如,Cluster_Tests 可能有 "Display_Checks" 和 "Input_Handling" 两个 Test Group)。
    • 配置所有 Module 的报告输出到 Test Reports 文件夹,可能使用不同的前缀命名。

第 35 期:测试模块 (Test Module) - 用例组织与报告描述

  1. TestGroupBegin/End: 用于在 MainTest 函数中逻辑地组织相关的 testcase 调用,形成测试组。
    • 调用位置: MainTest 函数。
  2. 报告结构影响: 在 Test Report Viewer 中创建可折叠的层级结构,将 testcase 显示在其所属的 TestGroup 之下,使报告结构更清晰,便于导航。
  3. TestModuleTitle/Description: 设置整个 Test Module 的自定义标题和描述信息,显示在测试报告的概要或头部区域。
    • 调用位置: 通常在 MainTest 函数的开头。
  4. TestCaseTitle/Description: 设置单个 testcase 的自定义标题 (含 ID) 和详细描述信息,显示在测试报告的用例详情部分。
    • 调用位置: 在对应的 testcase 函数内部,通常在函数开头。
  5. "TC_PWR_01" 作用: 它是该测试用例的唯一字符串标识符 (ID)。它会显示在测试报告中,通常与 TestCaseTitle 一起出现,用于测试追溯(例如,关联到测试需求或 JIRA ID)。
  6. 为何添加自定义文本: 提高测试报告的可读性、清晰度和上下文信息,使得报告更容易被团队成员(包括非编写者)理解测试的目的、范围和结果。
  7. TestGroupBegin 参数: 第一个参数是 groupID (字符串,组的标识符/名称),第二个参数是 description (字符串,组的详细描述)。
  8. 逻辑推理 (实现报告效果):
    • MainTest():
      • TestModuleTitle("XXX ECU V1.2 测试报告");
      • TestModuleDescription("本模块包含对 XXX ECU 的初始化流程和核心功能的测试。");
      • TestGroupBegin("InitGroup", "初始化测试");
      • TestCase_Init_01(); // 调用初始化用例1
      • TestCase_Init_02();
      • TestCase_Init_03();
      • TestGroupEnd();
      • TestGroupBegin("FuncGroup", "功能测试");
      • TestCase_Func_01(); // 调用功能用例1
      • TestCase_Func_02();
      • TestCase_Func_03();
      • TestCase_Func_04();
      • TestCase_Func_05();
      • TestGroupEnd();
    • testcase TestCase_Init_01():
      • TestCaseTitle("INIT_01", "检查电源上电状态");
      • TestCaseDescription("验证 ECU 上电后,电源状态是否正常...");
      • // ... 测试逻辑 ...
    • testcase TestCase_Func_01():
      • TestCaseTitle("FUNC_01", "验证 A 功能响应");
      • TestCaseDescription("发送 A 功能触发指令,检查 ECU 是否正确响应...");
      • // ... 测试逻辑 ...
    • 对所有其他 testcase 函数重复类似操作,提供唯一的 ID 和相应的中文标题/描述。