/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.Configuration; using Opc.Ua.Server.TestFramework; using Opc.Ua.Tests; namespace Opc.Ua.Server.Tests { /// /// Tests the deterministic synchronous and asynchronous disposal contracts of /// . /// [TestFixture] [Category("StandardServer")] [NonParallelizable] public class StandardServerDisposeAsyncTests { private sealed class TestableStandardServer : StandardServer { private int m_serverStoppingCount; public TestableStandardServer(ITelemetryContext telemetry) : base(telemetry) { } /// /// The number of times the orderly stop sequence entered /// . /// public int ServerStoppingCount => Volatile.Read(ref m_serverStoppingCount); protected override ValueTask OnServerStoppingAsync( CancellationToken cancellationToken = default) { Interlocked.Increment(ref m_serverStoppingCount); return base.OnServerStoppingAsync(cancellationToken); } } [Test] public async Task DisposeAsyncDisposesBaseResources() { ServerFixture fixture = CreateFixture(); TestableStandardServer? server = null; try { server = await fixture.StartAsync().ConfigureAwait(false); await server.DisposeAsync().ConfigureAwait(false); AssertReleased(server); } finally { await CleanupAsync(fixture, server!).ConfigureAwait(false); } } [Test] public async Task DisposeAsyncIsIdempotentAndDisposeAfterDisposeAsyncIsSafe() { TestableStandardServer server = CreateServer(); await server.DisposeAsync().ConfigureAwait(false); await server.DisposeAsync().ConfigureAwait(false); Assert.DoesNotThrow(server.Dispose); AssertReleased(server); } [Test] public async Task ConcurrentDisposeAndDisposeAsyncBothCompleteAndReleaseOnce() { ServerFixture fixture = CreateFixture(); TestableStandardServer? server = null; try { server = await fixture.StartAsync().ConfigureAwait(false); Task disposeTask = Task.Run(server.Dispose); Task disposeAsyncTask = server.DisposeAsync().AsTask(); await Task.WhenAll(disposeTask, disposeAsyncTask).ConfigureAwait(false); AssertReleased(server); } finally { await CleanupAsync(fixture, server!).ConfigureAwait(false); } } [Test] public async Task DisposeAsyncThenDisposeIsSafeAndReleasesOnce() { ServerFixture fixture = CreateFixture(); TestableStandardServer? server = null; try { server = await fixture.StartAsync().ConfigureAwait(false); await server.DisposeAsync().ConfigureAwait(false); Assert.DoesNotThrow(server.Dispose); AssertReleased(server); } finally { await CleanupAsync(fixture, server!).ConfigureAwait(false); } } [Test] public async Task DisposeAsyncOnNeverStartedServerReleasesCleanly() { TestableStandardServer server = CreateServer(); await server.DisposeAsync().ConfigureAwait(false); AssertReleased(server); } [Test] public async Task DisposeBlocksUntilShutdownCompletionAndReleasesOnce() { ServerFixture fixture = CreateFixture(); TestableStandardServer? server = null; try { server = await fixture.StartAsync().ConfigureAwait(false); await Task.Run(server.Dispose).ConfigureAwait(false); AssertReleased(server); } finally { await CleanupAsync(fixture, server!).ConfigureAwait(false); } } [Test] public async Task ConcurrentAndRepeatStopAsyncRunTeardownOnce() { ServerFixture fixture = CreateFixture(); TestableStandardServer? server = null; try { server = await fixture.StartAsync().ConfigureAwait(false); // Concurrent callers must join the single shared stop task. Task stopA = server.StopAsync().AsTask(); Task stopB = server.StopAsync().AsTask(); await Task.WhenAll(stopA, stopB).ConfigureAwait(false); // A later caller joins the same completed stop task and does not re-run teardown. await server.StopAsync().ConfigureAwait(false); Assert.That(server.ServerStoppingCount, Is.EqualTo(1)); } finally { await CleanupAsync(fixture, server!).ConfigureAwait(false); } } private static ServerFixture CreateFixture() { return new ServerFixture( telemetry => new TestableStandardServer(telemetry)) { SecurityNone = true }; } private static TestableStandardServer CreateServer() { return new TestableStandardServer(NUnitTelemetryContext.Create()); } private static void AssertReleased(TestableStandardServer server) { Assert.That(server.BaseResourcesDisposedForTest, Is.True); } private static async Task CleanupAsync( ServerFixture fixture, TestableStandardServer server) { if (server is not null && !server.BaseResourcesDisposedForTest) { await server.DisposeAsync().ConfigureAwait(false); } IApplicationInstance application = fixture.Application; if (application is not null) { await application.DisposeAsync().ConfigureAwait(false); } } } }