diff --git a/.github/workflows/stability-test.yml b/.github/workflows/stability-test.yml index 9ade56ae2..2d996ebc9 100644 --- a/.github/workflows/stability-test.yml +++ b/.github/workflows/stability-test.yml @@ -17,12 +17,13 @@ jobs: name: Connection Stability Test runs-on: ubuntu-latest timeout-minutes: 120 # Allow extra time beyond test duration for setup/teardown - + permissions: contents: read env: DOTNET_VERSION: '10.0.x' + TARGET_FRAMEWORK: 'net10.0' CONFIGURATION: 'Release' TEST_DURATION_MINUTES: ${{ github.event.inputs.duration || '90' }} @@ -53,7 +54,7 @@ jobs: dotnet test ./Tests/Opc.Ua.Client.Tests/Opc.Ua.Client.Tests.csproj \ --configuration ${{ env.CONFIGURATION }} \ --no-build \ - --framework ${{ env.DOTNET_VERSION }} + --framework ${{ env.TARGET_FRAMEWORK }} \ --filter "Category=ConnectionStability" \ --logger "console;verbosity=detailed" \ --results-directory ./TestResults diff --git a/Tests/Opc.Ua.Client.Tests/ConnectionStabilityTest.cs b/Tests/Opc.Ua.Client.Tests/ConnectionStabilityTest.cs index a61ac135a..f159f2a3c 100644 --- a/Tests/Opc.Ua.Client.Tests/ConnectionStabilityTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ConnectionStabilityTest.cs @@ -43,69 +43,80 @@ namespace Opc.Ua.Client.Tests /// Long-running connection stability test. /// [TestFixture] - [Category("ConnectionStability")] [SetCulture("en-us")] [SetUICulture("en-us")] + [Category("Client")] public class ConnectionStabilityTest : ClientTestFramework { - private const int SecurityTokenLifetimeCIMs = 5 * 60 * 1000; // 5 minutes for CI - private const int SecurityTokenLifetimeLocalMs = 10 * 1000; // 10 seconds for local testing - private const int StatusReportIntervalSeconds = 60; // Report status every 60 seconds - private const double NotificationToleranceRatio = 0.95; // Accept 95% of expected notifications (5% tolerance) - - public ConnectionStabilityTest() - : base(Utils.UriSchemeOpcTcp) - { - SingleSession = false; - } - /// - /// Set up a Server and a Client instance. + /// 5 minutes for CI /// - [OneTimeSetUp] - public override async Task OneTimeSetUpAsync() - { - SupportsExternalServerUrl = true; - - // Check if running in CI environment - bool isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); + private const int kSecurityTokenLifetimeCIMs = 5 * 60 * 1000; - // Configure security token lifetime based on environment - // CI: 5 minutes to force 18 renewals in 90 minute test - // Local: 10 seconds to force 6 renewals in 1 minute test - int tokenLifetime = isCI ? SecurityTokenLifetimeCIMs : SecurityTokenLifetimeLocalMs; - - SecurityTokenLifetime = tokenLifetime; + /// + /// 10 seconds for local testing + /// + private const int kSecurityTokenLifetimeLocalMs = 10 * 1000; - await base.OneTimeSetUpAsync().ConfigureAwait(false); - } + /// + /// Report status every 60 seconds + /// + private const int kStatusReportIntervalSeconds = 60; /// - /// Tear down the Server and the Client. + /// Accept 95% of expected notifications (5% tolerance) /// - [OneTimeTearDown] - public override Task OneTimeTearDownAsync() + private const double kNotificationToleranceRatio = 0.95; + + public ConnectionStabilityTest() + : base(Utils.UriSchemeOpcTcp) { - return base.OneTimeTearDownAsync(); + SupportsExternalServerUrl = true; } - /// - /// Test setup. - /// - [SetUp] - public override Task SetUpAsync() + [Test] + [Order(100)] + public async Task ShortHaulStabilityTestAsync() { - return base.SetUpAsync(); + try + { + SecurityTokenLifetime = kSecurityTokenLifetimeLocalMs; + await OneTimeSetUpAsync().ConfigureAwait(false); + + // 2 minutes for local testing + await RunStabilityTestAsync(2).ConfigureAwait(false); + } + finally + { + await OneTimeTearDownAsync().ConfigureAwait(false); + } } - /// - /// Test teardown. - /// - [TearDown] - public override Task TearDownAsync() + [Test] + [Order(100)] + [Explicit] + [Category("ConnectionStability")] + public async Task LongHaulStabilityTestAsync() { - return base.TearDownAsync(); + try + { + SecurityTokenLifetime = kSecurityTokenLifetimeCIMs; + await OneTimeSetUpAsync().ConfigureAwait(false); + + // Configurable duration for CI testing + string envValue = Environment.GetEnvironmentVariable("TEST_DURATION_MINUTES"); + if (string.IsNullOrEmpty(envValue) || + !int.TryParse(envValue, out int minutes) || + minutes <= 0) + { + minutes = 90; // Default to 90 minutes for CI + } + await RunStabilityTestAsync(minutes).ConfigureAwait(false); + } + finally + { + await OneTimeTearDownAsync().ConfigureAwait(false); + } } /// @@ -113,24 +124,18 @@ public override Task TearDownAsync() /// Tests that: /// - Connection remains stable over extended period /// - Subscriptions deliver all expected values (no message loss) - /// - Security token renewals happen correctly (every 5 minutes in CI, every 10 seconds locally) - /// Duration can be configured via TEST_DURATION_MINUTES environment variable (default: 90 minutes CI, 1 minute local) + /// - Security token renewals happen correctly /// - [Test] - [Order(100)] - public async Task LongRunningStabilityTestAsync() + private async Task RunStabilityTestAsync(int testDurationMinutes) { // Get test duration from environment variable or use default - int testDurationMinutes = GetTestDurationMinutes(); int testDurationSeconds = testDurationMinutes * 60; + int tokenLifetimeMs = SecurityTokenLifetime; - // Determine token lifetime based on environment - bool isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); - int tokenLifetimeMs = isCI ? SecurityTokenLifetimeCIMs : SecurityTokenLifetimeLocalMs; - - TestContext.Out.WriteLine($"Starting connection stability test for {testDurationMinutes} minutes ({testDurationSeconds} seconds)"); - TestContext.Out.WriteLine($"Security token lifetime: {tokenLifetimeMs / 1000} seconds ({tokenLifetimeMs / 60000.0:F1} minutes)"); + TestContext.Out.WriteLine( + $"Starting connection stability test for {testDurationMinutes} minutes ({testDurationSeconds} seconds)"); + TestContext.Out.WriteLine( + $"Security token lifetime: {tokenLifetimeMs / 1000} seconds ({tokenLifetimeMs / 60000.0:F1} minutes)"); const int publishingInterval = 1000; // 1 second const int writerInterval = 2000; // 2 seconds @@ -155,7 +160,9 @@ public async Task LongRunningStabilityTestAsync() TestContext.Out.WriteLine($"Subscribing to {nodeIds.Count} nodes."); // Create session - session = await ClientFixture.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256).ConfigureAwait(false); + session = await ClientFixture.ConnectAsync( + ServerUrl, + SecurityPolicies.Basic256Sha256).ConfigureAwait(false); Assert.NotNull(session, "Failed to create session"); // Create subscription @@ -221,7 +228,9 @@ public async Task LongRunningStabilityTestAsync() TestContext.Out.WriteLine($"Subscription created with {subscription.MonitoredItemCount} monitored items"); // Create writer session - ISession writerSession = await ClientFixture.ConnectAsync(ServerUrl, SecurityPolicies.Basic256Sha256).ConfigureAwait(false); + ISession writerSession = await ClientFixture.ConnectAsync( + ServerUrl, + SecurityPolicies.Basic256Sha256).ConfigureAwait(false); Assert.NotNull(writerSession, "Failed to create writer session"); // Writer task - continuously write values @@ -282,7 +291,9 @@ public async Task LongRunningStabilityTestAsync() { try { - await Task.Delay(TimeSpan.FromSeconds(StatusReportIntervalSeconds), statusReportingCts.Token).ConfigureAwait(false); + await Task.Delay( + TimeSpan.FromSeconds(kStatusReportIntervalSeconds), + statusReportingCts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -291,7 +302,7 @@ public async Task LongRunningStabilityTestAsync() reportCount++; int totalNotifications = valueChanges.Values.Sum(); - int elapsedMinutes = reportCount * StatusReportIntervalSeconds / 60; + int elapsedMinutes = reportCount * kStatusReportIntervalSeconds / 60; TestContext.Out.WriteLine( $"[Status Report {reportCount}] Elapsed: {elapsedMinutes} minutes, " + @@ -302,7 +313,7 @@ public async Task LongRunningStabilityTestAsync() if (reportCount % 5 == 0) // Every 5 minutes { TestContext.Out.WriteLine("Per-node notification counts:"); - foreach (var kvp in valueChanges.OrderBy(x => x.Key.ToString())) + foreach (KeyValuePair kvp in valueChanges.OrderBy(x => x.Key.ToString())) { TestContext.Out.WriteLine($" {kvp.Key}: {kvp.Value} notifications"); } @@ -346,7 +357,7 @@ public async Task LongRunningStabilityTestAsync() TestContext.Out.WriteLine("=== Final Results ==="); TestContext.Out.WriteLine($"Test duration: {testDurationMinutes} minutes"); TestContext.Out.WriteLine($"Security token lifetime: {tokenLifetimeMs / 1000} seconds ({tokenLifetimeMs / 60000.0:F1} minutes)"); - TestContext.Out.WriteLine($"Expected token renewals: ~{(testDurationMinutes * 60000) / tokenLifetimeMs} times"); + TestContext.Out.WriteLine($"Expected token renewals: ~{testDurationMinutes * 60000 / tokenLifetimeMs} times"); TestContext.Out.WriteLine($"Total write operations: {writeCount}"); TestContext.Out.WriteLine($"Total errors: {errors.Count}"); @@ -367,10 +378,10 @@ public async Task LongRunningStabilityTestAsync() #if DEBUG TestContext.Out.WriteLine($" {nodeId}: {changes} notifications"); #endif - if (changes < (writeCount * NotificationToleranceRatio)) + if (changes < (writeCount * kNotificationToleranceRatio)) { allNodesReceivedData = false; - TestContext.Out.WriteLine($" WARNING: Expected at least {writeCount * NotificationToleranceRatio:F0} notifications"); + TestContext.Out.WriteLine($" WARNING: Expected at least {writeCount * kNotificationToleranceRatio:F0} notifications"); } } else @@ -408,7 +419,10 @@ public async Task LongRunningStabilityTestAsync() // Assertions Assert.IsTrue(allNodesReceivedData, "Not all nodes received expected data"); Assert.AreEqual(0, errors.Count, $"Test encountered {errors.Count} errors"); - Assert.GreaterOrEqual(totalNotifications, expectedMinNotifications, "Total notifications received is less than expected minimum"); + Assert.GreaterOrEqual( + totalNotifications, + expectedMinNotifications, + "Total notifications received is less than expected minimum"); TestContext.Out.WriteLine("Connection stability test PASSED"); } @@ -441,27 +455,5 @@ public async Task LongRunningStabilityTestAsync() } } } - - /// - /// Gets the test duration in minutes from environment variable or returns default. - /// - private int GetTestDurationMinutes() - { - string envValue = Environment.GetEnvironmentVariable("TEST_DURATION_MINUTES"); - - if (!string.IsNullOrEmpty(envValue) && int.TryParse(envValue, out int minutes) && minutes > 0) - { - return minutes; - } - - // Default to 90 minutes for nightly runs, but use 1 minute for manual/local testing - // CI: 90 minutes with 5-minute token lifetime = 18 renewals - // Local: 1 minute with 10-second token lifetime = 6 renewals - // Check if running in CI environment - bool isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || - !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); - - return isCI ? 90 : 1; // 90 minutes for CI (18 renewals), 1 minute for local (6 renewals) - } } }