离线测试不依赖真实网络,集中验证 Parser、Builder、chunked、边界值和错误路径,为真机测试提供稳定底座。
适合谁收藏
- 准备复用本组源码并审查对象边界的工程师。
- 需要把公开代码装配进目标 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_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 核心离线用例
/// 功能 : 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
职责:离线测试全局状态
/// 功能 : HTTP 离线测试汇总变量。
/// 库依赖 : 暂无
{attribute 'qualified_only'}
VAR_GLOBAL
stSummary : ST_OfflineTestSummary; // 结构化协议或测试数据。
END_VAR完整源码 4:PRG_OfflineTest.st
职责:离线测试执行入口
/// 功能 : 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
职责:离线测试结果汇总
/// 功能 : 离线 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本篇阅读抓手
- 断言失败保留首个错误位置。
- 合法、非法和容量边界都要覆盖。
- 测试汇总对外暴露通过与失败计数。
如何验证这组源码
- 先针对
FB_OfflineAssert.st的公开输入和错误出口建立确定性用例。 - 再把“断言失败保留首个错误位置”转成至少一个正常场景和一个失败场景。
- 本篇 5 个文件必须一起编译,避免只验证单个函数而漏掉数据结构或调用边界。
- 真机复核时重点观察“测试汇总对外暴露通过与失败计数”,并保留对应状态、计数和原始报文。
这一篇你最该记住
- 断言失败保留首个错误位置。
- 合法、非法和容量边界都要覆盖。
- 测试汇总对外暴露通过与失败计数。
系列导航
- 系列:CodeSys HTTP 系列教程,第 25/28 篇。
- 当前源码加更:第 7/8 篇。
- 本篇完整源码文件数:5。
- 上一篇:第24篇
- 下一篇:第26篇
评论区预留
这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。