第 31 期:字符串处理 (Part 2) - 比较、查找与子串提取
strncmp("TestData", "TestDone", 4)返回值: 0。- 分析:
strncmp比较前n个字符。这里n=4,两个字符串的前 4 个字符都是 "Test",完全相同,因此返回 0。
- 分析:
- 查找逗号索引: 使用
strchr。 - 提取 "Data" 子串:
- 分析:
- 确定起始指针:
char *startPtr = msg + 6; - 准备目标缓冲区:
char buffer[5];(长度 4 + 1 个\0位置) - 复制子串:
strncpy(buffer, startPtr, 4); - 手动添加 null 终止符:
buffer[4] = '\0';(这是strncpy的关键点,当源长度不小于n时不自动添加)。
- 确定起始指针:
- 分析:
strstr("Hello World", "World"):- 作用: 在第一个字符串中查找第二个字符串(子串)首次出现的位置。
- 返回类型:
char *(字符指针)。 - 获取索引:
char *ptr = strstr("Hello World", "World"); long index = -1; if(ptr != NULL) index = ptr - "Hello World";
- 长度计算
index2 - index1 - 1:- 分析:
index2 - index1计算的是从第一个逗号(不含)到第二个逗号(包含)之间的字符数,也就是100km/h,这部分的长度。为了只提取100km/h,需要去掉末尾的逗号,因此长度再减 1。
- 分析:
strncmp返回值:- 0: 前
n个字符相等。 - 正数: 在第一个不匹配的字符处,
s1的字符大于s2的字符。 - 负数: 在第一个不匹配的字符处,
s1的字符小于s2的字符。
- 0: 前
strchr返回NULL(0): 表示在logLine字符串中没有找到字符 'E'。- 逻辑推理 (解析 "$CMD,PARAM1,PARAM2*CS"):
- 使用
strchr(cmdString, ',')找到第一个逗号的位置 (pos1)。 - 使用
strncpy提取从索引 1 (跳过 '$') 到pos1 - 1的字符作为CMD。 - 从
pos1 + 1开始,使用strchr找到第二个逗号的位置 (pos2)。 - 使用
strncpy提取从pos1 + 1到pos2 - 1的字符作为PARAM1。 - 从
pos2 + 1开始,使用strchr找到星号*的位置 (pos3)。 - 使用
strncpy提取从pos2 + 1到pos3 - 1的字符作为PARAM2。 - (提取校验和 CS 可以类似地从
pos3 + 1提取到字符串末尾,可能需要strlen) - 注意: 每次
strncpy后都要手动添加\0。
- 使用
第 32 期:时间处理函数
timeNow(): 返回自 CANoe/CANalyzer 测量开始 经过的时长。单位是 10 微秒 (10 µs)。返回类型是dword。timeNowNS()vstimeNow():timeNowNS单位是 纳秒 (ns),返回类型是double(或float);timeNow单位是 10 微秒 (10 µs),返回类型是dword。timeNowNS精度更高。timeNowNS()转毫秒 (ms):double ms = timeNowNS_value / 1000000.0;(1 毫秒 = 1,000,000 纳秒)。getLocalTime(long timeArray[]): 获取运行脚本的计算机的当前系统日期和时间(绝对时间)。它不直接返回值,而是将时间的各个部分(年、月、日、时、分、秒等)填充到传入的long数组timeArray中。tm[2]和tm[3]:tm[2]代表 小时 (0-23)。tm[3]代表 一个月中的第几天 (1-31)。tm[4](月份): 代表月份,取值范围是 0 (一月) 到 11 (十二月)。需要 加 1 得到实际月份。tm[5](年份): 代表 自 1900 年经过的年数。需要 加上 1900 得到实际公元年份。- 逻辑推理 (带时间间隔和时间戳的日志):
- 定义静态变量
dword gLastMsgATime = 0; - 在
on message MsgA事件中:dword currentTime = timeNow();(获取当前测量时间)double intervalMs = 0; if (gLastMsgATime != 0) intervalMs = (double)(currentTime - gLastMsgATime) / 100.0;(计算间隔,单位转为 ms,处理第一次接收情况)if (intervalMs > 500 || gLastMsgATime == 0)(判断间隔是否超 500ms 或是否为第一次)long tm[9]; getLocalTime(tm);(获取本地时间)int year=tm[5]+1900, month=tm[4]+1, day=tm[3], hr=tm[2], min=tm[1], sec=tm[0];(提取并调整时间分量)write("%d-%02d-%02d %02d:%02d:%02d: MsgA received (Interval: %.0f ms)", year, month, day, hr, min, sec, intervalMs);(打印日志)gLastMsgATime = currentTime;(更新上次接收时间)
- 定义静态变量
第 33 期:测试模块 (Test Module) 入门
- 关键字:
testcase。- 区别:
testcase明确标识该函数是一个可被 Test Module 框架识别和执行的测试用例,而普通函数(如void MyHelper())是辅助逻辑,不直接作为测试用例执行。
- 区别:
MainTest作用: 测试模块的入口点,负责组织和调用该模块中需要执行的testcase函数,决定了测试的执行流程和顺序。- 规定: 名称必须是
MainTest,返回类型必须是void。
- 规定: 名称必须是
MainTest为空: 执行该测试模块时,MainTest会被调用,但由于其内部没有调用任何testcase函数,因此不会有任何测试用例被执行。报告可能会显示模块已执行但包含 0 个测试用例。TestStepPass(...)作用: 在测试报告中记录一个成功的测试步骤,包含步骤 ID ("Step 1.1") 和描述信息 ("Init OK")。- 调用位置: 通常在
testcase函数内部,用于对某个检查点或操作结果进行断言。
- 调用位置: 通常在
TestStepFail: 通常在测试检查未通过或预期条件未满足时调用。- 共同点: 与
TestStepPass一样,都需要提供步骤 ID (字符串) 和描述信息 (字符串,可带格式化参数)。
- 共同点: 与
- 监控面板: Test Module execution panel (测试模块执行面板)。
- 信息: 显示执行的测试用例列表、每条用例的 Verdict (判定结果,如 Pass/Fail 图标)、执行耗时 (Duration)。
- 执行前提: CANoe 测量 (Measurement) 必须处于运行状态。
- 详细结果: 在自动生成的测试报告 (Test Report) 文件中,使用 Vector CANoe Test Report Viewer 工具打开查看。
- 逻辑推理 (
TestCase_CheckVoltage):Code snippettestcase 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 环境
- Test Setup 优点: 提供更好的组织性(层级结构)、集中管理测试资产、实现关注点分离(测试配置与仿真配置分开),更适合大型、规范化的测试项目。
- 访问 Test Setup: CANoe 菜单栏 -> Test -> Test Setup。
- 创建步骤: 在 Test Setup 窗口中 (通常先创建 Test Environment),右键 -> Insert Test Module -> 配置属性(名称、关联
.can文件、报告路径等)。 - 启动执行: 从 Test Setup 窗口 或 Test Execution 窗口(也在 Test 菜单下)启动。
- 层级结构: Test Environment -> Test Module -> Test Group。
- 修改生效: 保存
.can文件后,通常需要重新编译 (Compile) 该 Test Module(可以通过 CANoe 工具栏按钮或 Test Setup 中的选项完成)。运行时 CANoe 也可能自动编译。 - 自动化测试价值: 体现了回归测试的价值,即在修改代码(修复 Bug)后,能够快速、自动地重新运行现有测试,以确保修复有效且没有引入新的问题。
- 执行方式不同: Test Setup 提供更灵活的执行控制(可选模块/组/用例),是专门的测试管理入口。Simulation Setup 中的节点运行通常是执行该节点关联的整个模块,方式较为简单直接。
- 逻辑推理 (项目组织):
- 创建一个顶层 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) - 用例组织与报告描述
TestGroupBegin/End: 用于在MainTest函数中逻辑地组织相关的testcase调用,形成测试组。- 调用位置:
MainTest函数。
- 调用位置:
- 报告结构影响: 在 Test Report Viewer 中创建可折叠的层级结构,将
testcase显示在其所属的TestGroup之下,使报告结构更清晰,便于导航。 TestModuleTitle/Description: 设置整个 Test Module 的自定义标题和描述信息,显示在测试报告的概要或头部区域。- 调用位置: 通常在
MainTest函数的开头。
- 调用位置: 通常在
TestCaseTitle/Description: 设置单个testcase的自定义标题 (含 ID) 和详细描述信息,显示在测试报告的用例详情部分。- 调用位置: 在对应的
testcase函数内部,通常在函数开头。
- 调用位置: 在对应的
"TC_PWR_01"作用: 它是该测试用例的唯一字符串标识符 (ID)。它会显示在测试报告中,通常与TestCaseTitle一起出现,用于测试追溯(例如,关联到测试需求或 JIRA ID)。- 为何添加自定义文本: 提高测试报告的可读性、清晰度和上下文信息,使得报告更容易被团队成员(包括非编写者)理解测试的目的、范围和结果。
TestGroupBegin参数: 第一个参数是groupID(字符串,组的标识符/名称),第二个参数是description(字符串,组的详细描述)。- 逻辑推理 (实现报告效果):
- 在
MainTest()中:TestModuleTitle("XXX ECU V1.2 测试报告");TestModuleDescription("本模块包含对 XXX ECU 的初始化流程和核心功能的测试。");TestGroupBegin("InitGroup", "初始化测试");TestCase_Init_01();// 调用初始化用例1TestCase_Init_02();TestCase_Init_03();TestGroupEnd();TestGroupBegin("FuncGroup", "功能测试");TestCase_Func_01();// 调用功能用例1TestCase_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 和相应的中文标题/描述。
- 在