diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/FakeSidecarServer.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/FakeSidecarServer.cs index df81d8e2..aef45461 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/FakeSidecarServer.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/FakeSidecarServer.cs @@ -1,4 +1,5 @@ -using System.IO.Pipes; +using System.Net; +using System.Net.Sockets; using MessagePack; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc; @@ -7,12 +8,14 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests; /// /// In-process fake of the Wonderware historian sidecar. Reuses the client-side framing /// code (which is byte-identical to the real sidecar) so the wire bytes round-trip -/// correctly without requiring the .NET 4.8 sidecar binary at test time. +/// correctly without requiring the .NET 4.8 sidecar binary at test time. Listens on a +/// loopback and serves one connection at a time, mirroring the +/// real sidecar's TcpFrameServer single-active-connection model. /// internal sealed class FakeSidecarServer : IAsyncDisposable { - private readonly string _pipeName; private readonly string _expectedSecret; + private readonly TcpListener _listener; private readonly CancellationTokenSource _cts = new(); private Task? _loop; @@ -55,141 +58,146 @@ internal sealed class FakeSidecarServer : IAsyncDisposable /// public bool StallAfterRequest { get; set; } - /// Initializes a new instance of FakeSidecarServer with the specified pipe name and expected secret. - /// The name of the named pipe to use for communication. + /// Initializes a new instance of FakeSidecarServer with the specified expected secret. /// The expected shared secret for handshake validation. - public FakeSidecarServer(string pipeName, string expectedSecret) + public FakeSidecarServer(string expectedSecret) { - _pipeName = pipeName; _expectedSecret = expectedSecret; + // Bind synchronously in the ctor so BoundPort is readable before StartAsync returns. + _listener = new TcpListener(IPAddress.Loopback, 0); + _listener.Start(); } - /// Gets the named pipe name used for communication. - public string PipeName => _pipeName; + /// Gets the loopback host the listener is bound to. + public string Host => "127.0.0.1"; - /// Starts the fake sidecar server asynchronously. + /// Gets the TCP port the listener actually bound (OS-assigned). + public int BoundPort => ((IPEndPoint)_listener.LocalEndpoint).Port; + + /// Starts the fake sidecar server asynchronously. The listener is already bound (ctor). public Task StartAsync() { _loop = Task.Run(() => RunAsync(_cts.Token)); - // Give the listener a moment to start so client connect doesn't race. - return Task.Delay(50); + return Task.CompletedTask; } private async Task RunAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { - await using var pipe = new NamedPipeServerStream( - _pipeName, PipeDirection.InOut, maxNumberOfServerInstances: 1, - PipeTransmissionMode.Byte, PipeOptions.Asynchronous, - inBufferSize: 64 * 1024, outBufferSize: 64 * 1024); - - try { await pipe.WaitForConnectionAsync(ct).ConfigureAwait(false); } + TcpClient tcpClient; + try { tcpClient = await _listener.AcceptTcpClientAsync(ct).ConfigureAwait(false); } catch (OperationCanceledException) { break; } + catch (ObjectDisposedException) { break; } - try + using (tcpClient) { - using var reader = new FrameReader(pipe, leaveOpen: true); - using var writer = new FrameWriter(pipe, leaveOpen: true); - - // Hello handshake. - var first = await reader.ReadFrameAsync(ct).ConfigureAwait(false); - if (first is null || first.Value.Kind != MessageKind.Hello) continue; - var hello = MessagePackSerializer.Deserialize(first.Value.Body); - - if (!string.Equals(hello.SharedSecret, _expectedSecret, StringComparison.Ordinal)) + try { - await writer.WriteAsync(MessageKind.HelloAck, new HelloAck { Accepted = false, RejectReason = "shared-secret-mismatch" }, ct); - continue; - } + tcpClient.NoDelay = true; + var stream = tcpClient.GetStream(); + using var reader = new FrameReader(stream, leaveOpen: true); + using var writer = new FrameWriter(stream, leaveOpen: true); - await writer.WriteAsync(MessageKind.HelloAck, new HelloAck { Accepted = true, HostName = "fake-sidecar" }, ct); + // Hello handshake. + var first = await reader.ReadFrameAsync(ct).ConfigureAwait(false); + if (first is null || first.Value.Kind != MessageKind.Hello) continue; + var hello = MessagePackSerializer.Deserialize(first.Value.Body); - if (DisconnectAfterHandshake) - { - DisconnectAfterHandshake = false; // arm once - pipe.Disconnect(); - continue; - } - - while (!ct.IsCancellationRequested) - { - var frame = await reader.ReadFrameAsync(ct).ConfigureAwait(false); - if (frame is null) break; - - // Drop before sending any reply — lets the client fall into its catch / - // retry path or propagate on second failure. - if (DisconnectBeforeReply) + if (!string.Equals(hello.SharedSecret, _expectedSecret, StringComparison.Ordinal)) { - pipe.Disconnect(); - break; - } - - // Stall indefinitely to let the client's call-timeout token fire. - if (StallAfterRequest) - { - await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false); - break; - } - - // Optionally send a deliberately wrong kind back to exercise - // InvalidDataException detection in the client's ExchangeAsync. - if (ReplyWithWrongKind.HasValue) - { - var wrongKind = ReplyWithWrongKind.Value; - ReplyWithWrongKind = null; // arm once - // Send an empty body with the wrong kind so the client can parse it. - await writer.WriteAsync(wrongKind, new ReadRawReply { Success = false }, ct); + await writer.WriteAsync(MessageKind.HelloAck, new HelloAck { Accepted = false, RejectReason = "shared-secret-mismatch" }, ct); continue; } - switch (frame.Value.Kind) + await writer.WriteAsync(MessageKind.HelloAck, new HelloAck { Accepted = true, HostName = "fake-sidecar" }, ct); + + if (DisconnectAfterHandshake) { - case MessageKind.ReadRawRequest: + DisconnectAfterHandshake = false; // arm once + tcpClient.Close(); + continue; + } + + while (!ct.IsCancellationRequested) + { + var frame = await reader.ReadFrameAsync(ct).ConfigureAwait(false); + if (frame is null) break; + + // Drop before sending any reply — lets the client fall into its catch / + // retry path or propagate on second failure. + if (DisconnectBeforeReply) { - var req = MessagePackSerializer.Deserialize(frame.Value.Body); - var reply = OnReadRaw(req); - reply.CorrelationId = req.CorrelationId; - await writer.WriteAsync(MessageKind.ReadRawReply, reply, ct); + tcpClient.Close(); break; } - case MessageKind.ReadProcessedRequest: + + // Stall indefinitely to let the client's call-timeout token fire. + if (StallAfterRequest) { - var req = MessagePackSerializer.Deserialize(frame.Value.Body); - var reply = OnReadProcessed(req); - reply.CorrelationId = req.CorrelationId; - await writer.WriteAsync(MessageKind.ReadProcessedReply, reply, ct); + await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false); break; } - case MessageKind.ReadAtTimeRequest: + + // Optionally send a deliberately wrong kind back to exercise + // InvalidDataException detection in the client's ExchangeAsync. + if (ReplyWithWrongKind.HasValue) { - var req = MessagePackSerializer.Deserialize(frame.Value.Body); - var reply = OnReadAtTime(req); - reply.CorrelationId = req.CorrelationId; - await writer.WriteAsync(MessageKind.ReadAtTimeReply, reply, ct); - break; + var wrongKind = ReplyWithWrongKind.Value; + ReplyWithWrongKind = null; // arm once + // Send an empty body with the wrong kind so the client can parse it. + await writer.WriteAsync(wrongKind, new ReadRawReply { Success = false }, ct); + continue; } - case MessageKind.ReadEventsRequest: + + switch (frame.Value.Kind) { - var req = MessagePackSerializer.Deserialize(frame.Value.Body); - var reply = OnReadEvents(req); - reply.CorrelationId = req.CorrelationId; - await writer.WriteAsync(MessageKind.ReadEventsReply, reply, ct); - break; - } - case MessageKind.WriteAlarmEventsRequest: - { - var req = MessagePackSerializer.Deserialize(frame.Value.Body); - var reply = OnWriteAlarmEvents(req); - reply.CorrelationId = req.CorrelationId; - await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct); - break; + case MessageKind.ReadRawRequest: + { + var req = MessagePackSerializer.Deserialize(frame.Value.Body); + var reply = OnReadRaw(req); + reply.CorrelationId = req.CorrelationId; + await writer.WriteAsync(MessageKind.ReadRawReply, reply, ct); + break; + } + case MessageKind.ReadProcessedRequest: + { + var req = MessagePackSerializer.Deserialize(frame.Value.Body); + var reply = OnReadProcessed(req); + reply.CorrelationId = req.CorrelationId; + await writer.WriteAsync(MessageKind.ReadProcessedReply, reply, ct); + break; + } + case MessageKind.ReadAtTimeRequest: + { + var req = MessagePackSerializer.Deserialize(frame.Value.Body); + var reply = OnReadAtTime(req); + reply.CorrelationId = req.CorrelationId; + await writer.WriteAsync(MessageKind.ReadAtTimeReply, reply, ct); + break; + } + case MessageKind.ReadEventsRequest: + { + var req = MessagePackSerializer.Deserialize(frame.Value.Body); + var reply = OnReadEvents(req); + reply.CorrelationId = req.CorrelationId; + await writer.WriteAsync(MessageKind.ReadEventsReply, reply, ct); + break; + } + case MessageKind.WriteAlarmEventsRequest: + { + var req = MessagePackSerializer.Deserialize(frame.Value.Body); + var reply = OnWriteAlarmEvents(req); + reply.CorrelationId = req.CorrelationId; + await writer.WriteAsync(MessageKind.WriteAlarmEventsReply, reply, ct); + break; + } } } } + catch (OperationCanceledException) { break; } + catch (IOException) { /* peer dropped — accept next */ } } - catch (OperationCanceledException) { break; } - catch (IOException) { /* peer dropped — accept next */ } } } @@ -197,6 +205,7 @@ internal sealed class FakeSidecarServer : IAsyncDisposable public async ValueTask DisposeAsync() { _cts.Cancel(); + try { _listener.Stop(); } catch { /* ignore */ } if (_loop is not null) { try { await _loop.ConfigureAwait(false); } catch { /* ignore shutdown errors */ } diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs index 9107e30b..bbb3c4e2 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests/WonderwareHistorianClientTests.cs @@ -5,14 +5,13 @@ using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; -using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Internal; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Tests; /// /// End-to-end tests for : every interface method -/// round-trips through a real named pipe against the in-process +/// round-trips over a real loopback TCP connection against the in-process /// , which reuses the client's own byte-identical framing /// code. Covers byte→uint quality mapping, BadNoData propagation for null aggregate /// buckets, alarm-write per-event status flow, Hello handshake rejection on bad secret, @@ -22,31 +21,30 @@ public sealed class WonderwareHistorianClientTests { private const string Secret = "test-secret-123"; - private static string UniquePipeName() => $"otopcua-historian-test-{Guid.NewGuid():N}"; - - private static WonderwareHistorianClientOptions OptsFor(string pipe) => new( - PipeName: pipe, + private static WonderwareHistorianClientOptions OptsFor(FakeSidecarServer server) => new( + PipeName: "", SharedSecret: Secret, PeerName: "test", ConnectTimeout: TimeSpan.FromSeconds(2), - CallTimeout: TimeSpan.FromSeconds(2)); + CallTimeout: TimeSpan.FromSeconds(2)) + { + Host = "127.0.0.1", + Port = server.BoundPort, + UseTls = false, + }; /// - /// Creates a client over a named pipe using the - /// factory seam. Existing pipe-based tests use this after the public ctor was flipped to TCP. + /// Creates a client over loopback TCP against the fake's bound port using the public ctor + /// (which dials TCP). /// - private static WonderwareHistorianClient PipeClientFor(string pipe) - { - var opts = OptsFor(pipe); - return WonderwareHistorianClient.ForTests(opts, ct => FrameChannel.DefaultNamedPipeConnectFactory(opts, ct)); - } + private static WonderwareHistorianClient TcpClientFor(FakeSidecarServer server) + => new(OptsFor(server)); /// Verifies that ReadRawAsync round-trips samples and maps quality bytes to OPC UA status codes. [Fact] public async Task ReadRawAsync_RoundTripsSamples_AndMapsQualityByteToOpcUaStatusCode() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadRaw = req => new ReadRawReply { @@ -70,7 +68,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var result = await client.ReadRawAsync("Tank.Level", new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 4, 30, 0, 0, 0, DateTimeKind.Utc), @@ -87,8 +85,7 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadProcessedAsync_NullBuckets_MapToBadNoData() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadProcessed = _ => new ReadProcessedReply { @@ -102,7 +99,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var result = await client.ReadProcessedAsync("Tank.Level", new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 4, 29, 0, 2, 0, DateTimeKind.Utc), @@ -119,11 +116,10 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadAtTimeAsync_PreservesTimestampOrder() { - var pipe = UniquePipeName(); var t1 = new DateTime(2026, 4, 29, 1, 0, 0, DateTimeKind.Utc); var t2 = new DateTime(2026, 4, 29, 2, 0, 0, DateTimeKind.Utc); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadAtTime = req => new ReadAtTimeReply { @@ -135,7 +131,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var result = await client.ReadAtTimeAsync("Tank.Level", new[] { t1, t2 }, CancellationToken.None); result.Samples.Count.ShouldBe(2); @@ -147,12 +143,11 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadAtTimeAsync_PartialAndReorderedReply_AlignsByTimestamp_AndFillsGapsAsBad() { - var pipe = UniquePipeName(); var t1 = new DateTime(2026, 4, 29, 1, 0, 0, DateTimeKind.Utc); var t2 = new DateTime(2026, 4, 29, 2, 0, 0, DateTimeKind.Utc); var t3 = new DateTime(2026, 4, 29, 3, 0, 0, DateTimeKind.Utc); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { // Sidecar returns only t3 and t1 (out of order), drops t2 entirely. A // contract-compliant client must realign by timestamp and synthesize a @@ -177,7 +172,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var result = await client.ReadAtTimeAsync("Tank.Level", new[] { t1, t2, t3 }, CancellationToken.None); // Result MUST be the same length and order as the request. @@ -201,9 +196,8 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadEventsAsync_PreservesEventFields() { - var pipe = UniquePipeName(); var eid = Guid.NewGuid().ToString("N"); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadEvents = _ => new ReadEventsReply { @@ -222,7 +216,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var result = await client.ReadEventsAsync("Tank.HiHi", new DateTime(2026, 4, 29, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 4, 30, 0, 0, 0, DateTimeKind.Utc), @@ -239,14 +233,13 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadRawAsync_ServerError_ThrowsInvalidOperation() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadRaw = _ => new ReadRawReply { Success = false, Error = "historian unreachable" }, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var ex = await Should.ThrowAsync(() => client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None)); @@ -257,8 +250,7 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task WriteBatchAsync_PerEventOk_MapsToAckOrRetryPlease() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnWriteAlarmEvents = req => new WriteAlarmEventsReply { @@ -268,7 +260,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var batch = new[] { new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "operator", null, DateTime.UtcNow), @@ -286,8 +278,7 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task WriteBatchAsync_WholeCallFailure_ReturnsRetryPleaseForEveryEvent() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnWriteAlarmEvents = _ => new WriteAlarmEventsReply { @@ -298,7 +289,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var batch = new[] { new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow), @@ -316,11 +307,10 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task Hello_BadSecret_ThrowsUnauthorizedAccess() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, "different-secret"); + await using var server = new FakeSidecarServer("different-secret"); await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var ex = await Should.ThrowAsync(() => client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None)); @@ -331,8 +321,7 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task Reconnect_AfterTransportDrop_RetriesOnce() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { // First connection drops after handshake → client retries on next call. DisconnectAfterHandshake = true, @@ -344,7 +333,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); // First call: handshake + dropped. Reconnect kicks in inside the channel; second // attempt within the same InvokeAsync succeeds. From the caller's perspective it's @@ -361,9 +350,8 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task GetHealthSnapshot_TracksSuccessAndFailureCounts() { - var pipe = UniquePipeName(); var failNext = false; - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadRaw = _ => failNext ? new ReadRawReply { Success = false, Error = "boom" } @@ -371,7 +359,7 @@ public sealed class WonderwareHistorianClientTests }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); await client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 1, CancellationToken.None); @@ -397,17 +385,16 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task WriteBatchAsync_TransportDropDuringWrite_ReturnsRetryPleaseForEveryEvent() { - var pipe = UniquePipeName(); // Server disconnects before replying to the write request. The client's single retry // reconnects; on the second attempt the server is still armed to disconnect, so both // attempts fail and the catch block fires. - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { DisconnectBeforeReply = true, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); var batch = new[] { new AlarmHistorianEvent("ev-1", "Tank/HiHi", "HiHi", "LimitAlarm", AlarmSeverity.High, "Activated", "msg", "u", null, DateTime.UtcNow), @@ -429,16 +416,15 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task InvokeAsync_BothAttemptsFailTransport_PropagatesException() { - var pipe = UniquePipeName(); // DisconnectBeforeReply stays true so both the first attempt and the single retry // inside InvokeAsync are dropped, causing the second ExchangeAsync to throw. - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { DisconnectBeforeReply = true, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); // ReadRawAsync uses Invoke, which propagates the exception when both attempts fail. await Should.ThrowAsync(() => @@ -452,21 +438,25 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadRawAsync_StalledSidecar_TimesOutWithOperationCanceledException() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { StallAfterRequest = true, }; await server.StartAsync(); var opts = new WonderwareHistorianClientOptions( - PipeName: pipe, + PipeName: "", SharedSecret: Secret, PeerName: "test", ConnectTimeout: TimeSpan.FromSeconds(2), - CallTimeout: TimeSpan.FromMilliseconds(500)); // short timeout for test speed + CallTimeout: TimeSpan.FromMilliseconds(500)) // short timeout for test speed + { + Host = "127.0.0.1", + Port = server.BoundPort, + UseTls = false, + }; - await using var client = WonderwareHistorianClient.ForTests(opts, ct => FrameChannel.DefaultNamedPipeConnectFactory(opts, ct)); + await using var client = new WonderwareHistorianClient(opts); // The stall means neither the first nor the retry can complete, so the timeout // linked-token should cancel the operation. @@ -482,11 +472,10 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadProcessedAsync_TotalAggregate_ThrowsNotSupported() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret); + await using var server = new FakeSidecarServer(Secret); await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); await Should.ThrowAsync(() => client.ReadProcessedAsync("Tag", @@ -502,15 +491,14 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task ReadRawAsync_SidecarRepliesWithWrongKind_ThrowsInvalidDataException() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { // Force the server to reply with ReadAtTimeReply instead of ReadRawReply. ReplyWithWrongKind = MessageKind.ReadAtTimeReply, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); await Should.ThrowAsync(() => client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 100, CancellationToken.None)); @@ -529,14 +517,13 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task GetHealthSnapshot_SidecarFailure_NeverInflatesSuccessCounter() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadRaw = _ => new ReadRawReply { Success = false, Error = "boom" }, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); await Should.ThrowAsync(() => client.ReadRawAsync("Tag", DateTime.UtcNow, DateTime.UtcNow, 1, CancellationToken.None)); @@ -560,14 +547,13 @@ public sealed class WonderwareHistorianClientTests [Fact] public async Task GetHealthSnapshot_ConcurrentCallsAndReads_CountersAreInternallyConsistent() { - var pipe = UniquePipeName(); - await using var server = new FakeSidecarServer(pipe, Secret) + await using var server = new FakeSidecarServer(Secret) { OnReadRaw = _ => new ReadRawReply { Success = true }, }; await server.StartAsync(); - await using var client = PipeClientFor(pipe); + await using var client = TcpClientFor(server); using var stop = new CancellationTokenSource(); var readerSawInconsistent = false;