11using ManagedCode . Orleans . Graph . Interfaces ;
22using ManagedCode . Orleans . Graph . Models ;
3- using ManagedCode . Orleans . Graph . Tests . Cluster . Grains ;
43using ManagedCode . Orleans . Graph . Tests . Cluster . Grains . Interfaces ;
54
65namespace ManagedCode . Orleans . Graph . Tests ;
@@ -135,22 +134,94 @@ public void IsTransitionAllowed_UsesSourceIncomingMethodForOutgoingGrainCall()
135134 var grainBId = GrainId . Create ( "grainb" , "source-method" ) ;
136135
137136 var callHistory = new CallHistory ( ) ;
138- callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB2 ) ) ) ;
137+ callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB2 ) , Constants . AnyMethod ) ) ;
139138 callHistory . Push ( new InCall ( null , grainAId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB2 ) ) ) ;
140139 callHistory . Push ( new OutCall ( grainAId , grainBId , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodC2 ) , nameof ( IGrainA . MethodB2 ) ) ) ;
141140 callHistory . Push ( new InCall ( grainAId , grainBId , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodC2 ) ) ) ;
142141
143142 graph . IsTransitionAllowed ( callHistory ) . ShouldBeTrue ( ) ;
144143 }
145144
145+ [ Test ]
146+ public void IsLatestTransitionAllowed_ClientOutgoingWithoutIncoming_DoesNotEnforceClientSideGraph ( )
147+ {
148+ var graph = GrainCallsBuilder . Create ( )
149+ . Build ( ) ;
150+
151+ var callHistory = new CallHistory ( ) ;
152+ callHistory . Push ( new OutCall (
153+ null ,
154+ GrainId . Create ( "graina" , "client-side" ) ,
155+ Constants . ClientCallerId ,
156+ typeof ( IGrainA ) . FullName ! ,
157+ nameof ( IGrainA . MethodB1 ) ,
158+ Constants . AnyMethod ) ) ;
159+
160+ graph . IsLatestTransitionAllowed ( callHistory ) . ShouldBeTrue ( ) ;
161+ }
162+
163+ [ Test ]
164+ public void IsLatestTransitionAllowed_GrainOutgoing_EnforcesLatestTransition ( )
165+ {
166+ var graph = GrainCallsBuilder . Create ( )
167+ . From < IGrainA > ( )
168+ . To < IGrainB > ( )
169+ . MethodByName ( nameof ( IGrainA . MethodB1 ) , nameof ( IGrainB . MethodB1 ) )
170+ . And ( )
171+ . Build ( ) ;
172+
173+ var callHistory = new CallHistory ( ) ;
174+ callHistory . Push ( new InCall (
175+ null ,
176+ GrainId . Create ( "graina" , "latest-valid" ) ,
177+ typeof ( IGrainA ) . FullName ! ,
178+ nameof ( IGrainA . MethodB1 ) ) ) ;
179+ callHistory . Push ( new OutCall (
180+ GrainId . Create ( "graina" , "latest-valid" ) ,
181+ GrainId . Create ( "grainb" , "latest-valid" ) ,
182+ typeof ( IGrainA ) . FullName ! ,
183+ typeof ( IGrainB ) . FullName ! ,
184+ nameof ( IGrainB . MethodB1 ) ,
185+ nameof ( IGrainA . MethodB1 ) ) ) ;
186+
187+ graph . IsLatestTransitionAllowed ( callHistory ) . ShouldBeTrue ( ) ;
188+ }
189+
190+ [ Test ]
191+ public void IsLatestTransitionAllowed_GrainOutgoing_RejectsMissingLatestTransition ( )
192+ {
193+ var graph = GrainCallsBuilder . Create ( )
194+ . From < IGrainA > ( )
195+ . To < IGrainB > ( )
196+ . MethodByName ( nameof ( IGrainA . MethodB1 ) , nameof ( IGrainB . MethodB1 ) )
197+ . And ( )
198+ . Build ( ) ;
199+
200+ var callHistory = new CallHistory ( ) ;
201+ callHistory . Push ( new InCall (
202+ null ,
203+ GrainId . Create ( "graina" , "latest-invalid" ) ,
204+ typeof ( IGrainA ) . FullName ! ,
205+ nameof ( IGrainA . MethodB2 ) ) ) ;
206+ callHistory . Push ( new OutCall (
207+ GrainId . Create ( "graina" , "latest-invalid" ) ,
208+ GrainId . Create ( "grainb" , "latest-invalid" ) ,
209+ typeof ( IGrainA ) . FullName ! ,
210+ typeof ( IGrainB ) . FullName ! ,
211+ nameof ( IGrainB . MethodC2 ) ,
212+ nameof ( IGrainA . MethodB2 ) ) ) ;
213+
214+ graph . IsLatestTransitionAllowed ( callHistory ) . ShouldBeFalse ( ) ;
215+ }
216+
146217 [ Test ]
147218 public void GetObservedGraph_ReturnsVerticesAndClientAndNestedGrainEdges ( )
148219 {
149220 var grainAId = GrainId . Create ( "graina" , "observed" ) ;
150221 var grainBId = GrainId . Create ( "grainb" , "observed" ) ;
151222
152223 var callHistory = new CallHistory ( ) ;
153- callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
224+ callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) , Constants . AnyMethod ) ) ;
154225 callHistory . Push ( new InCall ( null , grainAId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
155226 callHistory . Push ( new OutCall ( grainAId , grainBId , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) , nameof ( IGrainA . MethodB1 ) ) ) ;
156227 callHistory . Push ( new InCall ( grainAId , grainBId , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
@@ -184,7 +255,7 @@ public void GetLatestObservedCall_ReturnsCurrentIncomingPair()
184255 var grainBId = GrainId . Create ( "grainb" , "latest" ) ;
185256
186257 var callHistory = new CallHistory ( ) ;
187- callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
258+ callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) , Constants . AnyMethod ) ) ;
188259 callHistory . Push ( new InCall ( null , grainAId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
189260 callHistory . Push ( new OutCall ( grainAId , grainBId , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) , nameof ( IGrainA . MethodB1 ) ) ) ;
190261 callHistory . Push ( new InCall ( grainAId , grainBId , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
@@ -198,6 +269,28 @@ public void GetLatestObservedCall_ReturnsCurrentIncomingPair()
198269 edge . TargetMethod . ShouldBe ( nameof ( IGrainB . MethodB1 ) ) ;
199270 }
200271
272+ [ Test ]
273+ public void GetObservedGraph_UsesExplicitCallerMethodForNonClientCallWithoutSourceId ( )
274+ {
275+ var callHistory = new CallHistory ( ) ;
276+ callHistory . Push ( new OutCall (
277+ null ,
278+ null ,
279+ typeof ( IGrainA ) . FullName ! ,
280+ typeof ( IGrainB ) . FullName ! ,
281+ nameof ( IGrainB . MethodC2 ) ,
282+ nameof ( IGrainA . MethodB2 ) ) ) ;
283+ callHistory . Push ( new InCall ( null , null , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodC2 ) ) ) ;
284+
285+ var graph = GrainTransitionManager . GetObservedGraph ( callHistory ) ;
286+
287+ graph . Edges . ShouldContain ( edge =>
288+ edge . Source == typeof ( IGrainA ) . FullName &&
289+ edge . Target == typeof ( IGrainB ) . FullName &&
290+ edge . SourceMethod == nameof ( IGrainA . MethodB2 ) &&
291+ edge . TargetMethod == nameof ( IGrainB . MethodC2 ) ) ;
292+ }
293+
201294 [ Test ]
202295 public void GetObservedGraph_RejectsBaseGrainIdentity ( )
203296 {
@@ -209,7 +302,8 @@ public void GetObservedGraph_RejectsBaseGrainIdentity()
209302 grainAId ,
210303 Constants . ClientCallerId ,
211304 typeof ( Grain ) . FullName ! ,
212- nameof ( IGrainA . MethodB1 ) ) ) ;
305+ nameof ( IGrainA . MethodB1 ) ,
306+ Constants . AnyMethod ) ) ;
213307 callHistory . Push ( new InCall (
214308 null ,
215309 grainAId ,
@@ -431,12 +525,85 @@ public void DetectDeadlocks_IgnoresReentrantTransitions()
431525 var grainId = GrainId . Create ( "test" , nameof ( IGrainA ) ) ;
432526
433527 var callHistory = new CallHistory ( ) ;
434- callHistory . Push ( new OutCall ( grainId , grainId , typeof ( IGrainA ) . FullName ! , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodA1 ) ) ) ;
528+ callHistory . Push ( new OutCall (
529+ grainId ,
530+ grainId ,
531+ typeof ( IGrainA ) . FullName ! ,
532+ typeof ( IGrainA ) . FullName ! ,
533+ nameof ( IGrainA . MethodA1 ) ,
534+ nameof ( IGrainA . MethodA1 ) ) ) ;
435535 callHistory . Push ( new InCall ( grainId , grainId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodA1 ) ) ) ;
436536
437537 graph . DetectDeadlocks ( callHistory ) . ShouldBeFalse ( ) ;
438538 }
439539
540+ [ Test ]
541+ public void DetectLatestDeadlock_DetectsCycleClosedByLatestOutgoingCall ( )
542+ {
543+ var graph = GrainCallsBuilder . Create ( )
544+ . AllowAll ( )
545+ . Build ( ) ;
546+
547+ var grainAId = GrainId . Create ( "graina" , "latest-deadlock" ) ;
548+ var grainBId = GrainId . Create ( "grainb" , "latest-deadlock" ) ;
549+ var grainCId = GrainId . Create ( "grainc" , "latest-deadlock" ) ;
550+
551+ var callHistory = new CallHistory ( ) ;
552+ callHistory . Push ( new OutCall (
553+ grainAId ,
554+ grainBId ,
555+ typeof ( IGrainA ) . FullName ! ,
556+ typeof ( IGrainB ) . FullName ! ,
557+ nameof ( IGrainB . MethodB1 ) ,
558+ nameof ( IGrainA . MethodB1 ) ) ) ;
559+ callHistory . Push ( new OutCall (
560+ grainBId ,
561+ grainCId ,
562+ typeof ( IGrainB ) . FullName ! ,
563+ typeof ( IGrainC ) . FullName ! ,
564+ nameof ( IGrainC . MethodC1 ) ,
565+ nameof ( IGrainB . MethodB1 ) ) ) ;
566+ callHistory . Push ( new OutCall (
567+ grainCId ,
568+ grainAId ,
569+ typeof ( IGrainC ) . FullName ! ,
570+ typeof ( IGrainA ) . FullName ! ,
571+ nameof ( IGrainA . MethodA1 ) ,
572+ nameof ( IGrainC . MethodC1 ) ) ) ;
573+
574+ graph . DetectLatestDeadlock ( callHistory ) . ShouldBeTrue ( ) ;
575+ }
576+
577+ [ Test ]
578+ public void DetectLatestDeadlock_ReturnsFalseWhenLatestOutgoingDoesNotCloseCycle ( )
579+ {
580+ var graph = GrainCallsBuilder . Create ( )
581+ . AllowAll ( )
582+ . Build ( ) ;
583+
584+ var grainAId = GrainId . Create ( "graina" , "latest-no-deadlock" ) ;
585+ var grainBId = GrainId . Create ( "grainb" , "latest-no-deadlock" ) ;
586+ var grainCId = GrainId . Create ( "grainc" , "latest-no-deadlock" ) ;
587+
588+ var callHistory = new CallHistory ( ) ;
589+ callHistory . Push ( new OutCall (
590+ grainAId ,
591+ grainBId ,
592+ typeof ( IGrainA ) . FullName ! ,
593+ typeof ( IGrainB ) . FullName ! ,
594+ nameof ( IGrainB . MethodB1 ) ,
595+ nameof ( IGrainA . MethodB1 ) ) ) ;
596+ callHistory . Push ( new OutCall (
597+ grainBId ,
598+ grainCId ,
599+ typeof ( IGrainB ) . FullName ! ,
600+ typeof ( IGrainC ) . FullName ! ,
601+ nameof ( IGrainC . MethodC1 ) ,
602+ nameof ( IGrainB . MethodB1 ) ) ) ;
603+
604+ graph . DetectLatestDeadlock ( callHistory ) . ShouldBeFalse ( ) ;
605+ }
606+
440607 [ Test ]
441608 public void GeneratePolicyMermaidDiagram_ProducesEdges ( )
442609 {
@@ -494,9 +661,15 @@ public void GenerateLiveMermaidDiagram_HighlightsActiveEdges()
494661 var grainBId = GrainId . Create ( "grainb" , "live-policy" ) ;
495662
496663 var callHistory = new CallHistory ( ) ;
497- callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
664+ callHistory . Push ( new OutCall ( null , grainAId , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) , Constants . AnyMethod ) ) ;
498665 callHistory . Push ( new InCall ( null , grainAId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
499- callHistory . Push ( new OutCall ( grainAId , grainBId , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
666+ callHistory . Push ( new OutCall (
667+ grainAId ,
668+ grainBId ,
669+ typeof ( IGrainA ) . FullName ! ,
670+ typeof ( IGrainB ) . FullName ! ,
671+ nameof ( IGrainB . MethodB1 ) ,
672+ nameof ( IGrainA . MethodB1 ) ) ) ;
500673 callHistory . Push ( new InCall ( grainAId , grainBId , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
501674
502675 var diagram = graph . GenerateLiveMermaidDiagram ( callHistory ) ;
@@ -514,7 +687,7 @@ public void GenerateLiveMermaidDiagram_RendersObservedCallsWithoutPolicyEdges()
514687 . Build ( ) ;
515688
516689 var callHistory = new CallHistory ( ) ;
517- callHistory . Push ( new OutCall ( null , null , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
690+ callHistory . Push ( new OutCall ( null , null , Constants . ClientCallerId , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) , Constants . AnyMethod ) ) ;
518691 callHistory . Push ( new InCall ( null , null , typeof ( IGrainA ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
519692
520693 var diagram = graph . GenerateLiveMermaidDiagram ( callHistory ) ;
@@ -595,9 +768,9 @@ public void GenerateLiveMermaidDiagram_AppendsUsageCounts()
595768 . Build ( ) ;
596769
597770 var callHistory = new CallHistory ( ) ;
598- callHistory . Push ( new OutCall ( null , null , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
771+ callHistory . Push ( new OutCall ( null , null , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) , nameof ( IGrainA . MethodB1 ) ) ) ;
599772 callHistory . Push ( new InCall ( null , null , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
600- callHistory . Push ( new OutCall ( null , null , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainA . MethodB1 ) ) ) ;
773+ callHistory . Push ( new OutCall ( null , null , typeof ( IGrainA ) . FullName ! , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) , nameof ( IGrainA . MethodB1 ) ) ) ;
601774 callHistory . Push ( new InCall ( null , null , typeof ( IGrainB ) . FullName ! , nameof ( IGrainB . MethodB1 ) ) ) ;
602775
603776 var diagram = graph . GenerateLiveMermaidDiagram ( callHistory ) ;
0 commit comments