Skip to content

Commit c78e5d9

Browse files
committed
more tests
1 parent 0fa38f2 commit c78e5d9

12 files changed

Lines changed: 298 additions & 12 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Update guidelines:
3434
- **Runtime Graph Telemetry**: For live graph features, have filters report observed calls to stateless worker aggregators that periodically flush to an in-memory grain; do not rely on per-request `CallHistory` alone for a global runtime graph.
3535
- **Telemetry Filtering**: Do not track Orleans.Graph internal telemetry calls by default; expose a configuration switch to include them, and test both filtered and full-tracking modes.
3636
- **Testing Scope**: Exercise new behavior through the Orleans-hosted integration tests in `ManagedCode.Orleans.Graph.Tests`, covering both positive and negative paths to mirror real cluster flows.
37+
- **Cluster-to-Cluster Tests**: For grain-only runtime graph scenarios, add coverage that starts from a silo-side `IGrainFactory` instead of `IClusterClient`, so client-origin edges cannot hide cluster-to-cluster behavior.
3738
- **Test Framework**: Use TUnit for tests and Shouldly for assertions; do not introduce FluentAssertions, so the test style stays aligned with the newer ManagedCode Orleans projects.
3839
- **Migration Releases**: For major framework/package migrations, complete three strict code-review-and-fix iterations before README polish, feature additions, commit, push, and CI verification, so release branches are hardened before publication.
3940
- **Code Style**: Use enums or constants over magic literals, keep documentation and comments in English, and avoid template placeholders—name files and types for their real domain roles.

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/GrainA.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using ManagedCode.Orleans.Graph.Interfaces;
12
using ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
23

34
namespace ManagedCode.Orleans.Graph.Tests.Cluster.Grains;
@@ -26,4 +27,35 @@ public async Task<int> MethodC1(int input)
2627
return await GrainFactory.GetGrain<IGrainC>(this.GetPrimaryKeyString())
2728
.MethodC1(input);
2829
}
30+
31+
public async Task<int> MethodComplexFlow(int input)
32+
{
33+
return await RunBranchingFlowAsync(input);
34+
}
35+
36+
public async Task<int> MethodGrainOnlyComplexFlow(int input)
37+
{
38+
await ClearRuntimeGraphTelemetryAsync();
39+
40+
return await RunBranchingFlowAsync(input);
41+
}
42+
43+
private async Task<int> RunBranchingFlowAsync(int input)
44+
{
45+
var grainKey = this.GetPrimaryKeyString();
46+
var fromB = await GrainFactory.GetGrain<IGrainB>(grainKey)
47+
.MethodB1(input);
48+
var fromC = await GrainFactory.GetGrain<IGrainC>(grainKey)
49+
.MethodBranchingFlow(fromB);
50+
51+
return await GrainFactory.GetGrain<IGrainD>(grainKey)
52+
.MethodE2(fromC);
53+
}
54+
55+
private async Task ClearRuntimeGraphTelemetryAsync()
56+
{
57+
await Task.Delay(100);
58+
await GrainFactory.GetGrain<IOrleansGraphTelemetryWorker>(Constants.LiveGraphTelemetryGrainKey).FlushAsync();
59+
await GrainFactory.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey).ClearAsync();
60+
}
2961
}

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/GrainC.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,14 @@ public async Task<int> MethodA2(int input)
1414
return await GrainFactory.GetGrain<IGrainA>(this.GetPrimaryKeyString())
1515
.MethodA1(input);
1616
}
17+
18+
public async Task<int> MethodBranchingFlow(int input)
19+
{
20+
var grainKey = this.GetPrimaryKeyString();
21+
var fromB = await GrainFactory.GetGrain<IGrainB>(grainKey)
22+
.MethodB1(input);
23+
24+
return await GrainFactory.GetGrain<IGrainD>(grainKey)
25+
.MethodE2(fromB);
26+
}
1727
}

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/Interfaces/IGrainA.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ public interface IGrainA : IGrainWithStringKey
88
Task<int> MethodB2(int input);
99

1010
Task<int> MethodC1(int input);
11+
12+
Task<int> MethodComplexFlow(int input);
13+
14+
Task<int> MethodGrainOnlyComplexFlow(int input);
1115
}

ManagedCode.Orleans.Graph.Tests/Cluster/Grains/Interfaces/IGrainC.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public interface IGrainC : IGrainWithStringKey
44
{
55
Task<int> MethodC1(int input);
66
Task<int> MethodA2(int input);
7+
Task<int> MethodBranchingFlow(int input);
78
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using ManagedCode.Orleans.Graph.Models;
2+
3+
namespace ManagedCode.Orleans.Graph.Tests;
4+
5+
public class GraphCallFilterConfigTests
6+
{
7+
[Test]
8+
public void Defaults_DoNotTrackOrleansOrOrleansGraphInternalCalls()
9+
{
10+
var config = new GraphCallFilterConfig();
11+
12+
config.TrackOrleansCalls.ShouldBeFalse();
13+
config.TrackOrleansGraphInternalCalls.ShouldBeFalse();
14+
config.LiveGraphFlushPeriod.ShouldBeGreaterThan(TimeSpan.Zero);
15+
}
16+
17+
[Test]
18+
public void TrackOrleansGraphInternalCalls_CanBeEnabledExplicitly()
19+
{
20+
var config = new GraphCallFilterConfig
21+
{
22+
TrackOrleansGraphInternalCalls = true
23+
};
24+
25+
config.TrackOrleansGraphInternalCalls.ShouldBeTrue();
26+
}
27+
}

ManagedCode.Orleans.Graph.Tests/RuntimeGraphTests.cs

Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ManagedCode.Orleans.Graph.Models;
33
using ManagedCode.Orleans.Graph.Tests.Cluster.Grains.Interfaces;
44
using ManagedCode.Orleans.Graph.Tests.RuntimeGraphCluster;
5+
using Microsoft.Extensions.DependencyInjection;
56

67
namespace ManagedCode.Orleans.Graph.Tests;
78

@@ -121,7 +122,7 @@ await WaitForEdgesAsync(_fixture.Cluster.Client, edges =>
121122
edge.TargetMethod == nameof(IGrainB.MethodB1)));
122123

123124
var telemetry = _fixture.Cluster.Client.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
124-
var diagram = await telemetry.GenerateMermaidDiagramAsync();
125+
var diagram = await telemetry.GenerateLiveMermaidDiagramAsync();
125126

126127
diagram.ShouldContain("graph LR");
127128
diagram.ShouldContain("ORLEANS_GRAIN_CLIENT");
@@ -132,6 +133,83 @@ await WaitForEdgesAsync(_fixture.Cluster.Client, edges =>
132133
diagram.ShouldContain("hits: 1");
133134
}
134135

136+
[Test]
137+
public async Task AllowAll_BuildsExactComplexRuntimeGraphAsync()
138+
{
139+
await ResetTelemetryAsync(_fixture.Cluster.Client);
140+
141+
var result = await _fixture.Cluster.Client
142+
.GetGrain<IGrainA>("complex-flow")
143+
.MethodComplexFlow(1);
144+
145+
result.ShouldBe(5);
146+
147+
var expectedEdges = BuildExpectedComplexFlowEdges(nameof(IGrainA.MethodComplexFlow), includeClient: true);
148+
149+
var edges = await WaitForEdgesAsync(_fixture.Cluster.Client, edges =>
150+
edges.Count == expectedEdges.Length && ContainsExpectedEdges(edges, expectedEdges));
151+
152+
AssertExpectedEdges(edges, expectedEdges);
153+
edges.Any(IsTelemetryEdge).ShouldBeFalse();
154+
155+
var telemetry = _fixture.Cluster.Client.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
156+
var diagram = await telemetry.GenerateLiveMermaidDiagramAsync();
157+
158+
diagram.ShouldBe(BuildExpectedComplexLiveMermaidDiagram(nameof(IGrainA.MethodComplexFlow), includeClient: true));
159+
}
160+
161+
[Test]
162+
public async Task AllowAll_BuildsExactGrainOnlyRuntimeGraphWithoutClientAsync()
163+
{
164+
await ResetTelemetryAsync(_fixture.Cluster.Client);
165+
166+
var result = await _fixture.Cluster.Client
167+
.GetGrain<IGrainA>("grain-only-complex-flow")
168+
.MethodGrainOnlyComplexFlow(1);
169+
170+
result.ShouldBe(5);
171+
172+
var expectedEdges = BuildExpectedComplexFlowEdges(nameof(IGrainA.MethodGrainOnlyComplexFlow), includeClient: false);
173+
174+
var edges = await WaitForEdgesAsync(_fixture.Cluster.Client, edges =>
175+
edges.Count == expectedEdges.Length && ContainsExpectedEdges(edges, expectedEdges));
176+
177+
AssertExpectedEdges(edges, expectedEdges);
178+
edges.Any(edge => edge.Source == Constants.ClientCallerId || edge.Target == Constants.ClientCallerId).ShouldBeFalse();
179+
edges.Any(IsTelemetryEdge).ShouldBeFalse();
180+
181+
var telemetry = _fixture.Cluster.Client.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
182+
var diagram = await telemetry.GenerateLiveMermaidDiagramAsync();
183+
184+
diagram.ShouldBe(BuildExpectedComplexLiveMermaidDiagram(nameof(IGrainA.MethodGrainOnlyComplexFlow), includeClient: false));
185+
}
186+
187+
[Test]
188+
public async Task AllowAll_BuildsExactGrainFactoryRuntimeGraphWithoutClientAsync()
189+
{
190+
var grainFactory = GetPrimarySiloGrainFactory();
191+
await ResetTelemetryAsync(grainFactory);
192+
193+
var result = await grainFactory
194+
.GetGrain<IGrainA>("grain-factory-complex-flow")
195+
.MethodGrainOnlyComplexFlow(1);
196+
197+
result.ShouldBe(5);
198+
199+
var expectedEdges = BuildExpectedComplexFlowEdges(nameof(IGrainA.MethodGrainOnlyComplexFlow), includeClient: false);
200+
var edges = await WaitForEdgesAsync(grainFactory, edges =>
201+
edges.Count == expectedEdges.Length && ContainsExpectedEdges(edges, expectedEdges));
202+
203+
AssertExpectedEdges(edges, expectedEdges);
204+
edges.Any(edge => edge.Source == Constants.ClientCallerId || edge.Target == Constants.ClientCallerId).ShouldBeFalse();
205+
edges.Any(IsTelemetryEdge).ShouldBeFalse();
206+
207+
var telemetry = grainFactory.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
208+
var diagram = await telemetry.GenerateLiveMermaidDiagramAsync();
209+
210+
diagram.ShouldBe(BuildExpectedComplexLiveMermaidDiagram(nameof(IGrainA.MethodGrainOnlyComplexFlow), includeClient: false));
211+
}
212+
135213
[Test]
136214
public async Task Telemetry_DoesNotTrackOrleansGraphInternalCallsByDefaultAsync()
137215
{
@@ -149,18 +227,24 @@ await _fixture.Cluster.Client
149227
edges.Any(IsTelemetryEdge).ShouldBeFalse();
150228
}
151229

152-
private static async Task ResetTelemetryAsync(IClusterClient client)
230+
private IGrainFactory GetPrimarySiloGrainFactory()
153231
{
154-
await client.GetGrain<IOrleansGraphTelemetryWorker>(Constants.LiveGraphTelemetryGrainKey).FlushAsync();
232+
var serviceProvider = _fixture.Cluster.GetSiloServiceProvider(_fixture.Cluster.Primary.SiloAddress);
233+
return serviceProvider.GetRequiredService<IGrainFactory>();
234+
}
235+
236+
private static async Task ResetTelemetryAsync(IGrainFactory grainFactory)
237+
{
238+
await grainFactory.GetGrain<IOrleansGraphTelemetryWorker>(Constants.LiveGraphTelemetryGrainKey).FlushAsync();
155239
await Task.Delay(200);
156-
await client.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey).ClearAsync();
240+
await grainFactory.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey).ClearAsync();
157241
}
158242

159243
private static async Task<IReadOnlyCollection<ObservedGrainCallEdge>> WaitForEdgesAsync(
160-
IClusterClient client,
244+
IGrainFactory grainFactory,
161245
Func<IReadOnlyCollection<ObservedGrainCallEdge>, bool> predicate)
162246
{
163-
var telemetry = client.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
247+
var telemetry = grainFactory.GetGrain<IOrleansGraphTelemetryGrain>(Constants.LiveGraphTelemetryGrainKey);
164248

165249
for (var attempt = 0; attempt < 50; attempt++)
166250
{
@@ -176,6 +260,124 @@ private static async Task<IReadOnlyCollection<ObservedGrainCallEdge>> WaitForEdg
176260
return await telemetry.GetEdgesAsync();
177261
}
178262

263+
private static void AssertExpectedEdges(
264+
IReadOnlyCollection<ObservedGrainCallEdge> edges,
265+
IReadOnlyCollection<ExpectedObservedEdge> expectedEdges)
266+
{
267+
edges.Count.ShouldBe(expectedEdges.Count);
268+
269+
foreach (var expected in expectedEdges)
270+
{
271+
edges.ShouldContain(edge =>
272+
edge.Source == expected.Source &&
273+
edge.Target == expected.Target &&
274+
edge.SourceMethod == expected.SourceMethod &&
275+
edge.TargetMethod == expected.TargetMethod &&
276+
edge.Count == expected.Count);
277+
}
278+
}
279+
280+
private static bool ContainsExpectedEdges(
281+
IReadOnlyCollection<ObservedGrainCallEdge> edges,
282+
IReadOnlyCollection<ExpectedObservedEdge> expectedEdges)
283+
{
284+
return expectedEdges.All(expected =>
285+
edges.Any(edge =>
286+
edge.Source == expected.Source &&
287+
edge.Target == expected.Target &&
288+
edge.SourceMethod == expected.SourceMethod &&
289+
edge.TargetMethod == expected.TargetMethod &&
290+
edge.Count == expected.Count));
291+
}
292+
293+
private static ExpectedObservedEdge[] BuildExpectedComplexFlowEdges(string rootMethod, bool includeClient)
294+
{
295+
var edges = new List<ExpectedObservedEdge>
296+
{
297+
new(
298+
typeof(IGrainA).FullName!,
299+
typeof(IGrainB).FullName!,
300+
rootMethod,
301+
nameof(IGrainB.MethodB1),
302+
1),
303+
new(
304+
typeof(IGrainA).FullName!,
305+
typeof(IGrainC).FullName!,
306+
rootMethod,
307+
nameof(IGrainC.MethodBranchingFlow),
308+
1),
309+
new(
310+
typeof(IGrainC).FullName!,
311+
typeof(IGrainB).FullName!,
312+
nameof(IGrainC.MethodBranchingFlow),
313+
nameof(IGrainB.MethodB1),
314+
1),
315+
new(
316+
typeof(IGrainC).FullName!,
317+
typeof(IGrainD).FullName!,
318+
nameof(IGrainC.MethodBranchingFlow),
319+
nameof(IGrainD.MethodE2),
320+
1),
321+
new(
322+
typeof(IGrainA).FullName!,
323+
typeof(IGrainD).FullName!,
324+
rootMethod,
325+
nameof(IGrainD.MethodE2),
326+
1),
327+
new(
328+
typeof(IGrainD).FullName!,
329+
typeof(IGrainE).FullName!,
330+
nameof(IGrainD.MethodE2),
331+
nameof(IGrainE.MethodE1),
332+
2)
333+
};
334+
335+
if (includeClient)
336+
{
337+
edges.Insert(0, new ExpectedObservedEdge(
338+
Constants.ClientCallerId,
339+
typeof(IGrainA).FullName!,
340+
Constants.AnyMethod,
341+
rootMethod,
342+
1));
343+
}
344+
345+
return edges.ToArray();
346+
}
347+
348+
private static string BuildExpectedComplexLiveMermaidDiagram(string rootMethod, bool includeClient)
349+
{
350+
var grainA = MermaidNode<IGrainA>();
351+
var grainB = MermaidNode<IGrainB>();
352+
var grainC = MermaidNode<IGrainC>();
353+
var grainD = MermaidNode<IGrainD>();
354+
var grainE = MermaidNode<IGrainE>();
355+
356+
var lines = new List<string>
357+
{
358+
"graph LR",
359+
$" {grainA.Id}[\"{grainA.DisplayName}\"] ==>|{rootMethod}->{nameof(IGrainB.MethodB1)}<br/>hits: 1| {grainB.Id}[\"{grainB.DisplayName}\"]",
360+
$" {grainA.Id}[\"{grainA.DisplayName}\"] ==>|{rootMethod}->{nameof(IGrainC.MethodBranchingFlow)}<br/>hits: 1| {grainC.Id}[\"{grainC.DisplayName}\"]",
361+
$" {grainA.Id}[\"{grainA.DisplayName}\"] ==>|{rootMethod}->{nameof(IGrainD.MethodE2)}<br/>hits: 1| {grainD.Id}[\"{grainD.DisplayName}\"]",
362+
$" {grainC.Id}[\"{grainC.DisplayName}\"] ==>|{nameof(IGrainC.MethodBranchingFlow)}->{nameof(IGrainB.MethodB1)}<br/>hits: 1| {grainB.Id}[\"{grainB.DisplayName}\"]",
363+
$" {grainC.Id}[\"{grainC.DisplayName}\"] ==>|{nameof(IGrainC.MethodBranchingFlow)}->{nameof(IGrainD.MethodE2)}<br/>hits: 1| {grainD.Id}[\"{grainD.DisplayName}\"]",
364+
$" {grainD.Id}[\"{grainD.DisplayName}\"] ==>|{nameof(IGrainD.MethodE2)}->{nameof(IGrainE.MethodE1)}<br/>hits: 2| {grainE.Id}[\"{grainE.DisplayName}\"]"
365+
};
366+
367+
if (includeClient)
368+
{
369+
lines.Add($" {Constants.ClientCallerId}[\"{Constants.ClientCallerId}\"] ==>|{rootMethod}<br/>hits: 1| {grainA.Id}[\"{grainA.DisplayName}\"]");
370+
}
371+
372+
return string.Join(Environment.NewLine, lines) + Environment.NewLine;
373+
}
374+
375+
private static (string Id, string DisplayName) MermaidNode<TGrain>()
376+
where TGrain : IGrain
377+
{
378+
return (typeof(TGrain).FullName!.Replace('.', '_'), typeof(TGrain).Name);
379+
}
380+
179381
private static bool IsTelemetryEdge(ObservedGrainCallEdge edge)
180382
{
181383
return IsTelemetryEndpoint(edge.Source) || IsTelemetryEndpoint(edge.Target);
@@ -186,4 +388,11 @@ private static bool IsTelemetryEndpoint(string endpoint)
186388
return endpoint.Contains(nameof(IOrleansGraphTelemetryWorker), StringComparison.Ordinal) ||
187389
endpoint.Contains(nameof(IOrleansGraphTelemetryGrain), StringComparison.Ordinal);
188390
}
391+
392+
private readonly record struct ExpectedObservedEdge(
393+
string Source,
394+
string Target,
395+
string SourceMethod,
396+
string TargetMethod,
397+
long Count);
189398
}

ManagedCode.Orleans.Graph/Extensions/RequestContextHelper.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public static bool IsTelemetrySuppressed()
7171

7272
public static async Task RunWithTelemetrySuppressedAsync(Func<Task> action)
7373
{
74+
ArgumentNullException.ThrowIfNull(action);
75+
7476
var previous = RequestContext.Get(Constants.TelemetrySuppressionContextKey);
7577
RequestContext.Set(Constants.TelemetrySuppressionContextKey, true);
7678

ManagedCode.Orleans.Graph/Interfaces/IOrleansGraphTelemetryGrain.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public interface IOrleansGraphTelemetryGrain : IGrainWithStringKey
88

99
Task<IReadOnlyCollection<ObservedGrainCallEdge>> GetEdgesAsync();
1010

11-
Task<string> GenerateMermaidDiagramAsync();
11+
Task<string> GenerateLiveMermaidDiagramAsync();
1212

1313
Task ClearAsync();
1414
}

0 commit comments

Comments
 (0)