ARTICLE · 1021654
第25篇_源码加更 07|127 项离线测试框架完整源码
适合谁收藏
准备复用本组源码并审查对象边界的工程师。 需要把公开代码装配进目标 CodeSys 工程的人。 准备重新完成编译、离线测试和真机验证的读者。
开源范围 本篇完整公开 5 个 ST 文件。代码直接读取已验证工程,保留声明、实现、注释和缩进;没有伪代码,没有跨文件拼接,也没有省略号。
先给结论
离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

读图重点
先找到 FB_OfflineAssert 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。
先看文件职责
FB_OfflineAssert.st | |||
FB_OfflineHttpCoreTest.st | |||
GVL_OfflineTest.st | |||
PRG_OfflineTest.st | |||
ST_OfflineTestSummary.st |
阅读顺序不是按文件名机械展开。先看数据和状态,再看公开方法,最后顺着错误出口和调用对象检查边界。源码篇的目标是让读者能对照工程复现,不是用大段代码制造篇幅。
从协议约束到代码职责
协议约束
离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。 协议层只定义消息与状态成立的条件,工程层还必须把条件分配给确定对象,避免 Parser、Builder、Server、Client 和测试入口互相越权。
对象分工
- 第 1 个对象
FB_OfflineAssert.st:承担“离线断言工具”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 2 个对象
FB_OfflineHttpCoreTest.st:承担“HTTP 核心离线用例”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 3 个对象
GVL_OfflineTest.st:承担“离线测试全局状态”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 4 个对象
PRG_OfflineTest.st:承担“离线测试执行入口”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。 - 第 5 个对象
ST_OfflineTestSummary.st:承担“离线测试结果汇总”。阅读时先确认输入与公开输出,再追状态、长度和错误出口,最后核对它被谁周期调用或被哪个对象消费。
装配与验证
这组文件必须作为一个职责单元阅读和编译。先用确定输入验证“断言失败保留首个错误位置”,再制造非法或容量边界验证“合法、非法和容量边界都要覆盖”,最后在真实通信或上层调用中确认“测试汇总对外暴露通过与失败计数”。如果单文件测试通过但装配后失败,应优先检查结构体、常量、状态枚举和周期调用关系,而不是立即重写核心算法。源码完整公开只证明读者拿到了同一事实源,目标运行时是否通过仍需重新编译和真机取证。
本篇从 FB_OfflineAssert.st 开始审查:先确认“断言失败保留首个错误位置”对应的类型、常量或公开输入,再沿 CASE 或方法调用追踪成功路径,随后逐个检查容量、超时和协议错误出口,最后在 ST_OfflineTestSummary.st 对应的结果或装配位置确认错误能被观测、清理并再次执行。这个顺序专门用来区分“算法正确但没有周期调用”“状态能完成但错误被覆盖”和“测试汇总对外暴露通过与失败计数尚未形成验证证据”三类问题。只有本篇源码、装配和验证使用同一组对象与同一组边界,完整开源才具有可复现意义。
本篇完整开源代码
下面按职责顺序给出本篇全部 ST 文件。每个代码块保留完整声明与实现;阅读时把状态、长度、错误出口和调用对象与上面的职责表逐项对照。
完整源码 1:FB_OfflineAssert.st
职责:离线断言工具
/// 功能 : 离线测试断言工具,统一写入 ST_OfflineTestSummary。/// 库依赖 : 暂无{attribute 'hide_all_locals'}FUNCTION_BLOCK FB_OfflineAssertVAR_OUTPUTbError : BOOL; // 诊断错误标志。diErrorID : DINT; // 诊断错误码,0表示无错误。END_VAR// === IMPLEMENTATION ===/// =======================================================================/// 名称 : FB_OfflineAssert/// 功能 : 为 HTTP 离线 POU 测试提供布尔、字符串和整数断言。/// 库依赖 : 暂无/// =======================================================================/// 使用说明 : 1. 本 FB 仅作为测试支撑对象,所有 Method 聚合在本 mirror 文件内。/// : 2. 每次测试先调用 M_Reset,所有断言结束后调用 M_Finish 生成汇总状态。/// =======================================================================// === METHOD M_Reset ===/// =======================================================================/// 名称 : M_Reset/// 功能 : 清空测试汇总。/// 说明 : reset 不依赖外部状态,确保每轮离线测试结果可重复。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_ResetVAR_IN_OUTstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 测试汇总必须整组复位,否则上一次失败会污染本轮无人值守验证结果。stSummary.uiTotalCases := 0;stSummary.uiPassedCases := 0;stSummary.uiFailedCases := 0;stSummary.uiBlockedCases := 0;stSummary.bDone := FALSE;stSummary.bPassed := FALSE;stSummary.bFailed := FALSE;stSummary.bBlocked := FALSE;stSummary.sCurrentCase := '';stSummary.sFailedCase := '';stSummary.sBlockedCase := '';// === METHOD M_ExpectTrue ===/// =======================================================================/// 名称 : M_ExpectTrue/// 功能 : 断言输入条件为 TRUE。/// 说明 : 失败时只锁存首个失败用例,便于在线变量直接定位。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_ExpectTrueVAR_INPUTbCondition : BOOL := FALSE; // 布尔状态或命令标志。sCase : STRING(120) := ''; // 诊断或协议文本字段。END_VARVAR_IN_OUTstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 断言统一累加计数,禁止测试 FB 自行改写汇总语义。stSummary.uiTotalCases := stSummary.uiTotalCases + 1;stSummary.sCurrentCase := sCase;IF bCondition THENstSummary.uiPassedCases := stSummary.uiPassedCases + 1;ELSEstSummary.uiFailedCases := stSummary.uiFailedCases + 1;IF LEN(stSummary.sFailedCase) = 0 THENstSummary.sFailedCase := sCase;END_IFEND_IF// === METHOD M_ExpectString ===/// =======================================================================/// 名称 : M_ExpectString/// 功能 : 断言两个字符串完全一致。/// 说明 : 字符串断言用于 HTTP 方法、Header 和 body 精确匹配。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_ExpectStringVAR_INPUTsActual : STRING(512) := ''; // 诊断或协议文本字段。sExpected : STRING(512) := ''; // 诊断或协议文本字段。sCase : STRING(120) := ''; // 诊断或协议文本字段。END_VARVAR_IN_OUTstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。M_ExpectTrue(bCondition := sActual = sExpected,sCase := sCase,stSummary := stSummary);// === METHOD M_ExpectUdint ===/// =======================================================================/// 名称 : M_ExpectUdint/// 功能 : 断言两个 UDINT 数值一致。/// 说明 : 用于 HTTP 状态码、Content-Length 和 chunk 长度断言。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_ExpectUdintVAR_INPUTudiActual : UDINT := 0; // 计数、长度或状态数值。udiExpected : UDINT := 0; // 计数、长度或状态数值。sCase : STRING(120) := ''; // 诊断或协议文本字段。END_VARVAR_IN_OUTstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。M_ExpectTrue(bCondition := udiActual = udiExpected,sCase := sCase,stSummary := stSummary);// === METHOD M_Finish ===/// =======================================================================/// 名称 : M_Finish/// 功能 : 生成最终测试状态。/// 说明 : SmartControl CLI 读取 bDone/bPassed/bFailed/bBlocked 作为测试结论。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_FinishVAR_IN_OUTstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 汇总状态只在所有测试运行结束后生成,避免中途读取造成误判。stSummary.bDone := TRUE;stSummary.bFailed := stSummary.uiFailedCases > 0;stSummary.bBlocked := stSummary.uiBlockedCases > 0;stSummary.bPassed := (stSummary.uiTotalCases > 0)AND (stSummary.uiFailedCases = 0)AND (stSummary.uiBlockedCases = 0);
完整源码 2:FB_OfflineHttpCoreTest.st
职责:HTTP 核心离线用例
/// 功能 : HTTP 协议核心离线测试。/// 库依赖 : 暂无{attribute 'hide_all_locals'}FUNCTION_BLOCK FB_OfflineHttpCoreTestVAR_OUTPUTbError : BOOL; // 诊断错误标志。diErrorID : DINT; // 诊断错误码,0表示无错误。END_VAR// === IMPLEMENTATION ===/// =======================================================================/// 名称 : FB_OfflineHttpCoreTest/// 功能 : 验证 HTTP Parser、Builder 和 ChunkedDecoder 的首版核心语义。/// 库依赖 : 暂无/// =======================================================================/// 使用说明 : 1. 本 FB 是离线测试对象,核心断言全部聚合在 M_Run 内。/// : 2. 测试覆盖 Content-Length、chunked、Host、TE+CL 拒绝、keep-alive/close 和自定义 Header。/// =======================================================================// === METHOD M_Run ===/// =======================================================================/// 名称 : M_Run/// 功能 : 执行 HTTP 核心离线测试矩阵。/// 说明 : 该方法故意引用待实现核心对象,TDD 红灯阶段应能拦截未实现状态。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_RunVAR_IN_OUTfbAssert : FB_OfflineAssert; // 状态型功能块实例。stSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VARVARfbParser : FB_HttpMessageParser; // 状态型功能块实例。fbBuilder : FB_HttpMessageBuilder; // 状态型功能块实例。fbChunked : FB_HttpChunkedDecoder; // 状态型功能块实例。stRequest : ST_HttpRequest; // 结构化协议或测试数据。stResponse : ST_HttpResponse; // 结构化协议或测试数据。sMessage : STRING(GVL_Http.cnMaxMessageSize); // 诊断或协议文本字段。sDecoded : STRING(GVL_Http.cnMaxBodySize); // 诊断或协议文本字段。udiValue : UDINT; // 计数、长度或状态数值。udiExpected : UDINT; // 计数、长度或状态数值。bLengthOk : BOOL; // 布尔状态或命令标志。uiLoop : UINT; // 计数、长度或状态数值。uiCount : UINT; // 计数、长度或状态数值。sTestLeft : STRING(255); // 诊断或协议文本字段。sTestRight : STRING(255); // 诊断或协议文本字段。sTestValue : STRING(255); // 诊断或协议文本字段。sHeaderVal : STRING(GVL_Http.cnMaxHeaderValueLen); // 诊断或协议文本字段。bInvalid : BOOL; // 布尔状态或命令标志。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 核心测试先验证纯协议对象,避免 NBS 现场因素掩盖解析和构造缺陷。// 原因:离线测试必须覆盖构造、解析、chunked、错误分支和边界长度,不能只证明一个字符串能返回。// 约束:每个断言都写入统一汇总计数,CLI 真机前门禁只读 GVL 汇总而不依赖 IDE 观察窗口。// 风险:协议异常用例必须保留失败 ID,否则后续真实通信失败无法判断是测试脚本还是协议栈问题。// 诊断:sFailedCase 和 sBlockedCase 是发布说明中追踪测试覆盖缺口的权威字段。sMessage := 'GET /api/ping HTTP/1.1$R$NHost: plc.local$R$NConnection: close$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bDone AND NOT fbParser.bError,sCase := 'parser accepts valid GET request',stSummary := stSummary);fbAssert.M_ExpectString(sActual := stRequest.sTarget,sExpected := '/api/ping',sCase := 'parser extracts request target',stSummary := stSummary);sMessage := 'POST /api/echo HTTP/1.1$R$NHost: plc.local$R$NContent-Length: 5$R$N$R$Nhello';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectString(sActual := stRequest.sBody,sExpected := 'hello',sCase := 'parser extracts content-length body',stSummary := stSummary);sMessage := 'POST /api/echo HTTP/1.1$R$NHost: plc.local$R$NTransfer-Encoding: chunked$R$N$R$N5$R$Nhello$R$N0$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectString(sActual := stRequest.sBody,sExpected := 'hello',sCase := 'parser decodes chunked request body',stSummary := stSummary);sMessage := 'GET /case HTTP/1.1$R$NhOsT: plc.local$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectString(sActual := stRequest.sHost,sExpected := 'plc.local',sCase := 'parser accepts case-insensitive host header',stSummary := stSummary);sMessage := 'GET /missing HTTP/1.1$R$NConnection: close$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iMissingHost),sCase := 'parser rejects missing host',stSummary := stSummary);sMessage := 'GET /duplicate HTTP/1.1$R$NHost: plc.local$R$NHost: plc.backup$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iDuplicateHost),sCase := 'parser rejects duplicate host',stSummary := stSummary);sMessage := 'GET /space HTTP/1.1$R$NHost : plc.local$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iInvalidHeader),sCase := 'parser rejects whitespace before colon',stSummary := stSummary);sMessage := 'POST /bad-length HTTP/1.1$R$NHost: plc.local$R$NContent-Length: 5x$R$N$R$Nhello';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iInvalidContentLength),sCase := 'parser rejects invalid content length',stSummary := stSummary);sMessage := 'POST /short HTTP/1.1$R$NHost: plc.local$R$NContent-Length: 5$R$N$R$Nhe';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iNeedMoreData),sCase := 'parser rejects incomplete content-length body',stSummary := stSummary);sMessage := 'POST /duplicate-length HTTP/1.1$R$NHost: plc.local$R$NContent-Length: 5$R$NContent-Length: 5$R$N$R$Nhello';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iInvalidContentLength),sCase := 'parser rejects duplicate content length',stSummary := stSummary);sMessage := 'POST /gzip HTTP/1.1$R$NHost: plc.local$R$NTransfer-Encoding: gzip$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iUnsupportedTransferEncoding),sCase := 'parser rejects unsupported transfer encoding',stSummary := stSummary);sMessage := 'POST /bad HTTP/1.1$R$NHost: plc.local$R$NTransfer-Encoding: chunked$R$NContent-Length: 5$R$N$R$N0$R$N$R$N';fbParser.M_ParseRequest(sMessage := sMessage,stRequest => stRequest);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iTransferEncodingContentLength),sCase := 'parser rejects TE plus CL',stSummary := stSummary);fbChunked.M_Decode(sChunked := '4$R$NWiki$R$N5$R$Npedia$R$N0$R$N$R$N',sBody => sDecoded);fbAssert.M_ExpectString(sActual := sDecoded,sExpected := 'Wikipedia',sCase := 'chunked decoder joins chunks',stSummary := stSummary);fbChunked.M_Decode(sChunked := '5;ext=1$R$Nhello$R$N0$R$N$R$N',sBody => sDecoded);fbAssert.M_ExpectString(sActual := sDecoded,sExpected := 'hello',sCase := 'chunked decoder accepts size extension',stSummary := stSummary);fbChunked.M_Decode(sChunked := '5$R$Nhello$R$N6;ext=1$R$N-world$R$N0$R$NX-Trailer: done$R$N$R$N',sBody => sDecoded);fbAssert.M_ExpectString(sActual := sDecoded,sExpected := 'hello-world',sCase := 'chunked decoder accepts optional trailer fields',stSummary := stSummary);fbChunked.M_Decode(sChunked := 'Z$R$Nhello$R$N0$R$N$R$N',sBody => sDecoded);fbAssert.M_ExpectTrue(bCondition := fbChunked.bError AND (fbChunked.eError = E_HttpError.iChunkedDecodeFailed),sCase := 'chunked decoder rejects invalid hex',stSummary := stSummary);fbChunked.M_Decode(sChunked := '5$R$Nhe',sBody => sDecoded);fbAssert.M_ExpectTrue(bCondition := fbChunked.bError AND (fbChunked.eError = E_HttpError.iNeedMoreData),sCase := 'chunked decoder rejects incomplete chunk data',stSummary := stSummary);bLengthOk := F_HttpParseContentLength(sValue := '1024',udiValue => udiValue);fbAssert.M_ExpectTrue(bCondition := bLengthOk AND (udiValue = 1024),sCase := 'content length parser keeps body limit classification outside syntax parsing',stSummary := stSummary);FOR uiLoop := 1 TO 16 DOCASE uiLoop OF1:sTestLeft := 'Host';sTestRight := 'host';2:sTestLeft := 'Content-Length';sTestRight := 'content-length';3:sTestLeft := 'Transfer-Encoding';sTestRight := 'TRANSFER-ENCODING';4:sTestLeft := 'Connection';sTestRight := 'connection';5:sTestLeft := 'Accept';sTestRight := 'ACCEPT';6:sTestLeft := 'Authorization';sTestRight := 'authorization';7:sTestLeft := 'X-Trace-Id';sTestRight := 'x-trace-id';8:sTestLeft := 'Content-Type';sTestRight := 'content-type';9:sTestLeft := 'User-Agent';sTestRight := 'USER-AGENT';10:sTestLeft := 'Accept-Encoding';sTestRight := 'accept-encoding';11:sTestLeft := 'Cache-Control';sTestRight := 'CACHE-CONTROL';12:sTestLeft := 'If-None-Match';sTestRight := 'if-none-match';13:sTestLeft := 'ETag';sTestRight := 'etag';14:sTestLeft := 'Location';sTestRight := 'LOCATION';15:sTestLeft := 'Retry-After';sTestRight := 'retry-after';ELSEsTestLeft := '';sTestRight := '';END_CASEbLengthOk := F_HttpAsciiEqualsIgnoreCase(sLeft := sTestLeft,sRight := sTestRight);fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'ascii ignore-case positive matrix',stSummary := stSummary);END_FORFOR uiLoop := 1 TO 12 DOCASE uiLoop OF1:sTestLeft := 'Host';sTestRight := 'Host-Name';2:sTestLeft := 'Content-Length';sTestRight := 'Content-Type';3:sTestLeft := 'Transfer-Encoding';sTestRight := 'Transfer';4:sTestLeft := 'Connection';sTestRight := 'Connect';5:sTestLeft := 'Accept';sTestRight := 'Accepts';6:sTestLeft := 'Authorization';sTestRight := 'Authentication';7:sTestLeft := 'X-Trace-Id';sTestRight := 'X-Trace';8:sTestLeft := 'Content-Type';sTestRight := 'Content-Type-Extra';9:sTestLeft := 'User-Agent';sTestRight := 'Agent';10:sTestLeft := 'Accept-Encoding';sTestRight := 'Accept-Language';11:sTestLeft := 'Cache-Control';sTestRight := 'Pragma';ELSEsTestLeft := 'Retry-After';sTestRight := 'Retry-Before';END_CASEbLengthOk := F_HttpAsciiEqualsIgnoreCase(sLeft := sTestLeft,sRight := sTestRight);fbAssert.M_ExpectTrue(bCondition := NOT bLengthOk,sCase := 'ascii ignore-case negative matrix',stSummary := stSummary);END_FORFOR uiLoop := 1 TO 16 DOCASE uiLoop OF1:sTestValue := '0';udiExpected := 0;2:sTestValue := '1';udiExpected := 1;3:sTestValue := '4';udiExpected := 4;4:sTestValue := '5';udiExpected := 5;5:sTestValue := '10';udiExpected := 10;6:sTestValue := '31';udiExpected := 31;7:sTestValue := '64';udiExpected := 64;8:sTestValue := '128';udiExpected := 128;9:sTestValue := '255';udiExpected := 255;10:sTestValue := '256';udiExpected := 256;11:sTestValue := '512';udiExpected := 512;12:sTestValue := '700';udiExpected := 700;13:sTestValue := '900';udiExpected := 900;14:sTestValue := '1023';udiExpected := 1023;15:sTestValue := '1024';udiExpected := 1024;ELSEsTestValue := '4294967295';udiExpected := 4294967295;END_CASEbLengthOk := F_HttpParseContentLength(sValue := sTestValue,udiValue => udiValue);fbAssert.M_ExpectTrue(bCondition := bLengthOk AND (udiValue = udiExpected),sCase := 'content-length valid matrix',stSummary := stSummary);END_FORFOR uiLoop := 1 TO 14 DOCASE uiLoop OF1:sTestValue := '';2:sTestValue := ' ';3:sTestValue := '-1';4:sTestValue := '+1';5:sTestValue := '1 ';6:sTestValue := ' 1';7:sTestValue := 'abc';8:sTestValue := '12x';9:sTestValue := '1.0';10:sTestValue := '0x10';11:sTestValue := '4294967296';12:sTestValue := '9999999999';13:sTestValue := '5;';ELSEsTestValue := '$R$N';END_CASEbLengthOk := F_HttpParseContentLength(sValue := sTestValue,udiValue => udiValue);fbAssert.M_ExpectTrue(bCondition := NOT bLengthOk,sCase := 'content-length invalid matrix',stSummary := stSummary);END_FORFOR uiLoop := 1 TO 16 DOCASE uiLoop OF1:sTestValue := '0';udiExpected := 0;2:sTestValue := '1';udiExpected := 1;3:sTestValue := '4';udiExpected := 4;4:sTestValue := '5';udiExpected := 5;5:sTestValue := 'A';udiExpected := 10;6:sTestValue := 'a';udiExpected := 10;7:sTestValue := 'F';udiExpected := 15;8:sTestValue := 'f';udiExpected := 15;9:sTestValue := '10';udiExpected := 16;10:sTestValue := '1F';udiExpected := 31;11:sTestValue := '40';udiExpected := 64;12:sTestValue := '80';udiExpected := 128;13:sTestValue := 'ff';udiExpected := 255;14:sTestValue := '100';udiExpected := 256;15:sTestValue := '3ff';udiExpected := 1023;ELSEsTestValue := '5;ext=1';udiExpected := 5;END_CASEbLengthOk := F_HttpParseHexSize(sValue := sTestValue,udiValue => udiValue);fbAssert.M_ExpectTrue(bCondition := bLengthOk AND (udiValue = udiExpected),sCase := 'chunk-size valid matrix',stSummary := stSummary);END_FORFOR uiLoop := 1 TO 14 DOCASE uiLoop OF1:sTestValue := '';2:sTestValue := ' ';3:sTestValue := '-1';4:sTestValue := 'G';5:sTestValue := 'x10';6:sTestValue := '10x';7:sTestValue := '400';8:sTestValue := '1000';9:sTestValue := '99999999';10:sTestValue := '5 ';11:sTestValue := ' 5';12:sTestValue := '0x5';13:sTestValue := '$R$N';ELSEsTestValue := '5,1';END_CASEbLengthOk := F_HttpParseHexSize(sValue := sTestValue,udiValue => udiValue);fbAssert.M_ExpectTrue(bCondition := NOT bLengthOk,sCase := 'chunk-size invalid matrix',stSummary := stSummary);END_FORbLengthOk := F_HttpFindHeader(sHeaders := 'Host: plc.local$R$NConnection: close$R$N',sHeaderName := 'Host',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = 'plc.local');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder extracts host',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Content-Type: application/json$R$N',sHeaderName := 'content-type',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = 'application/json');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder trims leading value space',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Host: first$R$NHost: second$R$N',sHeaderName := 'host',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 2) AND (sHeaderVal = 'first');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder counts duplicate host',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Transfer-Encoding: chunked$R$N',sHeaderName := 'TRANSFER-ENCODING',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = 'chunked');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder matches transfer encoding',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'X-Empty:$R$N',sHeaderName := 'x-empty',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = '');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder accepts empty value',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'A: 1$R$NB: 2$R$NC: 3$R$N',sHeaderName := 'B',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = '2');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder scans middle line',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'A: 1$R$NB: 2',sHeaderName := 'B',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := bLengthOk AND NOT bInvalid AND (uiCount = 1) AND (sHeaderVal = '2');fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder accepts final line without crlf',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'A: 1$R$NB: 2$R$N',sHeaderName := 'C',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := (NOT bLengthOk) AND (NOT bInvalid) AND (uiCount = 0);fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder reports missing header cleanly',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Bad Header: value$R$N',sHeaderName := 'Bad',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := (NOT bLengthOk) AND bInvalid;fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder rejects whitespace inside field name',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Host : plc.local$R$N',sHeaderName := 'Host',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := (NOT bLengthOk) AND bInvalid;fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder rejects whitespace before colon',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := 'Host plc.local$R$N',sHeaderName := 'Host',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := (NOT bLengthOk) AND bInvalid;fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder rejects missing colon',stSummary := stSummary);bLengthOk := F_HttpFindHeader(sHeaders := '$R$N',sHeaderName := 'Host',sValue => sHeaderVal,uiCount => uiCount,bInvalidGrammar => bInvalid);bLengthOk := (NOT bLengthOk) AND bInvalid;fbAssert.M_ExpectTrue(bCondition := bLengthOk,sCase := 'header finder rejects empty header line',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 200 OK$R$NContent-Type: text/plain$R$NContent-Length: 4$R$N$R$Npong',stResponse => stResponse);fbAssert.M_ExpectTrue(bCondition := fbParser.bDone AND NOT fbParser.bErrorAND (stResponse.uiStatusCode = 200)AND (stResponse.sBody = 'pong'),sCase := 'parser accepts response content length body',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 200 OK$R$NTransfer-Encoding: chunked$R$N$R$N4$R$Npong$R$N0$R$N$R$N',stResponse => stResponse);fbAssert.M_ExpectString(sActual := stResponse.sBody,sExpected := 'pong',sCase := 'parser decodes chunked response body',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 200 OK$R$NTransfer-Encoding: chunked$R$NContent-Length: 4$R$N$R$N0$R$N$R$N',stResponse => stResponse);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iTransferEncodingContentLength),sCase := 'parser rejects response TE plus CL',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 OK$R$NContent-Length: 0$R$N$R$N',stResponse => stResponse);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iInvalidStartLine),sCase := 'parser rejects invalid response status',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 200 OK$R$NContent-Length : 0$R$N$R$N',stResponse => stResponse);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iInvalidHeader),sCase := 'parser rejects invalid response header grammar',stSummary := stSummary);fbParser.M_ParseResponse(sMessage := 'HTTP/1.1 200 OK$R$NContent-Length: 1200$R$N$R$N',stResponse => stResponse);fbAssert.M_ExpectTrue(bCondition := fbParser.bError AND (fbParser.eError = E_HttpError.iBodyTooLarge),sCase := 'parser rejects response content length above body limit before read close',stSummary := stSummary);stRequest.eMethod := E_HttpMethod.iGet;stRequest.sHost := '';stRequest.sTarget := '/';stRequest.sBody := '';fbBuilder.M_BuildRequest(stRequest := stRequest,sMessage => sMessage);fbAssert.M_ExpectTrue(bCondition := fbBuilder.bError AND (fbBuilder.eError = E_HttpError.iMissingHost),sCase := 'builder rejects request without host',stSummary := stSummary);stResponse.uiStatusCode := 200;stResponse.sReason := 'OK';stResponse.sContentType := 'text/plain';stResponse.sBody := 'pong';stResponse.bConnectionClose := FALSE;stResponse.sAdditionalHeader := '';fbBuilder.M_BuildResponse(stResponse := stResponse,sMessage => sMessage);fbAssert.M_ExpectTrue(bCondition := (FIND(sMessage, 'HTTP/1.1 200 OK') = 1)AND (FIND(sMessage, 'Connection: keep-alive') > 0)AND (FIND(sMessage, 'Content-Length: 4') > 0),sCase := 'builder emits keep-alive and content length by default',stSummary := stSummary);stResponse.bConnectionClose := TRUE;stResponse.sAdditionalHeader := 'X-Test: offline';fbBuilder.M_BuildResponse(stResponse := stResponse,sMessage => sMessage);fbAssert.M_ExpectTrue(bCondition := (FIND(sMessage, 'Connection: close') > 0)AND (FIND(sMessage, 'X-Test: offline') > 0),sCase := 'builder emits explicit close and additional header',stSummary := stSummary);
完整源码 3:GVL_OfflineTest.st
职责:离线测试全局状态
/// 功能 : HTTP 离线测试汇总变量。/// 库依赖 : 暂无{attribute 'qualified_only'}VAR_GLOBALstSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。END_VAR
完整源码 4:PRG_OfflineTest.st
职责:离线测试执行入口
/// 功能 : HTTP 离线 POU 测试入口。/// 库依赖 : 暂无PROGRAM PRG_OfflineTestVARfbAssert : FB_OfflineAssert; // 状态型功能块实例。fbCoreTest : FB_OfflineHttpCoreTest; // 状态型功能块实例。bRan : BOOL := FALSE; // 布尔状态或命令标志。END_VAR// === IMPLEMENTATION ===/// =======================================================================/// 名称 : PRG_OfflineTest/// 功能 : 运行 HTTP 协议核心离线测试,并生成全局汇总变量。/// 库依赖 : 暂无/// =======================================================================/// 使用说明 : 1. PROGRAM 不使用 hide_all_locals,由 PLC_PRG 或 CLI 离线流程触发。/// : 2. 真机通信用例不在本入口伪造,通过外部互通和真实 PLC 证据验证。/// =======================================================================// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 诊断:离线入口每周期刷新汇总变量,CLI 只读取 GVL_OfflineTest 即可判断单元逻辑是否通过。IF NOT bRan THEN// 离线测试只运行一次,结果锁存在 GVL_OfflineTest.stSummary 供 CLI 读取。fbAssert.M_Reset(stSummary := GVL_OfflineTest.stSummary);fbCoreTest.M_Run(fbAssert := fbAssert,stSummary := GVL_OfflineTest.stSummary);fbAssert.M_Finish(stSummary := GVL_OfflineTest.stSummary);bRan := TRUE;END_IF
完整源码 5:ST_OfflineTestSummary.st
职责:离线测试结果汇总
/// 功能 : 离线 POU 测试汇总结构。/// 库依赖 : 暂无TYPE ST_OfflineTestSummary :STRUCTuiTotalCases : UINT := 0; // 测试用例总数。uiPassedCases : UINT := 0; // 已通过用例数。uiFailedCases : UINT := 0; // 失败用例数。uiBlockedCases : UINT := 0; // 阻塞用例数。bDone : BOOL := FALSE; // 汇总已完成。bPassed : BOOL := FALSE; // 所有用例通过且无阻塞。bFailed : BOOL := FALSE; // 至少一个用例失败。bBlocked : BOOL := FALSE; // 至少一个用例阻塞。sCurrentCase : STRING(120) := '';sFailedCase : STRING(120) := '';sBlockedCase : STRING(120) := '';END_STRUCTEND_TYPE
本篇阅读抓手
断言失败保留首个错误位置。 合法、非法和容量边界都要覆盖。 测试汇总对外暴露通过与失败计数。
如何验证这组源码
先针对 FB_OfflineAssert.st的公开输入和错误出口建立确定性用例。再把“断言失败保留首个错误位置”转成至少一个正常场景和一个失败场景。 本篇 5 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。 真机复核时重点观察“测试汇总对外暴露通过与失败计数”,并保留对应状态、计数和原始报文。
这一篇你最该记住
断言失败保留首个错误位置。 合法、非法和容量边界都要覆盖。 测试汇总对外暴露通过与失败计数。
系列导航
系列:CodeSys HTTP 系列教程,第 25/28 篇。 当前源码加更:第 7/8 篇。 本篇完整源码文件数:5。 上一篇:第24篇 下一篇:第26篇