@@ -43,94 +43,99 @@ namespace Opc.Ua.Client.Tests
4343 /// Long-running connection stability test.
4444 /// </summary>
4545 [ TestFixture ]
46- [ Category ( "ConnectionStability" ) ]
4746 [ SetCulture ( "en-us" ) ]
4847 [ SetUICulture ( "en-us" ) ]
48+ [ Category ( "Client" ) ]
4949 public class ConnectionStabilityTest : ClientTestFramework
5050 {
51- private const int SecurityTokenLifetimeCIMs = 5 * 60 * 1000 ; // 5 minutes for CI
52- private const int SecurityTokenLifetimeLocalMs = 10 * 1000 ; // 10 seconds for local testing
53- private const int StatusReportIntervalSeconds = 60 ; // Report status every 60 seconds
54- private const double NotificationToleranceRatio = 0.95 ; // Accept 95% of expected notifications (5% tolerance)
55-
56- public ConnectionStabilityTest ( )
57- : base ( Utils . UriSchemeOpcTcp )
58- {
59- SingleSession = false ;
60- }
61-
6251 /// <summary>
63- /// Set up a Server and a Client instance.
52+ /// 5 minutes for CI
6453 /// </summary>
65- [ OneTimeSetUp ]
66- public override async Task OneTimeSetUpAsync ( )
67- {
68- SupportsExternalServerUrl = true ;
69-
70- // Check if running in CI environment
71- bool isCI = ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "CI" ) ) ||
72- ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "GITHUB_ACTIONS" ) ) ;
54+ private const int kSecurityTokenLifetimeCIMs = 5 * 60 * 1000 ;
7355
74- // Configure security token lifetime based on environment
75- // CI: 5 minutes to force 18 renewals in 90 minute test
76- // Local: 10 seconds to force 6 renewals in 1 minute test
77- int tokenLifetime = isCI ? SecurityTokenLifetimeCIMs : SecurityTokenLifetimeLocalMs ;
78-
79- SecurityTokenLifetime = tokenLifetime ;
56+ /// <summary>
57+ /// 10 seconds for local testing
58+ /// </summary>
59+ private const int kSecurityTokenLifetimeLocalMs = 10 * 1000 ;
8060
81- await base . OneTimeSetUpAsync ( ) . ConfigureAwait ( false ) ;
82- }
61+ /// <summary>
62+ /// Report status every 60 seconds
63+ /// </summary>
64+ private const int kStatusReportIntervalSeconds = 60 ;
8365
8466 /// <summary>
85- /// Tear down the Server and the Client.
67+ /// Accept 95% of expected notifications (5% tolerance)
8668 /// </summary>
87- [ OneTimeTearDown ]
88- public override Task OneTimeTearDownAsync ( )
69+ private const double kNotificationToleranceRatio = 0.95 ;
70+
71+ public ConnectionStabilityTest ( )
72+ : base ( Utils . UriSchemeOpcTcp )
8973 {
90- return base . OneTimeTearDownAsync ( ) ;
74+ SupportsExternalServerUrl = true ;
9175 }
9276
93- /// <summary>
94- /// Test setup.
95- /// </summary>
96- [ SetUp ]
97- public override Task SetUpAsync ( )
77+ [ Test ]
78+ [ Order ( 100 ) ]
79+ public async Task ShortHaulStabilityTestAsync ( )
9880 {
99- return base . SetUpAsync ( ) ;
81+ try
82+ {
83+ SecurityTokenLifetime = kSecurityTokenLifetimeLocalMs ;
84+ await OneTimeSetUpAsync ( ) . ConfigureAwait ( false ) ;
85+
86+ // 2 minutes for local testing
87+ await RunStabilityTestAsync ( 2 ) . ConfigureAwait ( false ) ;
88+ }
89+ finally
90+ {
91+ await OneTimeTearDownAsync ( ) . ConfigureAwait ( false ) ;
92+ }
10093 }
10194
102- /// <summary>
103- /// Test teardown.
104- /// </summary>
105- [ TearDown ]
106- public override Task TearDownAsync ( )
95+ [ Test ]
96+ [ Order ( 100 ) ]
97+ [ Explicit ]
98+ [ Category ( "ConnectionStability" ) ]
99+ public async Task LongHaulStabilityTestAsync ( )
107100 {
108- return base . TearDownAsync ( ) ;
101+ try
102+ {
103+ SecurityTokenLifetime = kSecurityTokenLifetimeCIMs ;
104+ await OneTimeSetUpAsync ( ) . ConfigureAwait ( false ) ;
105+
106+ // Configurable duration for CI testing
107+ string envValue = Environment . GetEnvironmentVariable ( "TEST_DURATION_MINUTES" ) ;
108+ if ( string . IsNullOrEmpty ( envValue ) ||
109+ ! int . TryParse ( envValue , out int minutes ) ||
110+ minutes <= 0 )
111+ {
112+ minutes = 90 ; // Default to 90 minutes for CI
113+ }
114+ await RunStabilityTestAsync ( minutes ) . ConfigureAwait ( false ) ;
115+ }
116+ finally
117+ {
118+ await OneTimeTearDownAsync ( ) . ConfigureAwait ( false ) ;
119+ }
109120 }
110121
111122 /// <summary>
112123 /// Long-running test that verifies connection stability over a configurable duration.
113124 /// Tests that:
114125 /// - Connection remains stable over extended period
115126 /// - Subscriptions deliver all expected values (no message loss)
116- /// - Security token renewals happen correctly (every 5 minutes in CI, every 10 seconds locally)
117- /// Duration can be configured via TEST_DURATION_MINUTES environment variable (default: 90 minutes CI, 1 minute local)
127+ /// - Security token renewals happen correctly
118128 /// </summary>
119- [ Test ]
120- [ Order ( 100 ) ]
121- public async Task LongRunningStabilityTestAsync ( )
129+ private async Task RunStabilityTestAsync ( int testDurationMinutes )
122130 {
123131 // Get test duration from environment variable or use default
124- int testDurationMinutes = GetTestDurationMinutes ( ) ;
125132 int testDurationSeconds = testDurationMinutes * 60 ;
133+ int tokenLifetimeMs = SecurityTokenLifetime ;
126134
127- // Determine token lifetime based on environment
128- bool isCI = ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "CI" ) ) ||
129- ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "GITHUB_ACTIONS" ) ) ;
130- int tokenLifetimeMs = isCI ? SecurityTokenLifetimeCIMs : SecurityTokenLifetimeLocalMs ;
131-
132- TestContext . Out . WriteLine ( $ "Starting connection stability test for { testDurationMinutes } minutes ({ testDurationSeconds } seconds)") ;
133- TestContext . Out . WriteLine ( $ "Security token lifetime: { tokenLifetimeMs / 1000 } seconds ({ tokenLifetimeMs / 60000.0 : F1} minutes)") ;
135+ TestContext . Out . WriteLine (
136+ $ "Starting connection stability test for { testDurationMinutes } minutes ({ testDurationSeconds } seconds)") ;
137+ TestContext . Out . WriteLine (
138+ $ "Security token lifetime: { tokenLifetimeMs / 1000 } seconds ({ tokenLifetimeMs / 60000.0 : F1} minutes)") ;
134139
135140 const int publishingInterval = 1000 ; // 1 second
136141 const int writerInterval = 2000 ; // 2 seconds
@@ -155,7 +160,9 @@ public async Task LongRunningStabilityTestAsync()
155160 TestContext . Out . WriteLine ( $ "Subscribing to { nodeIds . Count } nodes.") ;
156161
157162 // Create session
158- session = await ClientFixture . ConnectAsync ( ServerUrl , SecurityPolicies . Basic256Sha256 ) . ConfigureAwait ( false ) ;
163+ session = await ClientFixture . ConnectAsync (
164+ ServerUrl ,
165+ SecurityPolicies . Basic256Sha256 ) . ConfigureAwait ( false ) ;
159166 Assert . NotNull ( session , "Failed to create session" ) ;
160167
161168 // Create subscription
@@ -221,7 +228,9 @@ public async Task LongRunningStabilityTestAsync()
221228 TestContext . Out . WriteLine ( $ "Subscription created with { subscription . MonitoredItemCount } monitored items") ;
222229
223230 // Create writer session
224- ISession writerSession = await ClientFixture . ConnectAsync ( ServerUrl , SecurityPolicies . Basic256Sha256 ) . ConfigureAwait ( false ) ;
231+ ISession writerSession = await ClientFixture . ConnectAsync (
232+ ServerUrl ,
233+ SecurityPolicies . Basic256Sha256 ) . ConfigureAwait ( false ) ;
225234 Assert . NotNull ( writerSession , "Failed to create writer session" ) ;
226235
227236 // Writer task - continuously write values
@@ -282,7 +291,9 @@ public async Task LongRunningStabilityTestAsync()
282291 {
283292 try
284293 {
285- await Task . Delay ( TimeSpan . FromSeconds ( StatusReportIntervalSeconds ) , statusReportingCts . Token ) . ConfigureAwait ( false ) ;
294+ await Task . Delay (
295+ TimeSpan . FromSeconds ( kStatusReportIntervalSeconds ) ,
296+ statusReportingCts . Token ) . ConfigureAwait ( false ) ;
286297 }
287298 catch ( OperationCanceledException )
288299 {
@@ -291,7 +302,7 @@ public async Task LongRunningStabilityTestAsync()
291302
292303 reportCount ++ ;
293304 int totalNotifications = valueChanges . Values . Sum ( ) ;
294- int elapsedMinutes = reportCount * StatusReportIntervalSeconds / 60 ;
305+ int elapsedMinutes = reportCount * kStatusReportIntervalSeconds / 60 ;
295306
296307 TestContext . Out . WriteLine (
297308 $ "[Status Report { reportCount } ] Elapsed: { elapsedMinutes } minutes, " +
@@ -302,7 +313,7 @@ public async Task LongRunningStabilityTestAsync()
302313 if ( reportCount % 5 == 0 ) // Every 5 minutes
303314 {
304315 TestContext . Out . WriteLine ( "Per-node notification counts:" ) ;
305- foreach ( var kvp in valueChanges . OrderBy ( x => x . Key . ToString ( ) ) )
316+ foreach ( KeyValuePair < NodeId , int > kvp in valueChanges . OrderBy ( x => x . Key . ToString ( ) ) )
306317 {
307318 TestContext . Out . WriteLine ( $ " { kvp . Key } : { kvp . Value } notifications") ;
308319 }
@@ -346,7 +357,7 @@ public async Task LongRunningStabilityTestAsync()
346357 TestContext . Out . WriteLine ( "=== Final Results ===" ) ;
347358 TestContext . Out . WriteLine ( $ "Test duration: { testDurationMinutes } minutes") ;
348359 TestContext . Out . WriteLine ( $ "Security token lifetime: { tokenLifetimeMs / 1000 } seconds ({ tokenLifetimeMs / 60000.0 : F1} minutes)") ;
349- TestContext . Out . WriteLine ( $ "Expected token renewals: ~{ ( testDurationMinutes * 60000 ) / tokenLifetimeMs } times") ;
360+ TestContext . Out . WriteLine ( $ "Expected token renewals: ~{ testDurationMinutes * 60000 / tokenLifetimeMs } times") ;
350361 TestContext . Out . WriteLine ( $ "Total write operations: { writeCount } ") ;
351362 TestContext . Out . WriteLine ( $ "Total errors: { errors . Count } ") ;
352363
@@ -367,10 +378,10 @@ public async Task LongRunningStabilityTestAsync()
367378#if DEBUG
368379 TestContext . Out . WriteLine ( $ " { nodeId } : { changes } notifications") ;
369380#endif
370- if ( changes < ( writeCount * NotificationToleranceRatio ) )
381+ if ( changes < ( writeCount * kNotificationToleranceRatio ) )
371382 {
372383 allNodesReceivedData = false ;
373- TestContext . Out . WriteLine ( $ " WARNING: Expected at least { writeCount * NotificationToleranceRatio : F0} notifications") ;
384+ TestContext . Out . WriteLine ( $ " WARNING: Expected at least { writeCount * kNotificationToleranceRatio : F0} notifications") ;
374385 }
375386 }
376387 else
@@ -408,7 +419,10 @@ public async Task LongRunningStabilityTestAsync()
408419 // Assertions
409420 Assert . IsTrue ( allNodesReceivedData , "Not all nodes received expected data" ) ;
410421 Assert . AreEqual ( 0 , errors . Count , $ "Test encountered { errors . Count } errors") ;
411- Assert . GreaterOrEqual ( totalNotifications , expectedMinNotifications , "Total notifications received is less than expected minimum" ) ;
422+ Assert . GreaterOrEqual (
423+ totalNotifications ,
424+ expectedMinNotifications ,
425+ "Total notifications received is less than expected minimum" ) ;
412426
413427 TestContext . Out . WriteLine ( "Connection stability test PASSED" ) ;
414428 }
@@ -441,27 +455,5 @@ public async Task LongRunningStabilityTestAsync()
441455 }
442456 }
443457 }
444-
445- /// <summary>
446- /// Gets the test duration in minutes from environment variable or returns default.
447- /// </summary>
448- private int GetTestDurationMinutes ( )
449- {
450- string envValue = Environment . GetEnvironmentVariable ( "TEST_DURATION_MINUTES" ) ;
451-
452- if ( ! string . IsNullOrEmpty ( envValue ) && int . TryParse ( envValue , out int minutes ) && minutes > 0 )
453- {
454- return minutes ;
455- }
456-
457- // Default to 90 minutes for nightly runs, but use 1 minute for manual/local testing
458- // CI: 90 minutes with 5-minute token lifetime = 18 renewals
459- // Local: 1 minute with 10-second token lifetime = 6 renewals
460- // Check if running in CI environment
461- bool isCI = ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "CI" ) ) ||
462- ! string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "GITHUB_ACTIONS" ) ) ;
463-
464- return isCI ? 90 : 1 ; // 90 minutes for CI (18 renewals), 1 minute for local (6 renewals)
465- }
466458 }
467459}
0 commit comments