using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests;
///
/// Regression coverage for Driver.AbLegacy-006 — a per-tag libplctag runtime (a single
/// Tag handle) is cached and shared between the server read path and the poll loop.
/// A Tag is not safe for concurrent operations, so the driver must serialise the
/// Read → GetStatus → DecodeValue (and Encode → Write → GetStatus) sequence per runtime.
///
[Trait("Category", "Unit")]
public sealed class AbLegacyRuntimeConcurrencyTests
{
///
/// A fake runtime that records the maximum number of operations in flight against the
/// same handle. If the driver fails to serialise, two callers overlap inside the
/// Read → GetStatus → Decode window and exceeds 1.
///
private sealed class OverlapDetectingFake : FakeAbLegacyTag
{
private int _inFlight;
public int MaxConcurrent { get; private set; }
public OverlapDetectingFake(AbLegacyTagCreateParams p) : base(p) { }
public override async Task ReadAsync(CancellationToken ct)
{
EnterOp();
try
{
// Yield + small delay so an unserialised second caller is guaranteed to overlap.
await Task.Delay(15, ct).ConfigureAwait(false);
await base.ReadAsync(ct).ConfigureAwait(false);
}
finally { LeaveOp(); }
}
public override async Task WriteAsync(CancellationToken ct)
{
EnterOp();
try
{
await Task.Delay(15, ct).ConfigureAwait(false);
await base.WriteAsync(ct).ConfigureAwait(false);
}
finally { LeaveOp(); }
}
private void EnterOp()
{
var n = Interlocked.Increment(ref _inFlight);
lock (this) { if (n > MaxConcurrent) MaxConcurrent = n; }
}
private void LeaveOp() => Interlocked.Decrement(ref _inFlight);
}
[Fact]
public async Task Concurrent_reads_of_same_tag_are_serialised_against_the_shared_runtime()
{
OverlapDetectingFake? shared = null;
var factory = new FakeAbLegacyTagFactory
{
Customise = p =>
{
shared = new OverlapDetectingFake(p) { Value = 7 };
return shared;
},
};
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
// Eight callers race for the same tag — mimics the server read path + poll loop(s)
// hitting one cached runtime at once.
var reads = Enumerable.Range(0, 8)
.Select(_ => drv.ReadAsync(["X"], CancellationToken.None))
.ToArray();
await Task.WhenAll(reads);
shared.ShouldNotBeNull();
shared!.MaxConcurrent.ShouldBe(1, "operations on a shared libplctag Tag must not overlap");
reads.ShouldAllBe(r => r.Result.Single().Value!.Equals(7));
}
[Fact]
public async Task Concurrent_read_and_write_of_same_tag_do_not_overlap()
{
OverlapDetectingFake? shared = null;
var factory = new FakeAbLegacyTagFactory
{
Customise = p =>
{
shared = new OverlapDetectingFake(p) { Value = 1 };
return shared;
},
};
var drv = new AbLegacyDriver(new AbLegacyDriverOptions
{
Devices = [new AbLegacyDeviceOptions("ab://10.0.0.5/1,0")],
Tags = [new AbLegacyTagDefinition("X", "ab://10.0.0.5/1,0", "N7:0", AbLegacyDataType.Int)],
Probe = new AbLegacyProbeOptions { Enabled = false },
}, "drv-1", factory);
await drv.InitializeAsync("{}", CancellationToken.None);
var readTask = drv.ReadAsync(["X"], CancellationToken.None);
var writeTask = drv.WriteAsync([new WriteRequest("X", 99)], CancellationToken.None);
await Task.WhenAll(readTask, writeTask);
shared.ShouldNotBeNull();
shared!.MaxConcurrent.ShouldBe(1);
}
}