From ad5ca6548482d712e6ec550f487732d777489322 Mon Sep 17 00:00:00 2001 From: JerrettDavis Date: Fri, 29 May 2026 13:42:46 -0500 Subject: [PATCH] test: cover table data gateway generator hosts --- .../TableDataGatewayGenerator.cs | 54 ++++- .../TableDataGatewayGeneratorTests.cs | 193 +++++++++++++++++- 2 files changed, 231 insertions(+), 16 deletions(-) diff --git a/src/PatternKit.Generators/TableDataGateway/TableDataGatewayGenerator.cs b/src/PatternKit.Generators/TableDataGateway/TableDataGatewayGenerator.cs index e77362d7..fb324328 100644 --- a/src/PatternKit.Generators/TableDataGateway/TableDataGatewayGenerator.cs +++ b/src/PatternKit.Generators/TableDataGateway/TableDataGatewayGenerator.cs @@ -101,6 +101,51 @@ private static string GenerateSource( sb.AppendLine(); } + var containingTypes = GetContainingTypes(type); + var indentLevel = 0; + foreach (var containingType in containingTypes) + { + AppendTypeDeclaration(sb, containingType, indentLevel); + sb.AppendLine(); + sb.AppendLine(new string(' ', indentLevel * 4) + "{"); + indentLevel++; + } + + AppendTypeDeclaration(sb, type, indentLevel); + var indent = new string(' ', indentLevel * 4); + sb.AppendLine(); + sb.AppendLine(indent + "{"); + var memberIndent = indent + " "; + var bodyIndent = memberIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<") + .Append(rowName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()"); + sb.AppendLine(memberIndent + "{"); + sb.Append(bodyIndent).Append("return global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<") + .Append(rowName).Append(", ").Append(keyName).Append(">.Create(\"").Append(Escape(tableName)).Append("\", ").Append(selectorName).AppendLine(").Build();"); + sb.AppendLine(memberIndent + "}"); + sb.AppendLine(indent + "}"); + for (var i = containingTypes.Length - 1; i >= 0; i--) + { + sb.AppendLine(new string(' ', i * 4) + "}"); + } + + return sb.ToString(); + } + + private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type) + { + var containingTypes = new Stack(); + for (var current = type.ContainingType; current is not null; current = current.ContainingType) + { + containingTypes.Push(current); + } + + return containingTypes.ToArray(); + } + + private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel) + { + sb.Append(new string(' ', indentLevel * 4)); sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); if (type.IsStatic) sb.Append("static "); @@ -108,14 +153,7 @@ private static string GenerateSource( 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.Application.TableDataGateway.InMemoryTableDataGateway<") - .Append(rowName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()"); - sb.Append(" => global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<") - .Append(rowName).Append(", ").Append(keyName).Append(">.Create(\"").Append(Escape(tableName)).Append("\", ").Append(selectorName).AppendLine(").Build();"); - sb.AppendLine("}"); - return sb.ToString(); + sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name); } private static bool IsKeySelector(IMethodSymbol method, INamedTypeSymbol rowType, INamedTypeSymbol keyType) diff --git a/test/PatternKit.Generators.Tests/TableDataGatewayGeneratorTests.cs b/test/PatternKit.Generators.Tests/TableDataGatewayGeneratorTests.cs index 70d46467..4d8720f4 100644 --- a/test/PatternKit.Generators.Tests/TableDataGatewayGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/TableDataGatewayGeneratorTests.cs @@ -15,8 +15,11 @@ public sealed partial class TableDataGatewayGeneratorTests(ITestOutputHelper out public Task Generator_Emits_Table_Data_Gateway_Factory() => Given("a valid table data gateway declaration", () => Compile(""" using PatternKit.Generators.TableDataGateway; + namespace Demo; + public sealed record OrderRow(string OrderId); + [GenerateTableDataGateway(typeof(OrderRow), typeof(string), FactoryName = "Build", TableName = "orders")] public static partial class OrderTableGateway { @@ -28,21 +31,42 @@ public static partial class OrderTableGateway { ScenarioExpect.Empty(result.Diagnostics); var source = ScenarioExpect.Single(result.GeneratedSources); + ScenarioExpect.Contains("public static partial class OrderTableGateway", source); ScenarioExpect.Contains("Build()", source); ScenarioExpect.Contains("Create(\"orders\", SelectKey).Build()", source); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); }) .AssertPassed(); - [Scenario("Generator reports invalid table data gateway declarations")] + [Scenario("Generator reports non-partial table data gateway declarations")] + [Fact] + public Task Generator_Reports_Non_Partial_Table_Data_Gateway_Declarations() + => Given("a non-partial table data gateway declaration", () => Compile(""" + using PatternKit.Generators.TableDataGateway; + + public sealed record OrderRow(string OrderId); + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + public static class OrderTableGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + """)) + .Then("the diagnostic identifies the host", result => + ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "PKTDG001")) + .AssertPassed(); + + [Scenario("Generator reports missing or duplicate table data gateway key selectors")] [Theory] - [InlineData("public static class OrderTableGateway { [TableGatewayKeySelector] private static string SelectKey(OrderRow row) => row.OrderId; }", "PKTDG001")] [InlineData("public static partial class OrderTableGateway;", "PKTDG002")] [InlineData("public static partial class OrderTableGateway { [TableGatewayKeySelector] private static string One(OrderRow row) => row.OrderId; [TableGatewayKeySelector] private static string Two(OrderRow row) => row.OrderId; }", "PKTDG002")] - [InlineData("public static partial class OrderTableGateway { [TableGatewayKeySelector] private static int SelectKey(OrderRow row) => 1; }", "PKTDG003")] - public Task Generator_Reports_Invalid_Table_Data_Gateway_Declarations(string declaration, string diagnosticId) - => Given("an invalid table data gateway declaration", () => Compile($$""" + public Task Generator_Reports_Missing_Or_Duplicate_Table_Data_Gateway_Key_Selectors(string declaration, string diagnosticId) + => Given("a table data gateway declaration with an invalid selector count", () => Compile($$""" using PatternKit.Generators.TableDataGateway; + public sealed record OrderRow(string OrderId); + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] {{declaration}} """)) @@ -50,16 +74,169 @@ public sealed record OrderRow(string OrderId); ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId)) .AssertPassed(); + [Scenario("Generator reports invalid table data gateway key selector signatures")] + [Theory] + [InlineData("[TableGatewayKeySelector] private string SelectKey(OrderRow row) => row.OrderId;")] + [InlineData("[TableGatewayKeySelector] private static T SelectKey(OrderRow row) => default!;")] + [InlineData("[TableGatewayKeySelector] private static string SelectKey() => \"missing\";")] + [InlineData("[TableGatewayKeySelector] private static string SelectKey(OrderRow row, string tenant) => row.OrderId;")] + [InlineData("[TableGatewayKeySelector] private static string SelectKey(string row) => row;")] + [InlineData("[TableGatewayKeySelector] private static int SelectKey(OrderRow row) => 1;")] + public Task Generator_Reports_Invalid_Table_Data_Gateway_Key_Selector_Signatures(string selector) + => Given("a table data gateway declaration with an invalid selector signature", () => Compile($$""" + using PatternKit.Generators.TableDataGateway; + + public sealed record OrderRow(string OrderId); + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + public partial class OrderTableGateway + { + {{selector}} + } + """)) + .Then("the expected diagnostic is reported", result => + ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "PKTDG003")) + .AssertPassed(); + + [Scenario("Generator emits table data gateway defaults and type shapes")] + [Fact] + public Task Generator_Emits_Table_Data_Gateway_Defaults_And_Type_Shapes() + => Given("table data gateway declarations using default names and different host shapes", () => Compile(""" + using PatternKit.Generators.TableDataGateway; + + namespace Demo; + + public sealed record OrderRow(string OrderId); + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + internal abstract partial class AbstractGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string), TableName = "tenant\\\"orders")] + public sealed partial class SealedGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + internal partial struct StructGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + """)) + .Then("generated sources preserve host shape and configured names", result => + { + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("internal abstract partial class AbstractGateway", combined); + ScenarioExpect.Contains("Create()", combined); + ScenarioExpect.Contains("Create(\"AbstractGateway\", SelectKey).Build()", combined); + ScenarioExpect.Contains("public sealed partial class SealedGateway", combined); + ScenarioExpect.Contains("Create(\"tenant\\\\\\\"orders\", SelectKey).Build()", combined); + ScenarioExpect.Contains("internal partial struct StructGateway", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Generator emits nested table data gateway host wrappers")] + [Fact] + public Task Generator_Emits_Nested_Table_Data_Gateway_Host_Wrappers() + => Given("nested table data gateway declarations with non-public accessibility", () => Compile(""" + using PatternKit.Generators.TableDataGateway; + + namespace Demo; + + public sealed record OrderRow(string OrderId); + + public partial class GatewayContainer + { + private partial class PrivateHost + { + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + protected partial class ProtectedGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + private protected partial class PrivateProtectedGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + + [GenerateTableDataGateway(typeof(OrderRow), typeof(string))] + protected internal partial class ProtectedInternalGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + } + } + """)) + .Then("generated sources preserve containing partial type wrappers", result => + { + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("public partial class GatewayContainer", combined); + ScenarioExpect.Contains("private partial class PrivateHost", combined); + ScenarioExpect.Contains("protected partial class ProtectedGateway", combined); + ScenarioExpect.Contains("private protected partial class PrivateProtectedGateway", combined); + ScenarioExpect.Contains("protected internal partial class ProtectedInternalGateway", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Generator skips malformed table data gateway type arguments")] + [Theory] + [InlineData("null!", "typeof(string)")] + [InlineData("typeof(OrderRow)", "null!")] + public Task Generator_Skips_Malformed_Table_Data_Gateway_Type_Arguments(string rowType, string keyType) + => Given("a table data gateway declaration with a null type argument", () => Compile($$""" + using PatternKit.Generators.TableDataGateway; + + public sealed record OrderRow(string OrderId); + + [GenerateTableDataGateway({{rowType}}, {{keyType}})] + public static partial class OrderTableGateway + { + [TableGatewayKeySelector] + private static string SelectKey(OrderRow row) => row.OrderId; + } + """)) + .Then("no source is generated", result => + ScenarioExpect.Empty(result.GeneratedSources)) + .AssertPassed(); + private static GeneratorResult Compile(string source) { var compilation = RoslynTestHelpers.CreateCompilation( source, "TableDataGatewayGeneratorTests", extra: MetadataReference.CreateFromFile(typeof(InMemoryTableDataGateway<,>).Assembly.Location)); - _ = RoslynTestHelpers.Run(compilation, new TableDataGatewayGenerator(), out var run, out _); + _ = RoslynTestHelpers.Run(compilation, new TableDataGatewayGenerator(), out var run, out var updated); var result = run.Results.Single(); - return new GeneratorResult(result.Diagnostics.ToArray(), result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray()); + var emit = updated.Emit(Stream.Null); + return new GeneratorResult( + result.Diagnostics.ToArray(), + result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray(), + emit.Success, + emit.Diagnostics.Select(static diagnostic => diagnostic.ToString()).ToArray()); } - private sealed record GeneratorResult(IReadOnlyList Diagnostics, IReadOnlyList GeneratedSources); + private sealed record GeneratorResult( + IReadOnlyList Diagnostics, + IReadOnlyList GeneratedSources, + bool EmitSuccess, + IReadOnlyList EmitDiagnostics); }