diff --git a/src/PatternKit.Generators/Messaging/MessageTranslatorGenerator.cs b/src/PatternKit.Generators/Messaging/MessageTranslatorGenerator.cs index 021bc3c2..5422aaee 100644 --- a/src/PatternKit.Generators/Messaging/MessageTranslatorGenerator.cs +++ b/src/PatternKit.Generators/Messaging/MessageTranslatorGenerator.cs @@ -64,7 +64,7 @@ private static void Generate(SourceProductionContext context, INamedTypeSymbol t var inputType = attribute.ConstructorArguments.Length >= 1 ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol : null; var outputType = attribute.ConstructorArguments.Length >= 2 ? attribute.ConstructorArguments[1].Value as INamedTypeSymbol : null; - if (inputType is null || outputType is null) + if (inputType is null || outputType is null || inputType.TypeKind == TypeKind.Error || outputType.TypeKind == TypeKind.Error) return; var handlers = type.GetMembers().OfType() @@ -117,31 +117,39 @@ private static string GenerateSource( sb.AppendLine(); } - sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); - if (type.IsStatic) - sb.Append("static "); - else if (type.IsAbstract && type.TypeKind == TypeKind.Class) - sb.Append("abstract "); - else if (type.IsSealed && type.TypeKind == TypeKind.Class) - sb.Append("sealed "); - sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); - sb.AppendLine("{"); - sb.Append(" public static global::PatternKit.Messaging.Transformation.MessageTranslator<") + var indent = string.Empty; + foreach (var containingType in GetContainingTypes(type)) + { + AppendTypeDeclaration(sb, containingType, indent); + sb.Append(indent).AppendLine("{"); + indent += " "; + } + + AppendTypeDeclaration(sb, type, indent); + sb.Append(indent).AppendLine("{"); + var memberIndent = indent + " "; + var bodyIndent = memberIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.Messaging.Transformation.MessageTranslator<") .Append(inputName).Append(", ").Append(outputName).Append("> ").Append(factoryName).AppendLine("()"); - sb.AppendLine(" {"); - sb.Append(" var builder = global::PatternKit.Messaging.Transformation.MessageTranslator<") + sb.Append(memberIndent).AppendLine("{"); + sb.Append(bodyIndent).Append("var builder = global::PatternKit.Messaging.Transformation.MessageTranslator<") .Append(inputName).Append(", ").Append(outputName).Append(">.Create(\"").Append(Escape(translatorName)).AppendLine("\")"); - sb.Append(" .PreserveHeaders(").Append(preserveHeaders ? "true" : "false").AppendLine(")"); - sb.Append(" .TranslateWith(static (message, context) => ").Append(handlerName).AppendLine("(message, context));"); + sb.Append(bodyIndent).Append(" .PreserveHeaders(").Append(preserveHeaders ? "true" : "false").AppendLine(")"); + sb.Append(bodyIndent).Append(" .TranslateWith(static (message, context) => ").Append(handlerName).AppendLine("(message, context));"); foreach (var drop in drops) - sb.Append(" builder.DropHeader(\"").Append(Escape(drop)).AppendLine("\");"); + sb.Append(bodyIndent).Append("builder.DropHeader(\"").Append(Escape(drop)).AppendLine("\");"); foreach (var set in sets) - sb.Append(" builder.SetHeader(\"").Append(Escape(set.Name)).Append("\", \"").Append(Escape(set.Value)).AppendLine("\");"); + sb.Append(bodyIndent).Append("builder.SetHeader(\"").Append(Escape(set.Name)).Append("\", \"").Append(Escape(set.Value)).AppendLine("\");"); - sb.AppendLine(" return builder.Build();"); - sb.AppendLine(" }"); - sb.AppendLine("}"); + sb.Append(bodyIndent).AppendLine("return builder.Build();"); + sb.Append(memberIndent).AppendLine("}"); + sb.Append(indent).AppendLine("}"); + while (indent.Length > 0) + { + indent = indent.Substring(0, indent.Length - 4); + sb.Append(indent).AppendLine("}"); + } return sb.ToString(); } @@ -175,6 +183,26 @@ private static IReadOnlyList GetSetHeaders(INamedTypeSymbol ty .Where(static header => !string.IsNullOrWhiteSpace(header.Name)) .ToArray(); + private static IReadOnlyList GetContainingTypes(INamedTypeSymbol type) + { + var stack = new Stack(); + for (var current = type.ContainingType; current is not null; current = current.ContainingType) + stack.Push(current); + return stack.ToArray(); + } + + private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, string indent) + { + sb.Append(indent).Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); + if (type.IsStatic) + sb.Append("static "); + else if (type.IsAbstract && type.TypeKind == TypeKind.Class) + sb.Append("abstract "); + else if (type.IsSealed && type.TypeKind == TypeKind.Class) + sb.Append("sealed "); + sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); + } + private static string? GetNamedString(AttributeData attribute, string name) => attribute.NamedArguments.FirstOrDefault(kv => kv.Key == name).Value.Value as string; diff --git a/src/PatternKit.Generators/PriorityQueue/PriorityQueueGenerator.cs b/src/PatternKit.Generators/PriorityQueue/PriorityQueueGenerator.cs index 08cc7ff6..d7b8caea 100644 --- a/src/PatternKit.Generators/PriorityQueue/PriorityQueueGenerator.cs +++ b/src/PatternKit.Generators/PriorityQueue/PriorityQueueGenerator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -58,7 +59,7 @@ private static void Generate(SourceProductionContext context, INamedTypeSymbol t var itemType = attribute.ConstructorArguments.Length >= 1 ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol : null; var priorityType = attribute.ConstructorArguments.Length >= 2 ? attribute.ConstructorArguments[1].Value as INamedTypeSymbol : null; - if (itemType is null || priorityType is null) + if (itemType is null || priorityType is null || itemType.TypeKind == TypeKind.Error || priorityType.TypeKind == TypeKind.Error) return; var selectors = type.GetMembers().OfType().Where(static method => @@ -113,7 +114,47 @@ private static string GenerateSource( sb.AppendLine(); } - sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); + var indent = string.Empty; + foreach (var containingType in GetContainingTypes(type)) + { + AppendTypeDeclaration(sb, containingType, indent); + sb.Append(indent).AppendLine("{"); + indent += " "; + } + + AppendTypeDeclaration(sb, type, indent); + sb.Append(indent).AppendLine("{"); + var memberIndent = indent + " "; + var bodyIndent = memberIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.PriorityQueue.PriorityQueuePolicy<").Append(itemTypeName).Append(", ").Append(priorityTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); + sb.Append(memberIndent).AppendLine("{"); + sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.PriorityQueue.PriorityQueuePolicy<").Append(itemTypeName).Append(", ").Append(priorityTypeName).Append(">.Create(\"").Append(Escape(queueName)).AppendLine("\")"); + sb.Append(bodyIndent).Append(" .WithPrioritySelector(").Append(selectorName).AppendLine(")"); + sb.Append(bodyIndent).AppendLine(dequeueHighestPriorityFirst + ? " .DequeueHighestPriorityFirst()" + : " .DequeueLowestPriorityFirst()"); + sb.Append(bodyIndent).AppendLine(" .Build();"); + sb.Append(memberIndent).AppendLine("}"); + sb.Append(indent).AppendLine("}"); + while (indent.Length > 0) + { + indent = indent.Substring(0, indent.Length - 4); + sb.Append(indent).AppendLine("}"); + } + return sb.ToString(); + } + + private static IReadOnlyList GetContainingTypes(INamedTypeSymbol type) + { + var stack = new Stack(); + for (var current = type.ContainingType; current is not null; current = current.ContainingType) + stack.Push(current); + return stack.ToArray(); + } + + private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, string indent) + { + sb.Append(indent).Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); if (type.IsStatic) sb.Append("static "); else if (type.IsAbstract && type.TypeKind == TypeKind.Class) @@ -121,18 +162,6 @@ private static string GenerateSource( else if (type.IsSealed && type.TypeKind == TypeKind.Class) sb.Append("sealed "); sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); - sb.AppendLine("{"); - sb.Append(" public static global::PatternKit.Cloud.PriorityQueue.PriorityQueuePolicy<").Append(itemTypeName).Append(", ").Append(priorityTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); - sb.AppendLine(" {"); - sb.Append(" return global::PatternKit.Cloud.PriorityQueue.PriorityQueuePolicy<").Append(itemTypeName).Append(", ").Append(priorityTypeName).Append(">.Create(\"").Append(Escape(queueName)).AppendLine("\")"); - sb.Append(" .WithPrioritySelector(").Append(selectorName).AppendLine(")"); - sb.AppendLine(dequeueHighestPriorityFirst - ? " .DequeueHighestPriorityFirst()" - : " .DequeueLowestPriorityFirst()"); - sb.AppendLine(" .Build();"); - sb.AppendLine(" }"); - sb.AppendLine("}"); - return sb.ToString(); } private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\""); diff --git a/src/PatternKit.Generators/QueueLoadLeveling/QueueLoadLevelingPolicyGenerator.cs b/src/PatternKit.Generators/QueueLoadLeveling/QueueLoadLevelingPolicyGenerator.cs index c5db34ff..16c9c7f0 100644 --- a/src/PatternKit.Generators/QueueLoadLeveling/QueueLoadLevelingPolicyGenerator.cs +++ b/src/PatternKit.Generators/QueueLoadLeveling/QueueLoadLevelingPolicyGenerator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -51,7 +52,7 @@ private static void Generate(SourceProductionContext context, INamedTypeSymbol t } var resultType = attribute.ConstructorArguments.Length >= 1 ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol : null; - if (resultType is null) + if (resultType is null || resultType.TypeKind == TypeKind.Error) return; var maxConcurrentWorkers = GetNamedInt(attribute, "MaxConcurrentWorkers") ?? 1; @@ -96,7 +97,46 @@ private static string GenerateSource( sb.AppendLine(); } - sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); + var indent = string.Empty; + foreach (var containingType in GetContainingTypes(type)) + { + AppendTypeDeclaration(sb, containingType, indent); + sb.Append(indent).AppendLine("{"); + indent += " "; + } + + AppendTypeDeclaration(sb, type, indent); + sb.Append(indent).AppendLine("{"); + var memberIndent = indent + " "; + var bodyIndent = memberIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.QueueLoadLeveling.QueueLoadLevelingPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); + sb.Append(memberIndent).AppendLine("{"); + sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.QueueLoadLeveling.QueueLoadLevelingPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")"); + sb.Append(bodyIndent).Append(" .WithMaxConcurrentWorkers(").Append(maxConcurrentWorkers).AppendLine(")"); + sb.Append(bodyIndent).Append(" .WithMaxQueueLength(").Append(maxQueueLength).AppendLine(")"); + sb.Append(bodyIndent).Append(" .WithQueueTimeout(global::System.TimeSpan.FromMilliseconds(").Append(queueTimeoutMilliseconds).AppendLine("))"); + sb.Append(bodyIndent).AppendLine(" .Build();"); + sb.Append(memberIndent).AppendLine("}"); + sb.Append(indent).AppendLine("}"); + while (indent.Length > 0) + { + indent = indent.Substring(0, indent.Length - 4); + sb.Append(indent).AppendLine("}"); + } + return sb.ToString(); + } + + private static IReadOnlyList GetContainingTypes(INamedTypeSymbol type) + { + var stack = new Stack(); + for (var current = type.ContainingType; current is not null; current = current.ContainingType) + stack.Push(current); + return stack.ToArray(); + } + + private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, string indent) + { + sb.Append(indent).Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); if (type.IsStatic) sb.Append("static "); else if (type.IsAbstract && type.TypeKind == TypeKind.Class) @@ -104,17 +144,6 @@ private static string GenerateSource( else if (type.IsSealed && type.TypeKind == TypeKind.Class) sb.Append("sealed "); sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); - sb.AppendLine("{"); - sb.Append(" public static global::PatternKit.Cloud.QueueLoadLeveling.QueueLoadLevelingPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); - sb.AppendLine(" {"); - sb.Append(" return global::PatternKit.Cloud.QueueLoadLeveling.QueueLoadLevelingPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")"); - sb.Append(" .WithMaxConcurrentWorkers(").Append(maxConcurrentWorkers).AppendLine(")"); - sb.Append(" .WithMaxQueueLength(").Append(maxQueueLength).AppendLine(")"); - sb.Append(" .WithQueueTimeout(global::System.TimeSpan.FromMilliseconds(").Append(queueTimeoutMilliseconds).AppendLine("))"); - sb.AppendLine(" .Build();"); - sb.AppendLine(" }"); - sb.AppendLine("}"); - return sb.ToString(); } private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\""); diff --git a/test/PatternKit.Generators.Tests/MessageTranslatorGeneratorTests.cs b/test/PatternKit.Generators.Tests/MessageTranslatorGeneratorTests.cs index 63df0147..85cc64a5 100644 --- a/test/PatternKit.Generators.Tests/MessageTranslatorGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/MessageTranslatorGeneratorTests.cs @@ -1,26 +1,24 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using PatternKit.Generators.Messaging; using PatternKit.Messaging; using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; namespace PatternKit.Generators.Tests; -public sealed class MessageTranslatorGeneratorTests +[Feature("Message Translator generator")] +public sealed partial class MessageTranslatorGeneratorTests(ITestOutputHelper output) : TinyBddXunitBase(output) { [Scenario("Generates message translator factory")] [Fact] - public void GeneratesMessageTranslatorFactory() - { - var source = """ + public Task Generates_Message_Translator_Factory() + => Given("a message translator declaration", () => Compile(""" using PatternKit.Generators.Messaging; using PatternKit.Messaging; - namespace Demo; - public sealed record PartnerOrder(string Id, decimal Amount); public sealed record Order(string OrderId, decimal Total); - [GenerateMessageTranslator(typeof(PartnerOrder), typeof(Order), FactoryName = "Build", TranslatorName = "partner-orders")] [MessageTranslatorDropHeader("raw-signature")] [MessageTranslatorHeader("content-type", "application/vnd.demo.order+json")] @@ -30,104 +28,203 @@ public static partial class PartnerOrderTranslator private static Order Translate(Message message, MessageContext context) => new(message.Payload.Id, message.Payload.Amount); } - """; - - var comp = CreateCompilation(source, nameof(GeneratesMessageTranslatorFactory)); - var gen = new MessageTranslatorGenerator(); - _ = 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("PartnerOrderTranslator.MessageTranslator.g.cs", generated.HintName); - ScenarioExpect.Contains("Build()", text); - ScenarioExpect.Contains("MessageTranslator.Create(\"partner-orders\")", text); - ScenarioExpect.Contains(".PreserveHeaders(true)", text); - ScenarioExpect.Contains(".TranslateWith(static (message, context) => Translate(message, context));", text); - ScenarioExpect.Contains("builder.DropHeader(\"raw-signature\");", text); - ScenarioExpect.Contains("builder.SetHeader(\"content-type\", \"application/vnd.demo.order+json\");", text); - ScenarioExpect.True(updated.Emit(Stream.Null).Success); - } - - [Scenario("Reports diagnostic for non-partial message translator host")] + """)) + .Then("the generated source creates the configured translator", result => + { + ScenarioExpect.Empty(result.Diagnostics); + var generated = ScenarioExpect.Single(result.GeneratedSources); + ScenarioExpect.Equal("PartnerOrderTranslator.MessageTranslator.g.cs", generated.HintName); + ScenarioExpect.Contains("Build()", generated.Source); + ScenarioExpect.Contains("MessageTranslator.Create(\"partner-orders\")", generated.Source); + ScenarioExpect.Contains(".PreserveHeaders(true)", generated.Source); + ScenarioExpect.Contains(".TranslateWith(static (message, context) => Translate(message, context));", generated.Source); + ScenarioExpect.Contains("builder.DropHeader(\"raw-signature\");", generated.Source); + ScenarioExpect.Contains("builder.SetHeader(\"content-type\", \"application/vnd.demo.order+json\");", generated.Source); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Reports diagnostics for invalid message translator declarations")] [Fact] - public void ReportsDiagnosticForNonPartialMessageTranslatorHost() - { - var source = """ - using PatternKit.Generators.Messaging; - - namespace Demo; - - [GenerateMessageTranslator(typeof(string), typeof(int))] - public static class Host; - """; - - var diagnostic = RunAndGetSingleDiagnostic(source, nameof(ReportsDiagnosticForNonPartialMessageTranslatorHost)); - - ScenarioExpect.Equal("PKMT001", diagnostic.Id); - } - - [Scenario("Reports diagnostic for missing message translator handler")] - [Fact] - public void ReportsDiagnosticForMissingMessageTranslatorHandler() - { - var source = """ - using PatternKit.Generators.Messaging; - - namespace Demo; - - [GenerateMessageTranslator(typeof(string), typeof(int))] - public static partial class Host; - """; - - var diagnostic = RunAndGetSingleDiagnostic(source, nameof(ReportsDiagnosticForMissingMessageTranslatorHandler)); - - ScenarioExpect.Equal("PKMT002", diagnostic.Id); - } - - [Scenario("Reports diagnostic for invalid message translator handler")] + public Task Reports_Diagnostics_For_Invalid_Message_Translator_Declarations() + => Given("invalid message translator declarations", () => new[] + { + Compile(""" + using PatternKit.Generators.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static class Host; + """), + Compile(""" + using PatternKit.Generators.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host; + """), + Compile(""" + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host + { + [MessageTranslatorHandler] + private static string Translate(Message message, MessageContext context) => message.Payload; + } + """), + Compile(""" + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host + { + [MessageTranslatorHandler] + private int Translate(Message message, MessageContext context) => 1; + } + """), + Compile(""" + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host + { + [MessageTranslatorHandler] + private static int Translate(Message message) => 1; + } + """), + Compile(""" + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host + { + [MessageTranslatorHandler] + private static int Translate(Message message, MessageContext context) => 1; + } + """), + Compile(""" + using PatternKit.Generators.Messaging; + using PatternKit.Messaging; + namespace Demo; + [GenerateMessageTranslator(typeof(string), typeof(int))] + public static partial class Host + { + [MessageTranslatorHandler] + private static int Translate(Message message, MessageContext context) => 1; + } + """) + }) + .Then("diagnostics identify the invalid declarations", results => + { + ScenarioExpect.Contains(results[0].Diagnostics, diagnostic => diagnostic.Id == "PKMT001"); + ScenarioExpect.Contains(results[1].Diagnostics, diagnostic => diagnostic.Id == "PKMT002"); + ScenarioExpect.Contains(results[2].Diagnostics, diagnostic => diagnostic.Id == "PKMT003"); + ScenarioExpect.Contains(results[3].Diagnostics, diagnostic => diagnostic.Id == "PKMT003"); + ScenarioExpect.Contains(results[4].Diagnostics, diagnostic => diagnostic.Id == "PKMT003"); + ScenarioExpect.Contains(results[5].Diagnostics, diagnostic => diagnostic.Id == "PKMT003"); + ScenarioExpect.Contains(results[6].Diagnostics, diagnostic => diagnostic.Id == "PKMT003"); + }) + .AssertPassed(); + + [Scenario("Generates translator defaults inside nested hosts")] [Fact] - public void ReportsDiagnosticForInvalidMessageTranslatorHandler() - { - var source = """ + public Task Generates_Translator_Defaults_Inside_Nested_Hosts() + => Given("a nested message translator declaration", () => Compile(""" using PatternKit.Generators.Messaging; using PatternKit.Messaging; - namespace Demo; - - [GenerateMessageTranslator(typeof(string), typeof(int))] - public static partial class Host + public static partial class IntegrationModule { - [MessageTranslatorHandler] - private static string Translate(Message message, MessageContext context) => message.Payload; + internal abstract partial class Translators + { + [GenerateMessageTranslator(typeof(string), typeof(int), TranslatorName = "translate\"" + "\\order", PreserveHeaders = false)] + [MessageTranslatorDropHeader("")] + [MessageTranslatorHeader("", "ignored")] + [MessageTranslatorHeader("x-source", "partner\"" + "\\feed")] + private sealed partial class OrderTranslator + { + [MessageTranslatorHandler] + private static int Translate(Message message, MessageContext context) => message.Payload.Length; + } + } } - """; - - var diagnostic = RunAndGetSingleDiagnostic(source, nameof(ReportsDiagnosticForInvalidMessageTranslatorHandler)); - - ScenarioExpect.Equal("PKMT003", diagnostic.Id); - } + """)) + .Then("the generated source preserves host shape and configured headers", result => + { + ScenarioExpect.Empty(result.Diagnostics); + var generated = ScenarioExpect.Single(result.GeneratedSources); + ScenarioExpect.Contains("public static partial class IntegrationModule", generated.Source); + ScenarioExpect.Contains("internal abstract partial class Translators", generated.Source); + ScenarioExpect.Contains("private sealed partial class OrderTranslator", generated.Source); + ScenarioExpect.Contains("Create(\"translate\\\"\\\\order\")", generated.Source); + ScenarioExpect.Contains(".PreserveHeaders(false)", generated.Source); + ScenarioExpect.DoesNotContain("DropHeader(\"\")", generated.Source); + ScenarioExpect.DoesNotContain("SetHeader(\"\"", generated.Source); + ScenarioExpect.Contains("builder.SetHeader(\"x-source\", \"partner\\\"\\\\feed\");", generated.Source); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Skips message translator generation for malformed type arguments")] + [Fact] + public Task Skips_Message_Translator_Generation_For_Malformed_Type_Arguments() + => Given("message translator declarations with unresolved input and output types", () => new[] + { + Compile(""" + using PatternKit.Generators.Messaging; + [GenerateMessageTranslator(typeof(MissingInput), typeof(int))] + public static partial class MissingInputTranslator; + """), + Compile(""" + using PatternKit.Generators.Messaging; + [GenerateMessageTranslator(typeof(string), typeof(MissingOutput))] + public static partial class MissingOutputTranslator; + """) + }) + .Then("no generated sources are produced by the generator", results => + { + foreach (var result in results) + { + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Empty(result.GeneratedSources); + ScenarioExpect.False(result.EmitSuccess); + } + }) + .AssertPassed(); - private static CSharpCompilation CreateCompilation(string source, string assemblyName) - => RoslynTestHelpers.CreateCompilation( + private static GeneratorResult Compile(string source) + { + var compilation = RoslynTestHelpers.CreateCompilation( source, - assemblyName, + "MessageTranslatorGeneratorTests", extra: [ MetadataReference.CreateFromFile(GetAbstractionsAssemblyPath()), MetadataReference.CreateFromFile(typeof(Message<>).Assembly.Location) ]); + _ = RoslynTestHelpers.Run(compilation, new MessageTranslatorGenerator(), out var run, out var updated); + var result = run.Results.Single(); + var emit = updated.Emit(Stream.Null); + return new GeneratorResult( + result.Diagnostics.ToArray(), + result.GeneratedSources.Select(static source => new GeneratedSource(source.HintName, source.SourceText.ToString())).ToArray(), + emit.Success, + emit.Diagnostics.Select(static diagnostic => diagnostic.ToString()).ToArray()); + } private static string GetAbstractionsAssemblyPath() => Path.Combine( Path.GetDirectoryName(typeof(MessageTranslatorGenerator).Assembly.Location)!, "PatternKit.Generators.Abstractions.dll"); - private static Diagnostic RunAndGetSingleDiagnostic(string source, string assemblyName) - { - var comp = CreateCompilation(source, assemblyName); - var gen = new MessageTranslatorGenerator(); - _ = RoslynTestHelpers.Run(comp, gen, out var run, out _); - return ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics)); - } + private sealed record GeneratorResult( + IReadOnlyList Diagnostics, + IReadOnlyList GeneratedSources, + bool EmitSuccess, + IReadOnlyList EmitDiagnostics); + + private sealed record GeneratedSource(string HintName, string Source); } diff --git a/test/PatternKit.Generators.Tests/PriorityQueueGeneratorTests.cs b/test/PatternKit.Generators.Tests/PriorityQueueGeneratorTests.cs index 599ae727..763f7927 100644 --- a/test/PatternKit.Generators.Tests/PriorityQueueGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/PriorityQueueGeneratorTests.cs @@ -93,6 +93,64 @@ internal partial struct PriorityDefaults }) .AssertPassed(); + [Scenario("Generates priority queue inside nested hosts")] + [Fact] + public Task Generates_Priority_Queue_Inside_Nested_Hosts() + => Given("a nested priority queue declaration", () => Compile(""" + using PatternKit.Generators.PriorityQueue; + namespace Demo; + public static partial class FulfillmentModule + { + internal abstract partial class Queues + { + [GeneratePriorityQueue(typeof(string), typeof(int))] + private sealed partial class WorkPriorityQueue + { + [PriorityQueuePrioritySelector] + private static int SelectPriority(string item) => item.Length; + } + } + } + """)) + .Then("the generated source recreates each containing partial type", result => + { + var source = ScenarioExpect.Single(result.GeneratedSources); + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Contains("public static partial class FulfillmentModule", source); + ScenarioExpect.Contains("internal abstract partial class Queues", source); + ScenarioExpect.Contains("private sealed partial class WorkPriorityQueue", source); + ScenarioExpect.Contains(".WithPrioritySelector(SelectPriority)", source); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Skips priority queue generation for malformed type arguments")] + [Fact] + public Task Skips_Priority_Queue_Generation_For_Malformed_Type_Arguments() + => Given("priority queue declarations with unresolved item and priority types", () => new[] + { + Compile(""" + using PatternKit.Generators.PriorityQueue; + [GeneratePriorityQueue(typeof(MissingItem), typeof(int))] + public static partial class MissingItemQueue; + """), + Compile(""" + using PatternKit.Generators.PriorityQueue; + [GeneratePriorityQueue(typeof(string), typeof(MissingPriority))] + public static partial class MissingPriorityQueue; + """) + }) + .Then("no generated sources are produced by the generator", results => + { + foreach (var result in results) + { + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Empty(result.GeneratedSources); + ScenarioExpect.False(result.EmitSuccess); + } + }) + .AssertPassed(); + private static GeneratorResult Compile(string source) { var compilation = RoslynTestHelpers.CreateCompilation( diff --git a/test/PatternKit.Generators.Tests/QueueLoadLevelingPolicyGeneratorTests.cs b/test/PatternKit.Generators.Tests/QueueLoadLevelingPolicyGeneratorTests.cs index c85b0038..b441625c 100644 --- a/test/PatternKit.Generators.Tests/QueueLoadLevelingPolicyGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/QueueLoadLevelingPolicyGeneratorTests.cs @@ -47,12 +47,24 @@ public static class QueueHost; using PatternKit.Generators.QueueLoadLeveling; [GenerateQueueLoadLevelingPolicy(typeof(string), MaxConcurrentWorkers = 0)] public static partial class QueueHost; + """), + Compile(""" + using PatternKit.Generators.QueueLoadLeveling; + [GenerateQueueLoadLevelingPolicy(typeof(string), MaxQueueLength = -1)] + public static partial class QueueHost; + """), + Compile(""" + using PatternKit.Generators.QueueLoadLeveling; + [GenerateQueueLoadLevelingPolicy(typeof(string), QueueTimeoutMilliseconds = -1)] + public static partial class QueueHost; """) }) .Then("diagnostics identify the invalid declarations", results => { ScenarioExpect.Contains(results[0].Diagnostics, diagnostic => diagnostic.Id == "PKQL001"); ScenarioExpect.Contains(results[1].Diagnostics, diagnostic => diagnostic.Id == "PKQL002"); + ScenarioExpect.Contains(results[2].Diagnostics, diagnostic => diagnostic.Id == "PKQL002"); + ScenarioExpect.Contains(results[3].Diagnostics, diagnostic => diagnostic.Id == "PKQL002"); }) .AssertPassed(); @@ -94,6 +106,48 @@ public abstract partial class EscapedQueue; }) .AssertPassed(); + [Scenario("Generates queue load leveling policy inside nested hosts")] + [Fact] + public Task Generates_Queue_Load_Leveling_Policy_Inside_Nested_Hosts() + => Given("a nested queue load leveling declaration", () => Compile(""" + using PatternKit.Generators.QueueLoadLeveling; + namespace Demo; + public static partial class FulfillmentModule + { + internal abstract partial class Queues + { + [GenerateQueueLoadLevelingPolicy(typeof(string))] + private sealed partial class WorkQueue; + } + } + """)) + .Then("the generated source recreates each containing partial type", result => + { + var source = ScenarioExpect.Single(result.GeneratedSources); + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Contains("public static partial class FulfillmentModule", source); + ScenarioExpect.Contains("internal abstract partial class Queues", source); + ScenarioExpect.Contains("private sealed partial class WorkQueue", source); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Skips queue load leveling generation for malformed result type")] + [Fact] + public Task Skips_Queue_Load_Leveling_Generation_For_Malformed_Result_Type() + => Given("a queue load leveling declaration with an unresolved result type", () => Compile(""" + using PatternKit.Generators.QueueLoadLeveling; + [GenerateQueueLoadLevelingPolicy(typeof(MissingResult))] + public static partial class MissingQueue; + """)) + .Then("no generated source is produced by the generator", result => + { + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Empty(result.GeneratedSources); + ScenarioExpect.False(result.EmitSuccess); + }) + .AssertPassed(); + private static GeneratorResult Compile(string source) { var compilation = RoslynTestHelpers.CreateCompilation(