Files
lmxopcua/tests/ZB.MOM.WW.LmxOpcUa.Tests/MxAccess/MxAccessClientMonitorTests.cs
Joseph Doherty 41a6b66943 Apply code style formatting and restore partial modifiers on Avalonia views
Linter/formatter pass across the full codebase. Restores required partial
keyword on AXAML code-behind classes that the formatter incorrectly removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:58:13 -04:00

172 lines
5.7 KiB
C#

using System;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
using ZB.MOM.WW.LmxOpcUa.Host.MxAccess;
using ZB.MOM.WW.LmxOpcUa.Tests.Helpers;
namespace ZB.MOM.WW.LmxOpcUa.Tests.MxAccess
{
/// <summary>
/// Verifies the background connectivity monitor used to reconnect the MXAccess bridge after faults or stale probes.
/// </summary>
public class MxAccessClientMonitorTests : IDisposable
{
private readonly PerformanceMetrics _metrics;
private readonly FakeMxProxy _proxy;
private readonly StaComThread _staThread;
/// <summary>
/// Initializes the monitor test fixture with a shared STA thread, fake proxy, and metrics collector.
/// </summary>
public MxAccessClientMonitorTests()
{
_staThread = new StaComThread();
_staThread.Start();
_proxy = new FakeMxProxy();
_metrics = new PerformanceMetrics();
}
/// <summary>
/// Disposes the monitor test fixture resources.
/// </summary>
public void Dispose()
{
_staThread.Dispose();
_metrics.Dispose();
}
/// <summary>
/// Confirms that the monitor reconnects the client after an observed disconnect.
/// </summary>
[Fact]
public async Task Monitor_ReconnectsOnDisconnect()
{
var config = new MxAccessConfiguration
{
MonitorIntervalSeconds = 1,
AutoReconnect = true
};
var client = new MxAccessClient(_staThread, _proxy, config, _metrics);
await client.ConnectAsync();
await client.DisconnectAsync();
client.StartMonitor();
// Wait for monitor to detect disconnect and reconnect
await Task.Delay(2500);
client.StopMonitor();
client.State.ShouldBe(ConnectionState.Connected);
client.ReconnectCount.ShouldBeGreaterThan(0);
client.Dispose();
}
/// <summary>
/// Confirms that the monitor can be started and stopped without throwing.
/// </summary>
[Fact]
public async Task Monitor_StopsOnCancel()
{
var config = new MxAccessConfiguration { MonitorIntervalSeconds = 1 };
var client = new MxAccessClient(_staThread, _proxy, config, _metrics);
await client.ConnectAsync();
client.StartMonitor();
client.StopMonitor();
// Should not throw
await Task.Delay(200);
client.Dispose();
}
/// <summary>
/// Confirms that a stale probe tag triggers a reconnect when monitoring is enabled.
/// </summary>
[Fact]
public async Task Monitor_ProbeStale_ForcesReconnect()
{
var config = new MxAccessConfiguration
{
ProbeTag = "TestProbe",
ProbeStaleThresholdSeconds = 2,
MonitorIntervalSeconds = 1,
AutoReconnect = true
};
var client = new MxAccessClient(_staThread, _proxy, config, _metrics);
await client.ConnectAsync();
client.StartMonitor();
// Wait long enough for probe to go stale (threshold=2s, monitor interval=1s)
// No data changes simulated → probe becomes stale → reconnect triggered
await Task.Delay(4000);
client.StopMonitor();
client.ReconnectCount.ShouldBeGreaterThan(0);
client.Dispose();
}
/// <summary>
/// Confirms that fresh probe updates prevent unnecessary reconnects.
/// </summary>
[Fact]
public async Task Monitor_ProbeDataChange_PreventsStaleReconnect()
{
var config = new MxAccessConfiguration
{
ProbeTag = "TestProbe",
ProbeStaleThresholdSeconds = 2,
MonitorIntervalSeconds = 1,
AutoReconnect = true
};
var client = new MxAccessClient(_staThread, _proxy, config, _metrics);
await client.ConnectAsync();
client.StartMonitor();
// Continuously simulate probe data changes to keep it fresh
for (var i = 0; i < 8; i++)
{
await Task.Delay(500);
_proxy.SimulateDataChangeByAddress("TestProbe", i);
}
client.StopMonitor();
// Probe was kept fresh → no reconnect should have happened
client.ReconnectCount.ShouldBe(0);
client.State.ShouldBe(ConnectionState.Connected);
client.Dispose();
}
/// <summary>
/// Confirms that enabling the monitor without a probe tag does not trigger false reconnects.
/// </summary>
[Fact]
public async Task Monitor_NoProbeConfigured_NoFalseReconnect()
{
var config = new MxAccessConfiguration
{
ProbeTag = null, // No probe
MonitorIntervalSeconds = 1,
AutoReconnect = true
};
var client = new MxAccessClient(_staThread, _proxy, config, _metrics);
await client.ConnectAsync();
client.StartMonitor();
// Wait several monitor cycles — should stay connected with no reconnects
await Task.Delay(3000);
client.StopMonitor();
client.State.ShouldBe(ConnectionState.Connected);
client.ReconnectCount.ShouldBe(0);
client.Dispose();
}
}
}