夜雨聆风学习资料网

ARTICLE · 1021654

第25篇_源码加更 07|127 项离线测试框架完整源码

第25篇_源码加更 07|127 项离线测试框架完整源码

适合谁收藏

  • 准备复用本组源码并审查对象边界的工程师。
  • 需要把公开代码装配进目标 CodeSys 工程的人。
  • 准备重新完成编译、离线测试和真机验证的读者。

开源范围 本篇完整公开 5 个 ST 文件。代码直接读取已验证工程,保留声明、实现、注释和缩进;没有伪代码,没有跨文件拼接,也没有省略号。

先给结论

离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。 本篇的通过标准不是“代码已经贴出”,而是每个文件的职责、调用位置、状态边界和验证入口都能对应起来,并且完整代码可逐字回查源文件。

读图重点

先找到 FB_OfflineAssert 在本组中的位置,再沿图确认其余对象分别承担数据、状态、执行或诊断职责。图只给阅读顺序,最终判断必须回到下面的完整 ST 代码。

先看文件职责

序号
文件
职责
行数
1
FB_OfflineAssert.st
离线断言工具
141
2
FB_OfflineHttpCoreTest.st
HTTP 核心离线用例
856
3
GVL_OfflineTest.st
离线测试全局状态
6
4
PRG_OfflineTest.st
离线测试执行入口
34
5
ST_OfflineTestSummary.st
离线测试结果汇总
17

阅读顺序不是按文件名机械展开。先看数据和状态,再看公开方法,最后顺着错误出口和调用对象检查边界。源码篇的目标是让读者能对照工程复现,不是用大段代码制造篇幅。

从协议约束到代码职责

协议约束

离线测试不依赖真实网络,集中验证 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_OUTPUT    bError    : 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_OUT    stSummary : 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_INPUT    bCondition : BOOL := FALSE;        // 布尔状态或命令标志。    sCase      : STRING(120) := '';    // 诊断或协议文本字段。END_VARVAR_IN_OUT    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。END_VAR// === IMPLEMENTATION ===// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。// 断言统一累加计数,禁止测试 FB 自行改写汇总语义。stSummary.uiTotalCases := stSummary.uiTotalCases + 1;stSummary.sCurrentCase := sCase;IF bCondition THEN    stSummary.uiPassedCases := stSummary.uiPassedCases + 1;ELSE    stSummary.uiFailedCases := stSummary.uiFailedCases + 1;    IF LEN(stSummary.sFailedCase) = 0 THEN        stSummary.sFailedCase := sCase;    END_IFEND_IF// === METHOD M_ExpectString ===/// =======================================================================/// 名称      : M_ExpectString/// 功能      : 断言两个字符串完全一致。/// 说明      : 字符串断言用于 HTTP 方法、Header 和 body 精确匹配。/// ======================================================================={attribute 'hide_all_locals'}METHOD PUBLIC M_ExpectStringVAR_INPUT    sActual   : STRING(512) := '';    // 诊断或协议文本字段。    sExpected : STRING(512) := '';    // 诊断或协议文本字段。    sCase     : STRING(120) := '';    // 诊断或协议文本字段。END_VARVAR_IN_OUT    stSummary : 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_INPUT    udiActual   : UDINT := 0;           // 计数、长度或状态数值。    udiExpected : UDINT := 0;           // 计数、长度或状态数值。    sCase       : STRING(120) := '';    // 诊断或协议文本字段。END_VARVAR_IN_OUT    stSummary : 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_OUT    stSummary : 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_OUTPUT    bError    : 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_OUT    fbAssert  : FB_OfflineAssert;         // 状态型功能块实例。    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。END_VARVAR    fbParser    : 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 DO    CASE uiLoop OF        1:            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';        ELSE            sTestLeft := '';            sTestRight := '';    END_CASE    bLengthOk := 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 DO    CASE uiLoop OF        1:            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';        ELSE            sTestLeft := 'Retry-After';            sTestRight := 'Retry-Before';    END_CASE    bLengthOk := 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 DO    CASE uiLoop OF        1:            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;        ELSE            sTestValue := '4294967295';            udiExpected := 4294967295;    END_CASE    bLengthOk := 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 DO    CASE uiLoop OF        1:            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;';        ELSE            sTestValue := '$R$N';    END_CASE    bLengthOk := 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 DO    CASE uiLoop OF        1:            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;        ELSE            sTestValue := '5;ext=1';            udiExpected := 5;    END_CASE    bLengthOk := 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 DO    CASE uiLoop OF        1:            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';        ELSE            sTestValue := '5,1';    END_CASE    bLengthOk := 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.bError        AND (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_GLOBAL    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。END_VAR

完整源码 4:PRG_OfflineTest.st

职责:离线测试执行入口

/// 功能    : HTTP 离线 POU 测试入口。/// 库依赖 : 暂无PROGRAM PRG_OfflineTestVAR    fbAssert   : 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 :STRUCT    uiTotalCases   : 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

本篇阅读抓手

  • 断言失败保留首个错误位置。
  • 合法、非法和容量边界都要覆盖。
  • 测试汇总对外暴露通过与失败计数。

如何验证这组源码

  1. 先针对 FB_OfflineAssert.st 的公开输入和错误出口建立确定性用例。
  2. 再把“断言失败保留首个错误位置”转成至少一个正常场景和一个失败场景。
  3. 本篇 5 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
  4. 真机复核时重点观察“测试汇总对外暴露通过与失败计数”,并保留对应状态、计数和原始报文。

这一篇你最该记住

  • 断言失败保留首个错误位置。
  • 合法、非法和容量边界都要覆盖。
  • 测试汇总对外暴露通过与失败计数。

系列导航

  • 系列:CodeSys HTTP 系列教程,第 25/28 篇。
  • 当前源码加更:第 7/8 篇。
  • 本篇完整源码文件数:5。
  • 上一篇:第24篇
  • 下一篇:第26篇

相关学习资料

返回首页浏览学习资料