test(twincat): failing repro for STAB-16 silent replay failure
This commit is contained in:
@@ -186,7 +186,7 @@
|
|||||||
{
|
{
|
||||||
"id": "B6.1",
|
"id": "B6.1",
|
||||||
"subject": "B6: failing ReplayFailure_EmitsBadQuality_AndDegradesHealth in TwinCATReconnectReplayTests \u2014 STAB-16 repro",
|
"subject": "B6: failing ReplayFailure_EmitsBadQuality_AndDegradesHealth in TwinCATReconnectReplayTests \u2014 STAB-16 repro",
|
||||||
"status": "pending",
|
"status": "completed",
|
||||||
"blockedBy": []
|
"blockedBy": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -170,6 +170,59 @@ public sealed class TwinCATReconnectReplayTests
|
|||||||
await drv.ShutdownAsync(TestContext.Current.CancellationToken);
|
await drv.ShutdownAsync(TestContext.Current.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- STAB-16 / STAB-17 / STAB-12 replay hardening ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// STAB-16: a replay failure must not be silent — the registration is marked dead, a Bad
|
||||||
|
/// snapshot is pushed to subscribers (instead of the frozen-Good-stale value), and health
|
||||||
|
/// degrades preserving the last successful read.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ReplayFailure_EmitsBadQuality_AndDegradesHealth()
|
||||||
|
{
|
||||||
|
var build = 0;
|
||||||
|
var factory = new FakeTwinCATClientFactory
|
||||||
|
{
|
||||||
|
// client1 normal; client2 (the reconnect target) fails AddNotification (replay) + reads.
|
||||||
|
Customise = () => new FakeTwinCATClient
|
||||||
|
{
|
||||||
|
ThrowOnAddNotification = ++build >= 2,
|
||||||
|
ThrowOnRead = build >= 2,
|
||||||
|
Exception = new InvalidOperationException("ADS notification quota exceeded"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var drv = new TwinCATDriver(new TwinCATDriverOptions
|
||||||
|
{
|
||||||
|
Devices = [new TwinCATDeviceOptions(Host)],
|
||||||
|
Tags = [new TwinCATTagDefinition("Speed", Host, "MAIN.Speed", TwinCATDataType.DInt)],
|
||||||
|
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||||
|
UseNativeNotifications = true,
|
||||||
|
}, "drv-1", factory);
|
||||||
|
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var events = new ConcurrentQueue<DataChangeEventArgs>();
|
||||||
|
drv.OnDataChange += (_, e) => events.Enqueue(e);
|
||||||
|
|
||||||
|
_ = await drv.SubscribeAsync(["Speed"], TimeSpan.FromMilliseconds(250), TestContext.Current.CancellationToken);
|
||||||
|
// Establish a last-successful-read on the healthy client1.
|
||||||
|
_ = await drv.ReadAsync(["Speed"], TestContext.Current.CancellationToken);
|
||||||
|
var lastGoodRead = drv.GetHealth().LastSuccessfulRead;
|
||||||
|
lastGoodRead.ShouldNotBeNull();
|
||||||
|
|
||||||
|
// Drop the wire; the next read reconnects onto client2, whose replay AddNotification fails.
|
||||||
|
factory.Clients[0].SimulateWireDrop();
|
||||||
|
_ = await drv.ReadAsync(["Speed"], TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// STAB-16: subscribers see the tag go Bad (not frozen Good), and health degraded while
|
||||||
|
// preserving the last successful read.
|
||||||
|
var bad = events.SingleOrDefault(e => e.FullReference == "Speed"
|
||||||
|
&& e.Snapshot.StatusCode == TwinCATStatusMapper.BadCommunicationError);
|
||||||
|
bad.ShouldNotBeNull("a failed replay must publish a Bad snapshot for the affected reference");
|
||||||
|
var health = drv.GetHealth();
|
||||||
|
health.State.ShouldBe(DriverState.Degraded);
|
||||||
|
health.LastSuccessfulRead.ShouldBe(lastGoodRead);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
|
private static async Task WaitForAsync(Func<bool> condition, TimeSpan timeout)
|
||||||
{
|
{
|
||||||
var deadline = DateTime.UtcNow + timeout;
|
var deadline = DateTime.UtcNow + timeout;
|
||||||
|
|||||||
Reference in New Issue
Block a user