using Opc.Ua.Client;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Tests.Adapters;
///
/// A deleted subscription must also leave the SDK session's Subscriptions list, or every alarm re-target in the UI
/// leaves a dead behind until the session is disposed.
///
public class DefaultSubscriptionAdapterTests
{
private sealed class CountingSubscription() : Subscription(ClientTelemetry.Context)
{
public int DisposeCount;
protected override void Dispose(bool disposing)
{
if (disposing) Interlocked.Increment(ref DisposeCount);
base.Dispose(disposing);
}
}
[Fact]
public async Task DeleteAndDispose_ReleaseSdkResourcesExactlyOnce()
{
var subscription = new CountingSubscription();
var detached = 0;
var adapter = new DefaultSubscriptionAdapter(subscription, (_, _) =>
{
Interlocked.Increment(ref detached);
return Task.CompletedTask;
});
await adapter.DeleteAsync(TestContext.Current.CancellationToken);
adapter.Dispose();
adapter.Dispose();
detached.ShouldBe(1);
subscription.DisposeCount.ShouldBe(1);
}
[Fact]
public async Task ConcurrentTeardown_HasOneSdkResourceOwner()
{
var subscription = new CountingSubscription();
var detached = 0;
var adapter = new DefaultSubscriptionAdapter(subscription, (_, _) =>
{
Interlocked.Increment(ref detached);
return Task.CompletedTask;
});
await Task.WhenAll(Task.Run(adapter.Dispose, TestContext.Current.CancellationToken), adapter.DeleteAsync(TestContext.Current.CancellationToken));
detached.ShouldBe(1);
subscription.DisposeCount.ShouldBe(1);
}
/// DeleteAsync detaches the wrapped subscription exactly once, even when Dispose follows.
[Fact]
public async Task DeleteAsync_detaches_the_subscription_from_its_session_once()
{
var subscription = new Subscription(ClientTelemetry.Context);
var detached = new List();
var adapter = new DefaultSubscriptionAdapter(subscription, (s, _) =>
{
lock (detached) detached.Add(s);
return Task.CompletedTask;
});
await adapter.DeleteAsync(TestContext.Current.CancellationToken);
adapter.Dispose();
detached.ShouldHaveSingleItem().ShouldBeSameAs(subscription);
}
/// Dispose alone (no prior DeleteAsync) detaches too.
[Fact]
public void Dispose_detaches_the_subscription_from_its_session()
{
var detached = 0;
var adapter = new DefaultSubscriptionAdapter(new Subscription(ClientTelemetry.Context), (_, _) =>
{
Interlocked.Increment(ref detached);
return Task.CompletedTask;
});
adapter.Dispose();
detached.ShouldBe(1);
}
/// A failing detach is logged, not thrown: teardown stays best effort.
[Fact]
public async Task A_failing_detach_does_not_throw_from_DeleteAsync()
{
var adapter = new DefaultSubscriptionAdapter(new Subscription(ClientTelemetry.Context),
(_, _) => throw new InvalidOperationException("boom"));
await Should.NotThrowAsync(() => adapter.DeleteAsync(TestContext.Current.CancellationToken));
}
}