From 066a1654e2b135b6c0c43b3e0c5b680e3948e7c4 Mon Sep 17 00:00:00 2001 From: JerrettDavis Date: Sun, 31 May 2026 11:31:01 -0500 Subject: [PATCH] test: harden routing generator coverage --- .../ProductCatalogStampedeProtectionDemo.cs | 31 +++++-- .../RecipientListGeneratorTests.cs | 90 +++++++++++++++++++ .../RoutingSlipGeneratorTests.cs | 79 ++++++++++++++++ 3 files changed, 192 insertions(+), 8 deletions(-) diff --git a/src/PatternKit.Examples/CacheStampedeProtectionDemo/ProductCatalogStampedeProtectionDemo.cs b/src/PatternKit.Examples/CacheStampedeProtectionDemo/ProductCatalogStampedeProtectionDemo.cs index aa1bdd25..9daab22e 100644 --- a/src/PatternKit.Examples/CacheStampedeProtectionDemo/ProductCatalogStampedeProtectionDemo.cs +++ b/src/PatternKit.Examples/CacheStampedeProtectionDemo/ProductCatalogStampedeProtectionDemo.cs @@ -59,28 +59,43 @@ public sealed class ProductCatalogStampedeProtectionDemoRunner(ProductCatalogSta { public async ValueTask> RunGeneratedAsync(ProductAvailabilityRequest request) { - var first = service.GetAvailabilityAsync(request).AsTask(); - var second = service.GetAvailabilityAsync(request).AsTask(); - return await Task.WhenAll(first, second).ConfigureAwait(false); + return await RunConcurrentLoadsAsync(service, request).ConfigureAwait(false); } public static async ValueTask> RunFluentAsync(ProductAvailabilityRequest request) { var origin = new ProductCatalogOrigin(); var service = new ProductCatalogStampedeProtectionService(ProductCatalogStampedeProtectionPolicies.CreateFluent(), origin); - var first = service.GetAvailabilityAsync(request).AsTask(); - var second = service.GetAvailabilityAsync(request).AsTask(); - return await Task.WhenAll(first, second).ConfigureAwait(false); + return await RunConcurrentLoadsAsync(service, request).ConfigureAwait(false); } public static async ValueTask> RunGeneratedStaticAsync(ProductAvailabilityRequest request) { var origin = new ProductCatalogOrigin(); var service = new ProductCatalogStampedeProtectionService(GeneratedProductCatalogStampedeProtectionPolicy.CreateGenerated(), origin); - var first = service.GetAvailabilityAsync(request).AsTask(); - var second = service.GetAvailabilityAsync(request).AsTask(); + return await RunConcurrentLoadsAsync(service, request).ConfigureAwait(false); + } + + private static async ValueTask> RunConcurrentLoadsAsync( + ProductCatalogStampedeProtectionService service, + ProductAvailabilityRequest request) + { + var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var first = WaitThenLoadAsync(start.Task, service, request); + var second = WaitThenLoadAsync(start.Task, service, request); + + start.SetResult(); return await Task.WhenAll(first, second).ConfigureAwait(false); } + + private static async Task WaitThenLoadAsync( + Task start, + ProductCatalogStampedeProtectionService service, + ProductAvailabilityRequest request) + { + await start.ConfigureAwait(false); + return await service.GetAvailabilityAsync(request).ConfigureAwait(false); + } } public static class ProductCatalogStampedeProtectionServiceCollectionExtensions diff --git a/test/PatternKit.Generators.Tests/RecipientListGeneratorTests.cs b/test/PatternKit.Generators.Tests/RecipientListGeneratorTests.cs index 815de57c..d69fe25b 100644 --- a/test/PatternKit.Generators.Tests/RecipientListGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/RecipientListGeneratorTests.cs @@ -101,6 +101,56 @@ private static ValueTask PriorityAudit(Message message, MessageContext co ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); } + [Scenario("Generates recipient-list factories for global struct with sync and async recipients")] + [Fact] + public void GeneratesRecipientListFactoriesForGlobalStructWithSyncAndAsyncRecipients() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + public sealed record Order(string Channel); + + [GenerateRecipientList(typeof(Order), FactoryName = "BuildSync", AsyncFactoryName = "BuildAsync")] + public partial struct OrderRecipients + { + private static bool IsRetail(Message message, MessageContext context) + => message.Payload.Channel == "retail"; + + private static ValueTask IsPriority(Message message, MessageContext context, CancellationToken cancellationToken) + => ValueTask.FromResult(message.Payload.Channel == "priority"); + + [RecipientListRecipient("retail\"audit", 10, nameof(IsRetail))] + private static void RetailAudit(Message message, MessageContext context) { } + + [RecipientListRecipient("priority-audit", 20, nameof(IsPriority))] + private static ValueTask PriorityAudit(Message message, MessageContext context, CancellationToken cancellationToken) + => ValueTask.CompletedTask; + } + """; + + var comp = CreateCompilation(source, nameof(GeneratesRecipientListFactoriesForGlobalStructWithSyncAndAsyncRecipients)); + var gen = new RecipientListGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated); + + ScenarioExpect.All(run.Results, result => ScenarioExpect.Empty(result.Diagnostics)); + + var generated = ScenarioExpect.Single(run.Results.SelectMany(result => result.GeneratedSources)); + var text = generated.SourceText.ToString(); + ScenarioExpect.Equal("OrderRecipients.RecipientList.g.cs", generated.HintName); + ScenarioExpect.DoesNotContain("namespace ", text); + ScenarioExpect.Contains("partial struct OrderRecipients", text); + ScenarioExpect.Contains("BuildSync()", text); + ScenarioExpect.Contains("BuildAsync()", text); + ScenarioExpect.Contains(".When(\"retail\\\"audit\", IsRetail).Then(RetailAudit)", text); + ScenarioExpect.Contains(".When(\"priority-audit\", IsPriority).Then(PriorityAudit)", text); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + [Scenario("Reports diagnostic for non-partial recipient list")] [Fact] public void ReportsDiagnosticForNonPartialRecipientList() @@ -184,6 +234,46 @@ public static partial class OrderRecipients ScenarioExpect.Equal("PKRL003", diagnostic.Id); } + [Scenario("Reports diagnostics for invalid recipient-list recipient shapes")] + [Fact] + public void ReportsDiagnosticsForInvalidRecipientListRecipientShapes() + { + var source = """ + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Order(string Channel); + + [GenerateRecipientList(typeof(Order))] + public static partial class OrderRecipients + { + private static bool IsRetail(Message message, MessageContext context) => true; + private static int InvalidPredicate(Message message, MessageContext context) => 1; + + [RecipientListRecipient(" ", 10, nameof(IsRetail))] + private static void BlankName(Message message, MessageContext context) { } + + [RecipientListRecipient("missing-predicate", 20, "Missing")] + private static void MissingPredicate(Message message, MessageContext context) { } + + [RecipientListRecipient("invalid-predicate", 30, nameof(InvalidPredicate))] + private static void InvalidPredicateRecipient(Message message, MessageContext context) { } + + [RecipientListRecipient("instance", 40, nameof(IsRetail))] + private void Instance(Message message, MessageContext context) { } + } + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticsForInvalidRecipientListRecipientShapes)); + var gen = new RecipientListGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostics = run.Results.SelectMany(result => result.Diagnostics).ToArray(); + ScenarioExpect.Equal(4, diagnostics.Count(diagnostic => diagnostic.Id == "PKRL003")); + } + [Scenario("Reports diagnostic for duplicate recipient name or order")] [Fact] public void ReportsDiagnosticForDuplicateRecipientNameOrOrder() diff --git a/test/PatternKit.Generators.Tests/RoutingSlipGeneratorTests.cs b/test/PatternKit.Generators.Tests/RoutingSlipGeneratorTests.cs index d1cb667b..1c5080a6 100644 --- a/test/PatternKit.Generators.Tests/RoutingSlipGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/RoutingSlipGeneratorTests.cs @@ -100,6 +100,51 @@ public static async Task Run() ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); } + [Scenario("Generates routing slip factories for global struct with sync and async steps")] + [Fact] + public void GeneratesRoutingSlipFactoriesForGlobalStructWithSyncAndAsyncSteps() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + public sealed record Order(string Status); + + [GenerateRoutingSlip(typeof(Order), FactoryName = "BuildSync", AsyncFactoryName = "BuildAsync")] + public partial struct OrderSlip + { + [RoutingSlipStep("validate", 10)] + private static Message Validate(Message message, MessageContext context) + => message; + + [RoutingSlipStep("ship\"express", 20)] + private static ValueTask> ShipAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => ValueTask.FromResult(message); + } + """; + + var comp = CreateCompilation(source, nameof(GeneratesRoutingSlipFactoriesForGlobalStructWithSyncAndAsyncSteps)); + var gen = new RoutingSlipGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated); + + ScenarioExpect.All(run.Results, result => ScenarioExpect.Empty(result.Diagnostics)); + + var generated = ScenarioExpect.Single(run.Results.SelectMany(result => result.GeneratedSources)); + var text = generated.SourceText.ToString(); + ScenarioExpect.Equal("OrderSlip.RoutingSlip.g.cs", generated.HintName); + ScenarioExpect.DoesNotContain("namespace ", text); + ScenarioExpect.Contains("partial struct OrderSlip", text); + ScenarioExpect.Contains("BuildSync()", text); + ScenarioExpect.Contains("BuildAsync()", text); + ScenarioExpect.Contains(".Step(\"validate\", Validate)", text); + ScenarioExpect.Contains(".Step(\"ship\\\"express\", ShipAsync)", text); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + [Scenario("ReportsDiagnosticForNonPartialSlip")] [Fact] public void ReportsDiagnosticForNonPartialSlip() @@ -179,6 +224,40 @@ public static partial class OrderSlip ScenarioExpect.Equal("PKRS003", diagnostic.Id); } + [Scenario("Reports diagnostics for invalid routing slip step shapes")] + [Fact] + public void ReportsDiagnosticsForInvalidRoutingSlipStepShapes() + { + var source = """ + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Order(string Status); + + [GenerateRoutingSlip(typeof(Order))] + public partial class OrderSlip + { + [RoutingSlipStep(" ", 10)] + private static Message BlankName(Message message, MessageContext context) => message; + + [RoutingSlipStep("missing-context", 20)] + private static Message MissingContext(Message message) => message; + + [RoutingSlipStep("instance", 30)] + private Message Instance(Message message, MessageContext context) => message; + } + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticsForInvalidRoutingSlipStepShapes)); + var gen = new RoutingSlipGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostics = run.Results.SelectMany(result => result.Diagnostics).ToArray(); + ScenarioExpect.Equal(3, diagnostics.Count(diagnostic => diagnostic.Id == "PKRS003")); + } + private static CSharpCompilation CreateCompilation(string source, string assemblyName) => RoslynTestHelpers.CreateCompilation( source,