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;