From 9dea46ef1e24ffe4390f95f3b318d7be8f140877 Mon Sep 17 00:00:00 2001 From: Marci Date: Fri, 28 Aug 2026 18:29:22 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=A2=9E=E5=8A=A0=E5=AE=98=E6=96=B9=20?= =?UTF-8?q?secs4net=20=E4=BA=92=E6=93=8D=E4=BD=9C=E5=A4=B9=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 7 +- CHANGELOG.md | 4 + Directory.Packages.props | 1 + README.md | 2 + SecsFrame.slnx | 1 + docs/ARCHITECTURE.md | 7 +- docs/COMPATIBILITY.md | 20 +- docs/HSMS-SESSION-STATE-MACHINE.md | 7 +- docs/ROADMAP.md | 15 +- docs/SECS4NET-INTEROP.md | 59 ++++ docs/STANDARDS.md | 4 + docs/STREAMFRAME-ADAPTER.md | 6 +- .../Secs4NetInteropTests.cs | 295 ++++++++++++++++++ .../SecsFrame.Secs4NetInterop.Tests.csproj | 21 ++ 14 files changed, 423 insertions(+), 26 deletions(-) create mode 100644 docs/SECS4NET-INTEROP.md create mode 100644 test/SecsFrame.Secs4NetInterop.Tests/Secs4NetInteropTests.cs create mode 100644 test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8273414..8c66d02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,11 +29,14 @@ jobs: 10.0.x - name: Build run: dotnet build SecsFrame.slnx -c Release - - name: Test + - name: Test supported projects shell: bash env: FRAMEWORKS: ${{ matrix.frameworks }} run: | for fw in $FRAMEWORKS; do - dotnet test SecsFrame.slnx -c Release --no-build -f "$fw" + dotnet test test/SecsFrame.Tests/SecsFrame.Tests.csproj -c Release --no-build -f "$fw" + if [ "$fw" != "net48" ]; then + dotnet test test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj -c Release --no-build -f "$fw" + fi done diff --git a/CHANGELOG.md b/CHANGELOG.md index e396ee2..81fc041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,5 +47,9 @@ 单消费者事件流、入站一次性回复和控制命令。 - 增加公共 API 选项边界、生命周期、等待/事务取消和 Active/Passive 双端真实 TCP 往返测试;入站回复令牌绑定原连接与原运输会话。 +- 增加独立的官方 Secs4Net 3.1.0 NuGet 互操作测试项目, + 由中央包管理固定版本且只从 nuget.org 还原,不引用本地源码 checkout。 +- 增加双方 Active/Passive、Select、双方 Linktest、双方 + Primary/Secondary 及嵌套 Item 边界值的真实 TCP 跨实现测试。 [Unreleased]: https://github.com/CSJ608/SecsFrame/compare/main...HEAD diff --git a/Directory.Packages.props b/Directory.Packages.props index 466245e..e8e3141 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/README.md b/README.md index c5dbfc4..d05d080 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Deselect、Separate、T6/T7 和 Selected 数据门控。内部数据事务 actor [docs/HSMS-DATA-TRANSACTIONS.md](docs/HSMS-DATA-TRANSACTIONS.md), 公共连接用法见 [docs/HSMS-CONNECTION.md](docs/HSMS-CONNECTION.md), +官方 secs4net 跨实现证据见 +[docs/SECS4NET-INTEROP.md](docs/SECS4NET-INTEROP.md), 整体路线图见 [docs/ROADMAP.md](docs/ROADMAP.md)。 diff --git a/SecsFrame.slnx b/SecsFrame.slnx index b657fde..625e5de 100644 --- a/SecsFrame.slnx +++ b/SecsFrame.slnx @@ -3,6 +3,7 @@ + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 93b541d..0fb31c2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -62,9 +62,10 @@ HSMS 会话切换后,旧会话的控制请求、数据事务和等待中的应 - 原始接收字节独立跟踪长度前缀和剩余 Payload,仅有未完成帧时运行 T8, 并用代次标识隔离接收进展与替换会话的陈旧回调。 -这些能力隔离 StreamFrame #38/#39 尚未提供的高级语义,不进入公共 API。 -上游支持后只替换 IHsmsTransport 实现。详细失效模式与替换 -条件见 [STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。 +这些能力隔离当前 StreamFrame 2.2.0 尚未提供的高级语义,不进入公共 +API。#38 已合入上游但尚未发布,#39 正在实现;正式包可用后只替换 +IHsmsTransport 实现。详细失效模式与替换条件见 +[STREAMFRAME-ADAPTER.md](STREAMFRAME-ADAPTER.md)。 ### 单线程会话状态机 diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 6c7fc49..e953608 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -19,8 +19,9 @@ ## 上游能力适配 -StreamFrame #38(未完成帧超时)和 #39(会话感知发送确认与消息上下文) -仍未提供原生 API。SecsFrame 当前通过内部 +StreamFrame #38(未完成帧超时)已经合入上游 main,但尚未 +进入正式 NuGet 版本;#39(会话感知发送确认与消息上下文)正在实现。 +SecsFrame 当前固定使用 StreamFrame 2.2.0,并通过内部 StreamFrameHsmsTransport 补足对应语义。 这不是设备兼容模式: @@ -35,10 +36,13 @@ StreamFrame #38(未完成帧超时)和 #39(会话感知发送确认与消 ## 跨实现测试依赖 -计划中的 secs4net 互操作夹具只能使用 nuget.org 发布的官方 NuGet 包, -并在 Directory.Packages.props 中固定明确版本。不得使用 -本地源码 checkout、ProjectReference、本地 feed 或自行修改的程序集作为 -互操作对端。源码仓库只读参考不进入 SecsFrame 的还原、构建或测试图。 +当前 secs4net 互操作夹具使用 nuget.org 发布的官方 +Secs4Net 3.1.0 包,并在 +Directory.Packages.props 中固定版本。仓库 +NuGet.Config 清除其它源并把全部包映射到 nuget.org。不得 +使用本地源码 checkout、ProjectReference、本地 feed 或自行修改的程序集 +作为互操作对端。源码仓库只读参考不进入 SecsFrame 的还原、构建或测试图。 -夹具和文档必须记录官方包 ID、版本、运行框架、测试方向与通过/失败结果, -避免把本地差异误判为上游互操作能力。 +包身份、运行框架、测试方向与结果见 +[SECS4NET-INTEROP.md](SECS4NET-INTEROP.md)。这些证据避免把本地差异 +误判为上游互操作能力。 diff --git a/docs/HSMS-SESSION-STATE-MACHINE.md b/docs/HSMS-SESSION-STATE-MACHINE.md index c7a0984..85b4caa 100644 --- a/docs/HSMS-SESSION-STATE-MACHINE.md +++ b/docs/HSMS-SESSION-STATE-MACHINE.md @@ -79,14 +79,17 @@ System Bytes。旧会话的帧、发送完成和计时器回调不会推进替 事务细节见 [HSMS-DATA-TRANSACTIONS.md](HSMS-DATA-TRANSACTIONS.md)。 公共 HsmsConnection 另行验证同一路径的外部组合契约,见 [HSMS-CONNECTION.md](HSMS-CONNECTION.md)。 -测试证明当前实现路径可互操作,不替代认证工具或第三方设备互操作证据。 +独立夹具还与 nuget.org 官方 Secs4Net 3.1.0 在双方 Active/Passive 模式下 +完成 Select、双方 Linktest 和双方 Primary/Secondary,见 +[SECS4NET-INTEROP.md](SECS4NET-INTEROP.md)。测试证明当前实现路径可互操作, +不替代认证工具或真实设备证据。 ## 尚未实现 - 授权标准核对后的 T5/T8 默认值和完整标准语义; - 自动周期 Linktest 调度; - Host/Equipment 能力 API; -- 与 secs4net 或真实设备的跨实现互操作矩阵。 +- 与真实设备、认证工具和更多实现版本的互操作矩阵。 实现这些行为前,需要使用团队合法获得的 SEMI E37-0222 与 E37.1-0819 副本核对精确状态转换、控制字段、状态/原因值和计时边界。仓库不得提交 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3c6b2c0..d1c4db4 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -18,15 +18,14 @@ - [x] 显式 T5 固定重连节流、T8 接收进展监视和跨会话旧回调隔离。 - [x] 最小公共连接外观、显式 T3/T5/T6/T7/T8 配置、动态消息事件与 确定的取消/释放语义。 +- [x] 使用 nuget.org 官方 Secs4Net 3.1.0 的 Active/Passive、Select、 + 双方 Linktest 和双方 Primary/Secondary 跨实现测试。 - [ ] 使用授权 E37/E37.1 核对 T5/T8 默认值与精确启停边界。 -- Host、Equipment 双角色回环集成测试,以及与 nuget.org 官方 secs4net - 包的互操作测试。 - -阶段 1 的下一最小切片是跨实现互操作夹具:通过中央包管理固定 -nuget.org 官方 secs4net 包版本,覆盖 Select、Linktest 和 -Primary/Secondary。不得引用、构建或链接本地 secs4net 源码 checkout。 -随后根据互操作结果收敛公共诊断模型。任何标准默认值和完整合规结论都 -依赖团队合法获得的 E37/E37.1 副本与一致性测试。 +- [ ] Host、Equipment 双角色能力及对应回环集成测试。 + +阶段 1 的下一最小切片是根据跨实现证据收敛公共诊断模型,同时继续跟踪 +StreamFrame #38/#39 的正式包发布并准备等价迁移。任何标准默认值和完整 +合规结论都依赖团队合法获得的 E37/E37.1 副本与一致性测试。 ## 阶段 2:GEM 通用能力 diff --git a/docs/SECS4NET-INTEROP.md b/docs/SECS4NET-INTEROP.md new file mode 100644 index 0000000..44bf1ee --- /dev/null +++ b/docs/SECS4NET-INTEROP.md @@ -0,0 +1,59 @@ +# 官方 secs4net 互操作证据 + +## 依赖身份 + +互操作项目 SecsFrame.Secs4NetInterop.Tests 只引用 nuget.org +发布的官方包: + +| 字段 | 值 | +|---|---| +| Package ID | Secs4Net | +| 固定版本 | 3.1.0 | +| NuGet 源 | https://api.nuget.org/v3/index.json | +| 作者 / 许可 | mkjeff / MIT | +| 包内仓库提交 | 6f540a7540fc7a531d1d526c04375f4b05a7641f | +| 测试框架 | net8.0、net10.0 | + +版本由根目录 Directory.Packages.props 中央管理。仓库 +NuGet.Config 先清除继承源,再把所有包映射到 nuget.org。 +测试项目没有指向 secs4net 源码的 ProjectReference、本地 feed +或程序集路径;本地源码 checkout 不参与还原、编译或运行。 + +Secs4Net 3.1.0 的正式包只提供 net8.0 和 net10.0 资产,所以互操作项目 +只运行这两个目标。SecsFrame 自身的 net48 构建和主测试矩阵继续保留, +但不伪造官方包未提供的 net48 跨实现结果。 + +## 真实 TCP 矩阵 + +每个目标框架都在 IPv4 loopback 上运行以下两种独立拓扑: + +| SecsFrame | 官方 Secs4Net | Select 发起方 | 已验证行为 | +|---|---|---|---| +| Active | Passive | SecsFrame | Select、双方 Linktest、双方 Primary/Secondary | +| Passive | Active | Secs4Net | Select、双方 Linktest、双方 Primary/Secondary | + +“双方 Linktest”包括 SecsFrame 主动请求并等待官方实现响应,以及启用官方 +实现的周期 Linktest 后确认它收到 SecsFrame 的响应。测试中的短周期只用于 +确定性验证,不是标准默认值或部署建议。 + +## 消息向量 + +SecsFrame 到 Secs4Net 使用带 W-Bit 的 S6F11,根 Item 包含嵌套 List、 +ASCII、U1 的 0/最大值、I4 的最小/最大值和 Boolean;官方实现读取每个值 +后以 S6F12 Boolean 回复。反向使用带 W-Bit 的 S1F1,包含 ASCII、嵌套 +List、U2 的 0/最大值和 Boolean;SecsFrame 严格解码后以 S1F2 Binary +回复。两边都验证 Stream、Function、协议 Session/Device ID 和 Item 值。 + +每个目标框架包含两个测试用例,对应上述两种连接拓扑。验证命令: + +~~~bash +dotnet test test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj -c Release -f net8.0 +dotnet test test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj -c Release -f net10.0 +~~~ + +## 结论边界 + +该矩阵证明 SecsFrame 当前工程实现能与指定官方软件版本完成所列 HSMS-SS +控制流程和动态 SECS-II 消息往返。它没有覆盖真实设备、网络故障注入、 +所有 Item 组合、全部控制状态或长期运行,也不替代 SEMI 标准核对、认证 +工具或一致性测试,不据此声明完整 E5/E37/E37.1 合规。 diff --git a/docs/STANDARDS.md b/docs/STANDARDS.md index eac65ce..c5ba911 100644 --- a/docs/STANDARDS.md +++ b/docs/STANDARDS.md @@ -60,3 +60,7 @@ 构建;互操作夹具只允许使用 nuget.org 发布并固定版本的官方 NuGet 包。 未来如引入实质性开源代码,必须保留对应版权与许可证文本,并在变更说明 中记录来源。 + +与官方 Secs4Net 3.1.0 的 Select、Linktest 和动态消息往返是独立软件实现 +之间的工程互操作证据,不代表任何一方通过 SEMI 一致性认证。具体矩阵见 +[SECS4NET-INTEROP.md](SECS4NET-INTEROP.md)。 diff --git a/docs/STREAMFRAME-ADAPTER.md b/docs/STREAMFRAME-ADAPTER.md index 94b1634..dcaada8 100644 --- a/docs/STREAMFRAME-ADAPTER.md +++ b/docs/STREAMFRAME-ADAPTER.md @@ -5,9 +5,9 @@ SecsFrame 当前依赖 StreamFrame 2.2.0。以下上游能力仍在跟踪: - [StreamFrame #38](https://github.com/CSJ608/StreamFrame/issues/38): - 未完成帧超时; + 未完成帧超时已经合入上游 main,但尚未进入正式包; - [StreamFrame #39](https://github.com/CSJ608/StreamFrame/issues/39): - 会话感知的发送确认与消息上下文。 + 会话感知的发送确认与消息上下文正在实现。 StreamFrame 现有 SendAsync 在消息进入连接级队列后完成,队列 会跨 TCP 重连保留;GetMessages 也是跨重连的稳定消息流。 @@ -83,7 +83,7 @@ T8 到期关闭。 ## 替换条件 -当 #38/#39 发布稳定 API 后: +当 #38/#39 都发布稳定 NuGet API 后: 1. 运行现有会话、发送确认、迟到消息和超时测试向量验证语义等价; 2. 用原生 Session ID、发送完成任务和未完成帧超时替换内部回调适配; diff --git a/test/SecsFrame.Secs4NetInterop.Tests/Secs4NetInteropTests.cs b/test/SecsFrame.Secs4NetInterop.Tests/Secs4NetInteropTests.cs new file mode 100644 index 0000000..162f981 --- /dev/null +++ b/test/SecsFrame.Secs4NetInterop.Tests/Secs4NetInteropTests.cs @@ -0,0 +1,295 @@ +using System.Collections.Concurrent; +using System.Net; +using Microsoft.Extensions.Options; +using OfficialConnectionState = Secs4Net.ConnectionState; +using OfficialHsmsConnection = Secs4Net.HsmsConnection; +using OfficialItem = Secs4Net.Item; +using OfficialSecsGem = Secs4Net.SecsGem; +using OfficialSecsGemOptions = Secs4Net.SecsGemOptions; +using OfficialSecsMessage = Secs4Net.SecsMessage; + +namespace SecsFrame.Secs4NetInterop.Tests; + +public sealed class Secs4NetInteropTests +{ + private const ushort DeviceId = 10; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Official_package_interoperates_in_both_connection_modes( + bool secs4NetIsActive) + { + var port = GetFreePort(); + using var lifetime = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var logger = new RecordingSecs4NetLogger(); + var officialOptions = Options.Create( + CreateOfficialOptions(port, secs4NetIsActive)); + await using var connection = new HsmsConnection( + CreateSecsFrameOptions(port, secs4NetIsActive)); + await using var officialConnection = new OfficialHsmsConnection( + officialOptions, + logger); + using var officialGem = new OfficialSecsGem( + officialOptions, + officialConnection, + logger); + var officialSelected = WaitUntilOfficialSelectedAsync( + officialConnection, + lifetime.Token); + + connection.Start(); + await using var events = connection + .GetEventsAsync(lifetime.Token) + .GetAsyncEnumerator(); + officialConnection.Start(lifetime.Token); + await Task.WhenAll( + connection.WaitUntilSelectedAsync(lifetime.Token), + officialSelected).ConfigureAwait(true); + + await VerifyLinktestsAsync( + connection, + officialConnection, + logger, + lifetime.Token).ConfigureAwait(true); + await VerifySecsFramePrimaryAsync( + connection, + officialGem, + lifetime.Token).ConfigureAwait(true); + await VerifyOfficialPrimaryAsync( + connection, + officialGem, + events, + lifetime.Token).ConfigureAwait(true); + } + + private static async Task VerifyLinktestsAsync( + HsmsConnection connection, + OfficialHsmsConnection officialConnection, + RecordingSecs4NetLogger logger, + CancellationToken cancellationToken) + { + await connection.LinktestAsync(cancellationToken).ConfigureAwait(true); + + officialConnection.LinkTestEnabled = true; + try + { + await logger.WaitForInfoAsync( + "Receive Control message: LinkTestResponse", + cancellationToken).ConfigureAwait(true); + } + finally + { + officialConnection.LinkTestEnabled = false; + } + + Assert.Equal(HsmsSessionState.Selected, connection.State); + Assert.Equal(OfficialConnectionState.Selected, officialConnection.State); + } + + private static async Task VerifySecsFramePrimaryAsync( + HsmsConnection connection, + OfficialSecsGem officialGem, + CancellationToken cancellationToken) + { + var officialMessages = officialGem + .GetPrimaryMessageAsync(cancellationToken) + .GetAsyncEnumerator(); + await using var ownedMessages = officialMessages.ConfigureAwait(true); + var send = connection.SendAsync( + new SecsMessage( + 6, + 11, + true, + SecsItem.List( + SecsItem.Ascii("LOT-42"), + SecsItem.List( + SecsItem.U1(0, 1, byte.MaxValue), + SecsItem.I4(int.MinValue, int.MaxValue), + SecsItem.Boolean(false, true)))), + cancellationToken); + + Assert.True(await officialMessages.MoveNextAsync().ConfigureAwait(true)); + var incoming = officialMessages.Current; + using var primary = incoming.PrimaryMessage; + AssertOfficialPrimary(primary); + using var reply = new OfficialSecsMessage(6, 12, replyExpected: false) + { + SecsItem = OfficialItem.Boolean(true), + }; + Assert.True(await incoming.TryReplyAsync(reply, cancellationToken) + .ConfigureAwait(true)); + + var secondary = await send.ConfigureAwait(true); + Assert.NotNull(secondary); + Assert.Equal(6, secondary.Message.Stream); + Assert.Equal(12, secondary.Message.Function); + Assert.Equal(SecsItem.Boolean(true), secondary.Message.RootItem); + } + + private static async Task VerifyOfficialPrimaryAsync( + HsmsConnection connection, + OfficialSecsGem officialGem, + IAsyncEnumerator events, + CancellationToken cancellationToken) + { + using var primary = new OfficialSecsMessage(1, 1) + { + SecsItem = OfficialItem.L( + OfficialItem.A("EQ-01"), + OfficialItem.L( + OfficialItem.U2(0, ushort.MaxValue), + OfficialItem.Boolean(true, false))), + }; + var send = officialGem.SendAsync(primary, cancellationToken); + var received = await NextDataMessageAsync(events).ConfigureAwait(true); + var incoming = received.IncomingMessage!; + + Assert.Equal(DeviceId, incoming.DataMessage.SessionId); + Assert.Equal(1, incoming.DataMessage.Message.Stream); + Assert.Equal(1, incoming.DataMessage.Message.Function); + Assert.Equal( + SecsItem.List( + SecsItem.Ascii("EQ-01"), + SecsItem.List( + SecsItem.U2(0, ushort.MaxValue), + SecsItem.Boolean(true, false))), + incoming.DataMessage.Message.RootItem); + await connection.ReplyAsync( + incoming, + new SecsMessage( + 1, + 2, + rootItem: SecsItem.Binary(0)), + cancellationToken).ConfigureAwait(true); + + using var secondary = await send.ConfigureAwait(true); + Assert.Equal(1, secondary.S); + Assert.Equal(2, secondary.F); + Assert.Equal(new byte[] { 0 }, secondary.SecsItem!.GetMemory().ToArray()); + } + + private static void AssertOfficialPrimary(OfficialSecsMessage primary) + { + Assert.Equal(6, primary.S); + Assert.Equal(11, primary.F); + Assert.True(primary.ReplyExpected); + var root = primary.SecsItem!; + Assert.Equal("LOT-42", root[0].GetString()); + Assert.Equal( + new byte[] { 0, 1, byte.MaxValue }, + root[1][0].GetMemory().ToArray()); + Assert.Equal( + new[] { int.MinValue, int.MaxValue }, + root[1][1].GetMemory().ToArray()); + Assert.Equal( + new[] { false, true }, + root[1][2].GetMemory().ToArray()); + } + + private static async Task NextDataMessageAsync( + IAsyncEnumerator events) + { + while (await events.MoveNextAsync().ConfigureAwait(true)) + { + if (events.Current.Kind == HsmsConnectionEventKind.DataMessageReceived) + return events.Current; + } + + Assert.Fail("The expected secs4net Primary was not received."); + return null!; + } + + private static async Task WaitUntilOfficialSelectedAsync( + OfficialHsmsConnection connection, + CancellationToken cancellationToken) + { + if (connection.State == OfficialConnectionState.Selected) + return; + + var selected = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); + void OnStateChanged(object? sender, OfficialConnectionState state) + { + if (state == OfficialConnectionState.Selected) + selected.TrySetResult(true); + } + + connection.ConnectionChanged += OnStateChanged; + try + { + if (connection.State == OfficialConnectionState.Selected) + return; + + await selected.Task.WaitAsync(cancellationToken).ConfigureAwait(true); + } + finally + { + connection.ConnectionChanged -= OnStateChanged; + } + } + + private static HsmsConnectionOptions CreateSecsFrameOptions( + int port, + bool secs4NetIsActive) + => new( + IPAddress.Loopback, + port, + secs4NetIsActive + ? HsmsConnectionMode.Passive + : HsmsConnectionMode.Active, + DeviceId, + t3: TimeSpan.FromSeconds(5), + t5: TimeSpan.FromMilliseconds(25), + t6: TimeSpan.FromSeconds(2), + t7: TimeSpan.FromSeconds(5), + t8: TimeSpan.FromSeconds(2)); + + private static OfficialSecsGemOptions CreateOfficialOptions( + int port, + bool isActive) + => new() + { + DeviceId = DeviceId, + IsActive = isActive, + IpAddress = IPAddress.Loopback.ToString(), + Port = port, + T3 = 5000, + T5 = 25, + T6 = 2000, + T7 = 5000, + T8 = 2000, + LinkTestInterval = 100, + }; + + private static int GetFreePort() + { + var listener = new System.Net.Sockets.TcpListener( + IPAddress.Loopback, + 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private sealed class RecordingSecs4NetLogger : Secs4Net.ISecsGemLogger + { + private readonly ConcurrentDictionary> + _infoMessages = new(StringComparer.Ordinal); + + public void Info(string message) + => GetSignal(message).TrySetResult(true); + + public Task WaitForInfoAsync( + string message, + CancellationToken cancellationToken) + => GetSignal(message).Task.WaitAsync(cancellationToken); + + private TaskCompletionSource GetSignal(string message) + => _infoMessages.GetOrAdd( + message, + static _ => new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously)); + } +} diff --git a/test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj b/test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj new file mode 100644 index 0000000..43ac6b7 --- /dev/null +++ b/test/SecsFrame.Secs4NetInterop.Tests/SecsFrame.Secs4NetInterop.Tests.csproj @@ -0,0 +1,21 @@ + + + net8.0;net10.0 + false + true + + + + + + + + + + + + + + + +