ControlRookie
返回文章

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

离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。

离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。

适合谁收藏

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

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

先给结论

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

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

读图重点

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

先看文件职责

序号文件职责行数
1FB_OfflineAssert.st离线断言工具141
2FB_OfflineHttpCoreTest.stHTTP 核心离线用例856
3GVL_OfflineTest.st离线测试全局状态6
4PRG_OfflineTest.st离线测试执行入口34
5ST_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

职责:离线断言工具

iecst
/// 功能    : 离线测试断言工具,统一写入 ST_OfflineTestSummary。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_OfflineAssert
VAR_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_Reset
VAR_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_ExpectTrue
VAR_INPUT
    bCondition : BOOL := FALSE;        // 布尔状态或命令标志。
    sCase      : STRING(120) := '';    // 诊断或协议文本字段。
END_VAR
VAR_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_IF
END_IF

// === METHOD M_ExpectString ===
/// =======================================================================
/// 名称      : M_ExpectString
/// 功能      : 断言两个字符串完全一致。
/// 说明      : 字符串断言用于 HTTP 方法、Header 和 body 精确匹配。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_ExpectString
VAR_INPUT
    sActual   : STRING(512) := '';    // 诊断或协议文本字段。
    sExpected : STRING(512) := '';    // 诊断或协议文本字段。
    sCase     : STRING(120) := '';    // 诊断或协议文本字段。
END_VAR
VAR_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_ExpectUdint
VAR_INPUT
    udiActual   : UDINT := 0;           // 计数、长度或状态数值。
    udiExpected : UDINT := 0;           // 计数、长度或状态数值。
    sCase       : STRING(120) := '';    // 诊断或协议文本字段。
END_VAR
VAR_IN_OUT
    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。
END_VAR
// === IMPLEMENTATION ===
// 工程说明:本段集中处理状态、边界或诊断,避免跨周期残留。
M_ExpectTrue(
    bCondition := udiActual = udiExpected,
    sCase      := sCase,
    stSummary  := stSummary
    );

// === METHOD M_Finish ===
/// =======================================================================
/// 名称      : M_Finish
/// 功能      : 生成最终测试状态。
/// 说明      : 当前测试工具读取 bDone/bPassed/bFailed/bBlocked 作为测试结论。
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD PUBLIC M_Finish
VAR_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 核心离线用例

iecst
/// 功能    : HTTP 协议核心离线测试。
/// 库依赖 : 暂无
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_OfflineHttpCoreTest
VAR_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_Run
VAR_IN_OUT
    fbAssert  : FB_OfflineAssert;         // 状态型功能块实例。
    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。
END_VAR
VAR
    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_FOR

FOR 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_FOR

FOR 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_FOR

FOR 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_FOR

FOR 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_FOR

FOR 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_FOR

bLengthOk := 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

职责:离线测试全局状态

iecst
/// 功能    : HTTP 离线测试汇总变量。
/// 库依赖 : 暂无
{attribute 'qualified_only'}
VAR_GLOBAL
    stSummary : ST_OfflineTestSummary;    // 结构化协议或测试数据。
END_VAR

完整源码 4:PRG_OfflineTest.st

职责:离线测试执行入口

iecst
/// 功能    : HTTP 离线 POU 测试入口。
/// 库依赖 : 暂无
PROGRAM PRG_OfflineTest
VAR
    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

职责:离线测试结果汇总

iecst
/// 功能    : 离线 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_STRUCT
END_TYPE

本篇阅读抓手

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

如何验证这组源码

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

这一篇你最该记住

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

系列导航

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

评论区预留

这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。

↑ ↓