100 lines
3.5 KiB
C#
100 lines
3.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// A deleted subscription must also leave the SDK session's <c>Subscriptions</c> list, or every alarm re-target in the UI
|
|
/// leaves a dead <see cref="Subscription"/> behind until the session is disposed.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>DeleteAsync detaches the wrapped subscription exactly once, even when Dispose follows.</summary>
|
|
[Fact]
|
|
public async Task DeleteAsync_detaches_the_subscription_from_its_session_once()
|
|
{
|
|
var subscription = new Subscription(ClientTelemetry.Context);
|
|
var detached = new List<Subscription>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>Dispose alone (no prior DeleteAsync) detaches too.</summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>A failing detach is logged, not thrown: teardown stays best effort.</summary>
|
|
[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));
|
|
}
|
|
}
|