diff --git a/test/PatternKit.Generators.Tests/BackplaneTopologyGeneratorTests.cs b/test/PatternKit.Generators.Tests/BackplaneTopologyGeneratorTests.cs index bb75c83a..a1a847c2 100644 --- a/test/PatternKit.Generators.Tests/BackplaneTopologyGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/BackplaneTopologyGeneratorTests.cs @@ -132,6 +132,220 @@ public static partial class OrderBackplane; ScenarioExpect.Equal("PKBT003", diagnostic.Id); } + [Scenario("Reports diagnostic for missing backplane topology")] + [Fact] + public void ReportsDiagnosticForMissingBackplaneTopology() + { + var source = """ + using PatternKit.Generators.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed class OrderServices { } + + [GenerateBackplaneTopology(typeof(OrderServices))] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForMissingBackplaneTopology)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT002", diagnostic.Id); + } + + [Scenario("Reports diagnostic for duplicate default request routes")] + [Fact] + public void ReportsDiagnosticForDuplicateDefaultRequestRoutes() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed record SubmitOrder(string Id); + public sealed record OrderAccepted(string Id); + + public sealed class OrderServices + { + public ValueTask AcceptAAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => new(new OrderAccepted(message.Payload.Id)); + + public ValueTask AcceptBAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => new(new OrderAccepted(message.Payload.Id)); + } + + [GenerateBackplaneTopology(typeof(OrderServices))] + [BackplaneRequestReply(typeof(SubmitOrder), typeof(OrderAccepted), "orders.a", nameof(OrderServices.AcceptAAsync))] + [BackplaneRequestReply(typeof(SubmitOrder), typeof(OrderAccepted), "orders.b", nameof(OrderServices.AcceptBAsync))] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForDuplicateDefaultRequestRoutes)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT005", diagnostic.Id); + } + + [Scenario("Reports diagnostic for invalid request predicate")] + [Fact] + public void ReportsDiagnosticForInvalidRequestPredicate() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed record SubmitOrder(string Id); + public sealed record OrderAccepted(string Id); + + public sealed class OrderServices + { + public ValueTask AcceptAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => new(new OrderAccepted(message.Payload.Id)); + } + + [GenerateBackplaneTopology(typeof(OrderServices))] + [BackplaneRequestReply(typeof(SubmitOrder), typeof(OrderAccepted), "orders", nameof(OrderServices.AcceptAsync), PredicateMethodName = nameof(IsPriority))] + public static partial class OrderBackplane + { + private static string IsPriority(Message message, MessageContext context) => "wrong"; + } + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForInvalidRequestPredicate)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT003", diagnostic.Id); + } + + [Scenario("Reports diagnostic for invalid subscription handler")] + [Fact] + public void ReportsDiagnosticForInvalidSubscriptionHandler() + { + var source = """ + using PatternKit.Generators.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed record OrderSubmitted(string Id); + public sealed class OrderServices { public void Audit(OrderSubmitted submitted) { } } + + [GenerateBackplaneTopology(typeof(OrderServices))] + [BackplaneSubscription(typeof(OrderSubmitted), "orders.submitted", "audit-service", nameof(OrderServices.Audit))] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForInvalidSubscriptionHandler)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT004", diagnostic.Id); + } + + [Scenario("Skips malformed backplane topology attribute")] + [Fact] + public void SkipsMalformedBackplaneTopologyAttribute() + { + var source = """ + namespace PatternKit.Generators.Messaging; + + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)] + public sealed class GenerateBackplaneTopologyAttribute : System.Attribute; + + namespace PatternKit.Examples.Messaging; + + [PatternKit.Generators.Messaging.GenerateBackplaneTopology] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(SkipsMalformedBackplaneTopologyAttribute)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + ScenarioExpect.All(run.Results, result => ScenarioExpect.Empty(result.Diagnostics)); + ScenarioExpect.Empty(run.Results.SelectMany(result => result.GeneratedSources)); + } + + [Scenario("Reports diagnostic for blank request reply endpoint")] + [Fact] + public void ReportsDiagnosticForBlankRequestReplyEndpoint() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed record SubmitOrder(string Id); + public sealed record OrderAccepted(string Id); + + public sealed class OrderServices + { + public ValueTask AcceptAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => new(new OrderAccepted(message.Payload.Id)); + } + + [GenerateBackplaneTopology(typeof(OrderServices))] + [BackplaneRequestReply(typeof(SubmitOrder), typeof(OrderAccepted), " ", nameof(OrderServices.AcceptAsync))] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForBlankRequestReplyEndpoint)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT003", diagnostic.Id); + } + + [Scenario("Reports diagnostic for blank subscription topic")] + [Fact] + public void ReportsDiagnosticForBlankSubscriptionTopic() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace PatternKit.Examples.Messaging; + + public sealed record OrderSubmitted(string Id); + + public sealed class OrderServices + { + public ValueTask AuditAsync(Message message, MessageContext context, CancellationToken cancellationToken) + => default; + } + + [GenerateBackplaneTopology(typeof(OrderServices))] + [BackplaneSubscription(typeof(OrderSubmitted), " ", "audit", nameof(OrderServices.AuditAsync))] + public static partial class OrderBackplane; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForBlankSubscriptionTopic)); + var gen = new BackplaneTopologyGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKBT004", diagnostic.Id); + } + private static CSharpCompilation CreateCompilation(string source, string assemblyName) => RoslynTestHelpers.CreateCompilation( source, diff --git a/test/PatternKit.Generators.Tests/SplitterAggregatorGeneratorTests.cs b/test/PatternKit.Generators.Tests/SplitterAggregatorGeneratorTests.cs index a16544da..e77e4456 100644 --- a/test/PatternKit.Generators.Tests/SplitterAggregatorGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/SplitterAggregatorGeneratorTests.cs @@ -233,6 +233,194 @@ public static partial class OrderLineAggregator ScenarioExpect.Equal("PKSA006", diagnostic.Id); } + [Scenario("Reports diagnostic for invalid splitter projection")] + [Fact] + public void ReportsDiagnosticForInvalidSplitterProjection() + { + var source = """ + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Order; + public sealed record Line; + + [GenerateSplitter(typeof(Order), typeof(Line))] + public static partial class OrderLineSplitter + { + [SplitterProjection] + private static string ProjectLines(Message message, MessageContext context) => "wrong"; + } + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForInvalidSplitterProjection)); + var gen = new SplitterAggregatorGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKSA003", diagnostic.Id); + } + + [Scenario("Generates splitter factory from concrete enumerable projection")] + [Fact] + public void GeneratesSplitterFactoryFromConcreteEnumerableProjection() + { + var source = """ + using System.Collections.Generic; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Order(string Id); + public sealed record Line(string OrderId); + + [GenerateSplitter(typeof(Order), typeof(Line))] + public static partial class OrderLineSplitter + { + [SplitterProjection] + private static List ProjectLines(Message message, MessageContext context) + => [new(message.Payload.Id)]; + } + """; + + var comp = CreateCompilation(source, nameof(GeneratesSplitterFactoryFromConcreteEnumerableProjection)); + var gen = new SplitterAggregatorGenerator(); + _ = 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)); + ScenarioExpect.Equal("OrderLineSplitter.Splitter.g.cs", generated.HintName); + ScenarioExpect.Contains(".Use(ProjectLines)", generated.SourceText.ToString()); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + + [Scenario("Reports diagnostic for non-partial aggregator contract")] + [Fact] + public void ReportsDiagnosticForNonPartialAggregatorContract() + { + var source = """ + using PatternKit.Generators.Messaging; + + namespace MyApp; + + public sealed record Line; + + [GenerateAggregator(typeof(string), typeof(Line), typeof(decimal))] + public static class OrderLineAggregator; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForNonPartialAggregatorContract)); + var gen = new SplitterAggregatorGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKSA001", diagnostic.Id); + } + + [Scenario("Reports diagnostic for missing aggregator methods")] + [Fact] + public void ReportsDiagnosticForMissingAggregatorMethods() + { + var source = """ + using PatternKit.Generators.Messaging; + + namespace MyApp; + + public sealed record Line; + + [GenerateAggregator(typeof(string), typeof(Line), typeof(decimal))] + public static partial class OrderLineAggregator; + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticForMissingAggregatorMethods)); + var gen = new SplitterAggregatorGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); + ScenarioExpect.Equal("PKSA004", diagnostic.Id); + } + + [Scenario("Reports diagnostics for invalid aggregator correlation and completion")] + [Fact] + public void ReportsDiagnosticsForInvalidAggregatorCorrelationAndCompletion() + { + var source = """ + using System.Collections.Generic; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Line(string OrderId, decimal Amount); + + [GenerateAggregator(typeof(string), typeof(Line), typeof(decimal))] + public static partial class OrderLineAggregator + { + [AggregatorCorrelation] + private static int Correlate(Message message, MessageContext context) => 1; + + [AggregatorCompletion] + private static bool Complete(string key, Message[] messages, MessageContext context) => true; + + [AggregatorProjection] + private static decimal Project(string key, IReadOnlyList> messages, MessageContext context) => 0m; + } + """; + + var comp = CreateCompilation(source, nameof(ReportsDiagnosticsForInvalidAggregatorCorrelationAndCompletion)); + var gen = new SplitterAggregatorGenerator(); + _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); + + var diagnostics = run.Results.SelectMany(result => result.Diagnostics).ToArray(); + ScenarioExpect.Equal(2, diagnostics.Length); + ScenarioExpect.All(diagnostics, diagnostic => ScenarioExpect.Equal("PKSA005", diagnostic.Id)); + } + + [Scenario("Generates aggregator factory with case-insensitive duplicate policies")] + [Theory] + [InlineData("ignore", "DuplicateMessagePolicy.Ignore")] + [InlineData("INCLUDE", "DuplicateMessagePolicy.Include")] + public void GeneratesAggregatorFactoryWithCaseInsensitiveDuplicatePolicies(string policy, string expected) + { + var source = $$""" + using System.Collections.Generic; + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + + namespace MyApp; + + public sealed record Line(string OrderId, decimal Amount); + + [GenerateAggregator(typeof(string), typeof(Line), typeof(decimal), DuplicatePolicy = "{{policy}}")] + public static partial class OrderLineAggregator + { + [AggregatorCorrelation] + private static string Correlate(Message message, MessageContext context) => message.Payload.OrderId; + + [AggregatorCompletion] + private static bool Complete(string key, IReadOnlyList> messages, MessageContext context) => true; + + [AggregatorProjection] + private static decimal Project(string key, IReadOnlyList> messages, MessageContext context) => 0m; + } + """; + + var comp = CreateCompilation(source, $"{nameof(GeneratesAggregatorFactoryWithCaseInsensitiveDuplicatePolicies)}_{policy}"); + var gen = new SplitterAggregatorGenerator(); + _ = 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)); + ScenarioExpect.Contains(expected, generated.SourceText.ToString()); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics)); + } + private static CSharpCompilation CreateCompilation(string source, string assemblyName) => RoslynTestHelpers.CreateCompilation( source,