前言
在汽车电子开发与测试中,诊断功能是保障 ECU 正常工作、快速定位故障的关键环节。CANoe 作为 Vector 公司推出的总线开发与测试工具,提供了强大的诊断仿真和测试能力,而 CAPL 语言则让工程师能够灵活地编写自动化诊断测试脚本。
本文围绕 CANoe 中的 CAPL 诊断测试展开,系统介绍从诊断协议基础到安全解锁的完整知识体系,涵盖诊断描述文件、传输层与诊断层参数、常用诊断函数、通用函数封装以及安全解锁等核心内容。
一、诊断协议介绍
ISO 14229-1 是一项国际标准,用于规范汽车诊断通信,也被称为 UDS。标准定义了诊断通信的数据通信方式,命令结构,数据格式和错误码,也定义了诊断仪器和车辆之间的通信方式,包括物理层和传输层协议;
ISO 14229-1 处于 OSI 模型中的顶层,该标准适用于所有的网络协议;
CAN 总线的物理层和传输层的协议为 ISO 11898-2 和 ISO 15765-2;
LIN 总线的物理层和传输层的协议为 ISO 17987-4 和 ISO 17987-2;
以太网的物理层和传输层的协议为 ISO 13400-3 和 ISO 13400-2;
1. 诊断描述文件
CANoe 的诊断功能非常强大,常见的诊断描述文件的格式有 CDD,ODX,PDX;
CDD:CANdela Diagnostic Description,是 Vector 公司开发的一种诊断描述文件,可通过 CANdelaStudio 软件创建和编辑;
ODX:Open Diagnostic Data Exchange,是全球通用的诊断描述格式,可通过 ODXStudio 软件创建和编辑;
PDX:Open Diagnostic Data Exchange/Packed ODX,可以将多个 ODX 文件打包成一个 PDX 文件;
2. 诊断描述信息

ECU qualifier:诊断目标 ECU 的名称,该参数在 CANoe 环境中必须是唯一的。在 CAPL 代码中使用 diagSetTarget(char ecuName[]) 函数设置诊断对象时,ecuName 参数传递的值就是 ECU qualifier 参数的值;
Diagnostic tester:在 CANoe 中使用诊断控制台发送诊断请求时,用户可以选择使用物理地址发送诊断或者使用功能地址发送诊断;
Simulation by:如果对仿真的 ECU 发送诊断,这里需要选择 ECU,如果是真实在线的 ECU,无需选择;
3. 传输层参数

Override manually:如果诊断描述文件的信息和实际总线信息不匹配,可以勾选该选项,手动更改参数;
Addressing:定义了诊断请求的物理寻址,功能寻址,以及诊断响应的报文 ID;
STmin:多包之间最小的响应间隔时间;
Block size:流控帧中接收方最多一次性接收的包数;
FC delay:流控帧的延迟时间;
MaxLength:诊断能支持的最大的包长;
4. 诊断层参数

Override manually:如果加载的诊断文件信息和实际总线信息不匹配,可以勾选该选项,手动更改参数;
Tester Present's request:如果勾选这个选项,在诊断仪和 ECU 建立了诊断会话之后,CANoe 将周期行的发送 0x3E 服务来维持诊断会话在线。在 CAPL 中可以使用 diagStopTesterPresent() 和 diagStartTesterPresent() 禁用和启用该功能;
S3 client time:CANoe 自动发送会话保持诊断请求的周期;
S3 server time:服务器离开非默认会话的超时时间,这个时间必须要大于 S3 client time;
P2 client:客户端成功发送请求消息,到接收到服务端的应答消息的超时时间;
P2 extended client:客户端成功发送请求消息,在接收到服务端响应的 NRC 0x78 之后,客户端使用此时间再次等待 ECU 响应;
P2 Server:服务端在接收到客户端的请求消息后,开始答复消息的运行时间,典型值为 50ms;
P2 extended server:如果服务端在 P2 Server 时间内无法给出答复,则可以响应 NRC 0x78,之后服务器必须在 P2 Server Extended 时间内给出响应,也可以继续响应 NRC 0x78,再重置该定时器;
Seed & Key DLL:如果客户端请求的服务器需要使用 0x27 服务安全解锁之后才能请求,则需要在此加载 DLL 文件;
二、传输层协议
一帧 CAN/LIN 报文最多可以携带 8 个字节的有效数据,一帧 CANFD 报文最多可以携带 64 字节的有效数据,一帧 Flexray 报文最多可以携带 254 字节的有效数据,一帧标准以太网报文最多可以携带 1500 字节有效数据。
在应用层,ISO 14229-1 协议规定诊断数据的长度是可以大于单帧报文的负载长度的。传输层负责的内容就是将数据进行有效得分组传输。
不同的总线类型对应得传输协议标准不同,CAN 总线是基于 ISO 15765-2 进行传输的。
协议控制信息(N_PCI)将 CAN 总线的诊断数据分为单帧(Single Frame,SF),首帧(First Frame,FF),连续帧(Consecutive Frame,CF)和流控帧(Flow Control Frame,FC)。
| N_PDU name | N_PCI Bytes | ||||||
| Byte1 Bit7-4 | Byte1 Bit7-4 | Byte2 | Byte3 | Byte4 | Byte5 | Byte6 | |
|
SingleFrame(SF) (CAN_DL <= 8) | 0000 | SF_DL | - | - | - | - | - |
|
SingleFrame(SF) (CAN_DL > 8) | 0000 | 0000 | SF_DL | - | - | - | - |
|
FirstFrame(FF) (FF_DL <= 4095) | 0001 | FF_DL | - | - | - | - | |
|
FirstFrame(FF) (FF_DL > 4095) | 0001 | 0000 | 0000 0000 | FF_DL | |||
| ConsecutiveFrame(CF) | 0010 | SN | - | - | - | - | - |
| FlowControl(FC) | 0011 | FS | BS | STmin | - | - | - |
三、CAPL 诊断函数
1. 发送诊断请求
下列函数可以以诊断物理寻址或功能寻址的方式发送诊断请求;
long diagSendRequest(diagRequest request):以物理寻址方式发送诊断请求;
long diagSendFunctional(diagRequest request):以功能寻址方式发送诊断请求;
2. 诊断目标设置
如果 CANoe 工程中加载了多个 ECU 的诊断描述文件,则不建议直接发送诊断请求,而是要先设置诊断目标 ECU;
指定了诊断的 ECU,代码就只能用于测试当前 ECU;
long diagSetTarget(char ecuName[]):设置诊断目标;
3. 等待诊断请求发送
long testWaitForDiagRequestSent(diagRequest request, dword timeout):返回地响应值为 1,表示诊断请求发送到总线上,在工程应用中,需要再次使用这个函数确认;
4. 等待接收诊断响应
long testWaitForDiagResponse(diagRequest request, dword timeout):等待接收诊断响应;
long testWaitForDiagResponse(dword timeout):不指定诊断请求,等待接收诊断响应;
返回值为 1 表示接收到诊断响应;
返回值为 0 表示超时,指定时间内没接收到诊断响应;
返回值为 < 0 表示内部错误;
函数收到肯定响应或者否定响应都会立即退出,如果收到 NRC 0x78,该函数会使用 P2*Server 时间继续等待,知道 timeout 参数超时;
5. 判断响应类型
testWaitForDiagResponse() 函数只能判断 ECU 是否返回响应,但无法判断是肯定响应还是否定响应;
long diagGetLastResponseCode(diagRequest request):获取最后一次响应结果;
long diagGetLastResponseCode():获取最后一次响应结果;
返回值 -1:肯定响应;
返回值 0:超时,无响应;
返回值 > 0:否定响应,返回 NRC 响应码;
6. 打印诊断数据到测试报告
下列函数可以把诊断请求数据和诊断响应数据输出到测试报告中;
long testReportWriteDiagObject(diagRequest request):打印诊断请求数据;
long testReportWriteDiagResponse(diagRequest request):打印诊断响应数据;
7. 代码示例
variables
{
diagRequest * request;
char ecuName[20] = "Door";
}
testcase TC_001_diagFunction_Test()
{
byte data[2] = {0x10, 0x01};
long ret;
// 1. 指定诊断节点
diagSetTarget(ecuName);
// 2. 设置诊断请求
diagResize(request, 2);
diagSetPrimitiveData(request, data, elCount(data));
// 3. 发送诊断请求
ret = diagSendRequest(request);
testStep("", "pysical address, ret = %d", ret);
if(ret != 0)
return;
// 4. 确认诊断请求发送到总线上
ret = testWaitForDiagRequestSent(request, 1000);
if(ret == 1)
{
testStepPass("", "Request was sent successfully");
testReportWriteDiagObject(request); // 打印诊断请求
}
else
{
testStepFail("", "Request could not be sent!");
return;
}
// 5. 确认接收到诊断响应
ret = testWaitForDiagResponse(request, 5000);
if(ret != 1)
{
testStepFail("No response received or internal error.");
return;
}
testStepPass("Received ECU response successfully.");
testReportWriteDiagResponse(request); // 打印诊断响应
// 6. 判断响应类型
ret = diagGetLastResponseCode(request);
if(ret >= 0)
{
testStepFail("", "Negative response, NRC code is 0x%X", ret);
return;
}
testStepPass("Received ECU positive response successfully");
}
测试结果:

四、诊断通用函数(一)
基于上述诊断函数,可以将上述代码封装成通用的函数,提高代码的复用度;
void diag_sendRequest(diagRequest request):发送诊断请求并期望 ECU 肯定应答;
void diag_SendRequest(diagRequest* request)
{
long ret;
// 1. 发送诊断请求
ret = diagSendRequest(request);
testStep("", "pysical address, ret = %d", ret);
if(ret != 0)
{
testStepFail("Request was sent failed");
return;
}
// 2. 确认诊断请求发送到总线上
ret = testWaitForDiagRequestSent(request, 1000);
if(ret != 1)
{
testStepFail("", "Request could not be sent!");
return;
}
testStepPass("", "Request was sent successfully");
testReportWriteDiagObject(request); // 打印诊断请求
// 5. 确认接收到诊断响应
ret = testWaitForDiagResponse(request, 5000);
if(ret != 1)
{
testStepFail("No response received or internal error.");
return;
}
testStepPass("Received ECU response successfully.");
testReportWriteDiagResponse(request); // 打印诊断响应
// 6. 判断响应类型
ret = diagGetLastResponseCode(request);
if(ret >= 0)
{
testStepFail("", "Negative response, NRC code is 0x%X", ret);
return;
}
testStepPass("Received ECU positive response successfully");
}
测试代码:
variables
{
diagRequest * request;
char ecuName[20] = "Door";
}
testcase TC_002_sendRequestFunction_Test()
{
byte data[2] = {0x10, 0x01};
diagSetTarget(ecuName);
diagResize(request, 2);
diagSetPrimitiveData(request, data, elCount(data));
diag_SendRequest(request);
}
测试结果:

五、诊断服务数据设置
1. 诊断参数设置
long diagSetParameter(diagRequest request, char parameterName[], double newValue):更改请求的参数值,发送不同的诊断数据;
long diagSetParameter(diagRequest request, char parameterName[], char newValue[]):更改请求的参数值,发送不同的诊断书数据;
这个函数能够修改诊断参数,需要提前知道诊断参数的名称,实际测试中并不好用;如果使用 testWaitForDiagResponse() 接收诊断响应,容易遇到接收不到诊断响应的问题;
testcase TC_003_setRequestParameter_Test()
{
diagRequest Door.DefaultSession_Start request;
long ret;
diagSetTarget(ecuName);
diagSetParameter(request, "DiagSessionType", 0x03);
diag_SendRequest(request);
}

实际上查看 log 确实可以接收到响应,但是使用 testWaitForDiagResponse() 接收不到;

2. 单字节设置诊断服务
long diagSetPrimitiveByte(diagRequest request, dword bytePos, dword newValue):设置诊断服务指定位置的字节进行重新赋值;
testcase TC_004_setPrimitiveByte_Test()
{
diagRequest Door.DefaultSession_Start request;
long ret;
diagSetTarget(ecuName);
DiagSetPrimitiveByte(request, 1, 0x03);
diag_SendRequest(request);
}

3. 设置全部的诊断服务
long diagSetPrimitiveData(diagRequest request, byte* buffer, dword buffersize):重置诊断服务的全部数据;
variables
{
diagRequest * request;
char ecuName[20] = "Door";
}
testcase TC_005_setPrimitiveData_Test()
{
long ret;
byte data[4095];
diagSetTarget(ecuName);
diagResize(request, 2);
data[0] = 0x10;
data[1] = 0x02;
diagSetPrimitiveData(request, data, 2);
diag_SendRequest(request);
}


通常来说,设置全部的诊断服务更加常用,它可以搭配通用的诊断请求(使用通配符定义的诊断请求),在测试中更加灵活;
搭配通用的诊断请求,需要注意,一定要设置诊断请求的字节数,diagResize(diagRequest request, dword buteNum),否则无法确定诊断请求有多少个字节。
六、诊断通用函数(二)
1. 通用诊断函数(一)
我们可以基于上面的诊断服务数据设置的方法,封装一个通用性更强的函数,这个函数不必依赖于 CDD 文件。
void diag_SendRequest_PrimitiveData(byte primitiveData[], long dataSize)
{
diagRequest * request;
long ret;
// 1. 设置诊断请求的字节数
diagResize(request, dataSize);
// 2. 设置诊断请求的全部字节
diagSetPrimitiveData(request, primitiveData, dataSize);
// 3. 发送诊断请求
ret = diagSendRequest(request);
testStep("", "pysical address, ret = %d", ret);
if(ret != 0)
{
testStepFail("Request was sent failed");
return;
}
// 4. 确认诊断请求发送到总线上
ret = testWaitForDiagRequestSent(request, 1000);
if(ret != 1)
{
testStepFail("", "Request could not be sent!");
return;
}
testStepPass("", "Request was sent successfully");
testReportWriteDiagObject(request); // 打印诊断请求
// 5. 确认接收到诊断响应
ret = testWaitForDiagResponse(request, 5000);
if(ret != 1)
{
testStepFail("No response received or internal error.");
return;
}
testStepPass("Received ECU response successfully.");
testReportWriteDiagResponse(request); // 打印诊断响应
// 6. 判断响应类型
ret = diagGetLastResponseCode(request);
if(ret >= 0)
{
testStepFail("", "Negative response, NRC code is 0x%X", ret);
return;
}
testStepPass("Received ECU positive response successfully");
}
测试代码:
testcase TC_006_sendRequestFunction_Test()
{
byte primitiveData[4095];
primitiveData[0] = 0x10;
primitiveData[1] = 0x03;
diagSetTarget(ecuName);
diag_SendRequest_PrimitiveData(primitiveData, 2);
}

2. 通用诊断函数(二)
上述的函数都期望诊断响应为正响应,并且都是以物理寻址的方式发送诊断请求。在诊断测试中,往往也需要注入错误的诊断数据,期望 ECU 不响应或者否定响应,并且还可以选择物理寻址或者功能寻址,基于这个需求,封装通用的诊断函数;
long diag_SendRequest_Generic(byte primitiveData[], long dataSize, long expResCode, byte sendType)
{
diagRequest * request;
long ret;
// 1. 设置诊断请求字节数
diagResize(request, dataSize);
// 2. 设置诊断请求数据
diagSetPrimitiveData(request, primitiveData, dataSize);
// 3. 发送诊断请求
if(sendType == 0) // 物理寻址
{
ret = diagSendRequest(request);
testStep("", "Physical Address, ret = %d", ret);
}
else // 功能寻址
{
ret = diagSendFunctional(request);
testStep("", "Functional Address, ret = %d", ret);
}
if(ret != 0)
{
testStepFail("Request could not be sent!");
return 0;
}
// 4. 确认诊断请求是否发送成功
ret = testWaitForDiagRequestSent(request, 1000);
if(ret != 1)
{
testStepFail("Request could not be sent!");
return 0;
}
testStepPass("Request was sent successfully!");
testReportWriteDiagObject(request);
// 5. 确认诊断响应
ret = testWaitForDiagResponse(request, 5000);
switch(ret)
{
case 0: // 超时
if(expResCode == 0)
{
testStepPass("No response was received, as expected.");
}
else
{
testStepFail("No response was received, but expected receive response.");
}
break;
case 1: // 收到响应
testReportWriteDiagResponse(request);
ret = diagGetLastResponseCode(request);
if(ret == -1) // 肯定响应
{
if(expResCode == -1)
{
testStepPass("Positive response was received, as expected.");
}
else
{
testStepFail("", "Positive response was received, but NRC 0x%X was expected", expResCode);
}
return 1;
}
else
{
if(ret == expResCode)
{
testStepPass("", "NRC 0x%X was received, as expected.", ret);
}
else
{
testStepFail("", "NRC 0x%X was received when 0x%X was expected.", ret, expResCode);
}
}
break;
default: // 内部错误
testStepFail("", "There a Error Code received = %d", ret);
}
return 0;
}
测试代码:
variables
{
char ecuName[20] = "Door";
const long No_Response = 0;
const long Positive_Response = -1;
const long NRC_Response_12 = 0x12;
const long NRC_Response_13 = 0x13;
const byte gPhys = 0;
const byte gFunc = 1;
}
testcase TC_007_sendRequestFunction_Test()
{
byte data[4095];
diagSetTarget(ecuName);
data[0] = 0x10;
data[1] = 0x04;
diag_SendRequest_Generic(data, 2, NRC_Response_12, gPhys);
}

七、获取诊断响应数据
1. 获取诊断响应参数值
double diagGetRespParameter(diagRequest request, char parameterName[]):返回诊断响应的参数值;
long diagGetRespParameter(diagRequest request, char parameterName[], char buffer[], dword bufferLen):以字符串的方式返回诊断响应的参数值;
long diagGetRespParameterRaw(diagRequest request, char parameterName[], byte buffer[], dword bufferLen):以 byte 数组的方式返回诊断响应的参数值;
上述函数都需要知道参数名,才能获取对应的参数值,实际应用中用的不多;
2. 获取诊响应的所有数据
long diagGetRespPrimitiveSize(diagRequest request):返回诊断响应的数据大小;
long diagGetRespPrimitiveByte(diagRequest request, dword bytePos):根据字节索引读取诊断数据,每次获取一个字节,通常放在循环里面使用;
上述函数都不需要知道参数名,就可以直接获取所有的响应数据,通常搭配循环使用;
variables
{
diagRequest * request;
char ecuName[20] = "Door";
}
testcase TC_008_diagResponseData_Test()
{
long ret;
byte primitiveData[4095] = {0x10, 0x01};
long size;
long i;
byte resp[4095];
diagSetTarget(ecuName);
diagResize(request, 2);
diagSetPrimitiveData(request, primitiveData, 2);
diag_SendRequest(request);
size = DiagGetRespPrimitiveSize(request);
for(i = 0; i < size; i++)
{
resp[i] = DiagGetRespPrimitiveByte(request, i);
testStep("", "resp[%d] = 0x%X", i, resp[i]);
}
}

八、通用诊断函数(三)
前面的通用函数实现了诊断请求的发送和诊断响应结果的判断,下面实现一个包含诊断请求发送和获取诊断响应数据的函数;
long diag_SendRequestAndGetResponse(byte requestData[], long requestSize, byte sendType, byte responseData[])
{
diagRequest * request;
long ret;
long size;
long i;
// 1. 调整诊断请求的大小和数据
diagResize(request, requestSize);
diagSetPrimitiveData(request, requestData, requestSize);
// 2. 发送诊断请求
if(sendType == 0)
{
ret = diagSendRequest(request);
testStep("", "Physical Address, ret = %d", ret);
}
else
{
ret = diagSendFunctional(request);
testStep("", "Functional Address, ret = %d", ret);
}
if(ret != 0)
{
testStepFail("Request could not be sent!");
return 0;
}
// 3. 确认诊断请求发送成功
ret = testWaitForDiagRequestSent(request, 2000);
if(ret != 1)
{
testStepFail("Request could not be sent!");
return 0;
}
testStepPass("Request was sent successfully!");
testReportWriteDiagObject(request);
// 4. 确认接收到诊断响应
ret = testWaitForDiagResponse(request, 5000);
if(ret != 1)
{
testStepFail("Not Received ECU Response!");
return 0;
}
testStepPass("Receive ECU Response successfully!");
// 5. 解析诊断响应
size = DiagGetRespPrimitiveSize(request);
testStep("", "Response size = %d", size);
for(i = 0; i < size; i++)
{
responseData[i] = DiagGetRespPrimitiveByte(request, i);
}
return size;
}
测试代码:
testcase TC_009_diagRequestAndResponseFunction_Test()
{
byte requestData[4095] = {0x10, 0x03};
byte responseData[4095];
long size;
long i;
diagSetTarget(ecuName);
size = diag_SendRequestAndGetResponse(requestData, 2, 0, responseData);
for(i = 0; i <size; i++)
{
testStep("", "responseData[%d] = 0x%X", i, responseData[i]);
}
}

九、安全解锁
有些诊断服务,需要安全解锁后,才能获取响应的读写权限,CAPL 也内置了一些相关的函数来实现解锁功能。
1. testWaitForUnlockEcu 函数
long testWaitForUnlockEcu(long securityLevel);
long testWaitForUnlockEcu(char ecuQualifier, dword securityLevel);
上述函数,传入安全等级进行解锁,返回值为 0 表示安全解锁成功;使用上述函数,需要在诊断配置窗口加载 Seed & Key DLL.dll 文件。
上述函数的优点是能够直接按照 DLL 的算法解锁,优点是快,但无法观察到解锁的细节;
testcase TC_010_unlockEcu_Test()
{
byte reqData[4095] = {0x10, 0x03};
long ret;
diagSetTarget(ecuName);
diagResize(request, 2);
diagSetPrimitiveData(request, reqData, 2);
diag_SendRequest(request);
ret = testWaitForUnlockEcu(ecuName, 0x01);
testStep("", "ret = %d(0 表示成功, 否则失败)", ret);
}

2. diagGenerateKeyFromSeed 函数
在某些场景下,需要注入错误的密钥验证 ECU 的防攻击性和安全性是否满足规范要求。此时 testWaitForUnlockEcu 函数就不适用了。
long diagFenerateKeyFromSeed(
byte seedArray[],
dword seedArraySize,
dword securityLevel,
char variant[],
char ipOption[],
byte keyArray[],
dword maxKeyArraySize,
dword& keyActualSizeOut
);
- seedArray:输入的 seed 数组;
- seedArraySize:输入的 seed 数组的长度;
- securityLevel:安全解锁等级;
- variant:诊断描述文件中的 variant 参数值,一般传入空字符串;
- ipOption:可选参数,一般传入空字符串;
- keyArray:该函数计算出来的 key 数组;
- maxKeyArraySize:输入的 keyArray 数组允许的最大字节数;
- keyActualSizeOut:输出的 keyArray 数组的实际字节数;
封装通用的安全解锁函数:
void securityAccess(long securityLevel)
{
byte reqData[4095];
diagRequest * request;
long ret;
long size;
byte seedArray[255];
byte keyArray[255];
long i;
dword actualKeySize;
dword seedSize;
reqData[0] = 0x27;
reqData[1] = securityLevel;
// 1. 设置诊断请求
diagResize(request, 2);
diagSetPrimitiveData(request, reqData, 2);
// 2. 发送诊断请求并判断肯定响应
ret = diag_SendRequest(request);
if(ret != 1)
{
testStepFail("can not generate key.");
return;
}
// 3. 接收诊断响应, 获取 seedArray
size = DiagGetRespPrimitiveSize(request);
seedSize = size - 2;
for(i = 2; i < size; i++)
{
seedArray[i - 2] = DiagGetRespPrimitiveByte(request, i);
}
// 5. 计算密钥
ret = diagGenerateKeyFromSeed(seedArray, seedSize, securityLevel, "", "", keyArray, elCount(keyArray), actualKeySize);
if(ret != 0)
{
testStepFail("can't generate key!");
return;
}
// 6. 发送密钥
reqData[0] = 0x27;
reqData[1] = securityLevel + 1;
for(i = 0; i < actualKeySize; i++)
{
reqData[i + 2] = keyArray[i];
}
diagResize(request, actualKeySize + 2);
diagSetPrimitiveData(request, reqData, actualKeySize + 2);
diag_SendRequest(request);
}
测试代码:
testcase TC_011_securityAccess_Test()
{
byte reqData[4095] = {0x10, 0x03};
diagSetTarget(ecuName);
diagResize(request, 2);
diagSetPrimitiveData(request, reqData, 2);
diag_SendRequest(request);
securityAccess(0x01);
}

通过上面的方式,可以将诊断过程中的请求数据和响应数据,都打印到测试报告中。在故障注入的测试中,能够将故障注入可视化,看到种子和密钥的信息,实现了整个解锁过程;
总结
本文系统介绍了 CANoe 中 CAPL 诊断测试的完整流程,从 UDS 诊断协议、诊断描述文件到传输层与诊断层参数,再到诊断请求的发送、响应判断和通用函数封装,最后讲解了安全解锁的两种实现方式。
通过封装通用诊断函数,可以显著提高测试代码的复用度和开发效率。读者掌握这些方法后,即可根据实际测试需求灵活编写诊断测试脚本,提升诊断测试的效率和可维护性。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_41965831/article/details/167784682




