@@ -42,6 +42,22 @@ namespace Opc.Ua.Client.Tests
4242 [ Parallelizable ]
4343 public class SubscriptionUnitTests
4444 {
45+ private const PublishStateChangedMask kSessionNotConnected = PublishStateChangedMask . Stopped | PublishStateChangedMask . SessionNotConnected ;
46+
47+ public record KeepAliveTestDataProvider ( PublishStateChangedMask ExpectedPublishState ) : IFormattable
48+ {
49+ public bool SessionConnected { get ; init ; }
50+ public bool SessionReconnecting { get ; init ; }
51+ public bool SessionKeepAliveStopped { get ; init ; }
52+ public string ToString ( string format , IFormatProvider formatProvider )
53+ {
54+ return $ "Connected={ SessionConnected } , " +
55+ $ "reconnecting={ SessionReconnecting } , " +
56+ $ "keepAlive={ SessionKeepAliveStopped } . " +
57+ $ "Expected status:{ ExpectedPublishState } ";
58+ }
59+ }
60+
4561 private sealed class SubscriptionContainer : IDisposable
4662 {
4763 private readonly CancellationTokenRegistration m_tokedCancellation ;
@@ -73,28 +89,31 @@ private static Task AwaitForRepublishTimeout(CancellationToken ct)
7389 return Task . Delay ( Subscription . RepublishMessageTimeout + 100 , ct ) ;
7490 }
7591
76- private static ISession BuildSessionMock ( Func < uint , uint , bool > republishHandler )
92+ private static ISession BuildSessionMock ( Func < uint , uint , bool > republishHandler = null , Action < Mock < ISession > > setup = null )
7793 {
7894 uint subscriptionIdSeed = 0u ;
7995
8096 var session = new Mock < ISession > ( ) ;
81- session
82- . Setup ( x => x . RepublishAsync ( It . IsAny < uint > ( ) , It . IsAny < uint > ( ) , It . IsAny < CancellationToken > ( ) ) )
83- . ReturnsAsync < uint , uint , CancellationToken , ISession , ( bool , ServiceResult ) > ( (
84- subscriptionId ,
85- sequenceNumber ,
86- ct ) =>
87- {
88- if ( subscriptionId > subscriptionIdSeed )
89- {
90- return ( true , StatusCodes . BadSubscriptionIdInvalid ) ;
91- }
92- if ( republishHandler ( subscriptionId , sequenceNumber ) )
97+ if ( republishHandler is not null )
98+ {
99+ session
100+ . Setup ( x => x . RepublishAsync ( It . IsAny < uint > ( ) , It . IsAny < uint > ( ) , It . IsAny < CancellationToken > ( ) ) )
101+ . ReturnsAsync < uint , uint , CancellationToken , ISession , ( bool , ServiceResult ) > ( (
102+ subscriptionId ,
103+ sequenceNumber ,
104+ ct ) =>
93105 {
94- return ( true , ServiceResult . Good ) ;
95- }
96- return ( true , StatusCodes . BadMessageNotAvailable ) ;
97- } ) ;
106+ if ( subscriptionId > subscriptionIdSeed )
107+ {
108+ return ( true , StatusCodes . BadSubscriptionIdInvalid ) ;
109+ }
110+ if ( republishHandler ( subscriptionId , sequenceNumber ) )
111+ {
112+ return ( true , ServiceResult . Good ) ;
113+ }
114+ return ( true , StatusCodes . BadMessageNotAvailable ) ;
115+ } ) ;
116+ }
98117 session
99118 . Setup ( x => x
100119 . CreateSubscriptionAsync (
@@ -149,6 +168,7 @@ private static ISession BuildSessionMock(Func<uint, uint, bool> republishHandler
149168 StatusCodes . Good ) ] ,
150169 DiagnosticInfos = [ .. subscriptionIds . ConvertAll ( _ => new DiagnosticInfo ( ) ) ]
151170 } ) ;
171+ setup ? . Invoke ( session ) ;
152172 return session . Object ;
153173 }
154174
@@ -350,5 +370,48 @@ public async Task WillRepublishIfMissedMessagesInBetweenOfPublishesAsync(
350370 Assert . That ( subscription . Notifications , Is . EquivalentTo ( messages . Skip ( 1 ) ) ) ;
351371 }
352372 }
373+
374+ [ DatapointSource ]
375+ public IEnumerable < KeepAliveTestDataProvider > SubscriptionKeepAliveValues ( )
376+ {
377+ yield return new ( PublishStateChangedMask . Stopped ) { SessionConnected = true , SessionReconnecting = false , SessionKeepAliveStopped = false } ;
378+
379+ yield return new ( kSessionNotConnected ) { SessionConnected = false , SessionReconnecting = false , SessionKeepAliveStopped = false } ;
380+ yield return new ( kSessionNotConnected ) { SessionConnected = false , SessionReconnecting = true , SessionKeepAliveStopped = false } ;
381+ yield return new ( kSessionNotConnected ) { SessionConnected = false , SessionReconnecting = false , SessionKeepAliveStopped = true } ;
382+ yield return new ( kSessionNotConnected ) { SessionConnected = false , SessionReconnecting = true , SessionKeepAliveStopped = true } ;
383+
384+ yield return new ( kSessionNotConnected ) { SessionConnected = true , SessionReconnecting = true , SessionKeepAliveStopped = false } ;
385+ yield return new ( kSessionNotConnected ) { SessionConnected = true , SessionReconnecting = true , SessionKeepAliveStopped = true } ;
386+
387+ yield return new ( kSessionNotConnected ) { SessionConnected = true , SessionReconnecting = false , SessionKeepAliveStopped = true } ;
388+ }
389+
390+ [ Theory ]
391+ [ CancelAfter ( Subscription . MinKeepAliveTimerInterval * 10 ) ]
392+ public async Task RespectsStateOfSessionDuringKeepAliveCalls ( KeepAliveTestDataProvider testData , CancellationToken ct )
393+ {
394+ var keepAliveCompleted = new TaskCompletionSource < PublishStateChangedMask > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
395+ void KeepAliveHasTriggered ( Subscription x , PublishStateChangedEventArgs y ) => keepAliveCompleted . TrySetResult ( y . Status ) ;
396+ ISession session = BuildSessionMock (
397+ setup : mock =>
398+ {
399+ mock . Setup ( x => x . Connected ) . Returns ( testData . SessionConnected ) ;
400+ mock . Setup ( x => x . Reconnecting ) . Returns ( testData . SessionReconnecting ) ;
401+ mock . Setup ( x => x . KeepAliveStopped ) . Returns ( testData . SessionKeepAliveStopped ) ;
402+ } ) ;
403+
404+ using var subscription = new Subscription (
405+ NUnitTelemetryContext . Create ( ) ,
406+ new ( ) { PublishingEnabled = true } )
407+ {
408+ Session = session
409+ } ;
410+ subscription . PublishStatusChanged += KeepAliveHasTriggered ;
411+ await subscription . CreateAsync ( ct ) . ConfigureAwait ( false ) ;
412+ await Task . WhenAny ( keepAliveCompleted . Task , Task . Delay ( - 1 , ct ) ) . ConfigureAwait ( false ) ;
413+
414+ Assert . That ( keepAliveCompleted . Task . Result , Is . EqualTo ( testData . ExpectedPublishState ) ) ;
415+ }
353416 }
354417}
0 commit comments