Skip to content

Commit a3fe0b9

Browse files
authored
test: cover repository generator hosts (#432)
1 parent 2014641 commit a3fe0b9

2 files changed

Lines changed: 205 additions & 80 deletions

File tree

src/PatternKit.Generators/Repository/RepositoryGenerator.cs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using System.Text;
34
using Microsoft.CodeAnalysis;
@@ -105,21 +106,57 @@ private static string GenerateSource(
105106
sb.AppendLine();
106107
}
107108

109+
var containingTypes = GetContainingTypes(type);
110+
var indentLevel = 0;
111+
foreach (var containingType in containingTypes)
112+
{
113+
AppendTypeDeclaration(sb, containingType, indentLevel);
114+
sb.AppendLine();
115+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
116+
indentLevel++;
117+
}
118+
119+
AppendTypeDeclaration(sb, type, indentLevel);
120+
sb.AppendLine();
121+
var indent = new string(' ', indentLevel * 4);
122+
sb.AppendLine(indent + "{");
123+
var memberIndent = indent + " ";
124+
var bodyIndent = memberIndent + " ";
125+
sb.Append(memberIndent).Append("public static global::PatternKit.Application.Repository.InMemoryRepository<")
126+
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
127+
sb.Append(bodyIndent).Append("=> global::PatternKit.Application.Repository.InMemoryRepository<")
128+
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
129+
sb.AppendLine(indent + "}");
130+
for (var i = containingTypes.Length - 1; i >= 0; i--)
131+
{
132+
sb.AppendLine(new string(' ', i * 4) + "}");
133+
}
134+
135+
return sb.ToString();
136+
}
137+
138+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
139+
{
140+
var containingTypes = new Stack<INamedTypeSymbol>();
141+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
142+
{
143+
containingTypes.Push(current);
144+
}
145+
146+
return containingTypes.ToArray();
147+
}
148+
149+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
150+
{
151+
sb.Append(new string(' ', indentLevel * 4));
108152
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
109153
if (type.IsStatic)
110154
sb.Append("static ");
111155
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
112156
sb.Append("abstract ");
113157
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
114158
sb.Append("sealed ");
115-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
116-
sb.AppendLine("{");
117-
sb.Append(" public static global::PatternKit.Application.Repository.InMemoryRepository<")
118-
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
119-
sb.Append(" => global::PatternKit.Application.Repository.InMemoryRepository<")
120-
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
121-
sb.AppendLine("}");
122-
return sb.ToString();
159+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
123160
}
124161

125162
private static bool IsKeySelector(IMethodSymbol method, INamedTypeSymbol entityType, INamedTypeSymbol keyType)
Lines changed: 160 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
using Microsoft.CodeAnalysis;
2-
using Microsoft.CodeAnalysis.CSharp;
32
using PatternKit.Application.Repository;
43
using PatternKit.Generators.Repository;
54
using TinyBDD;
5+
using TinyBDD.Xunit;
6+
using Xunit.Abstractions;
67

78
namespace PatternKit.Generators.Tests;
89

9-
public sealed class RepositoryGeneratorTests
10+
[Feature("Repository generator")]
11+
public sealed partial class RepositoryGeneratorTests(ITestOutputHelper output) : TinyBddXunitBase(output)
1012
{
11-
[Scenario("Generates repository factory")]
13+
[Scenario("Generator emits repository factory")]
1214
[Fact]
13-
public void GeneratesRepositoryFactory()
14-
{
15-
var source = """
15+
public Task Generator_Emits_Repository_Factory()
16+
=> Given("a valid repository declaration", () => Compile("""
1617
using PatternKit.Generators.Repository;
1718
18-
namespace MyApp;
19+
namespace Demo;
1920
2021
public sealed record Order(string Id);
2122
@@ -25,90 +26,177 @@ public static partial class OrderRepositoryFactory
2526
[RepositoryKeySelector]
2627
private static string SelectKey(Order order) => order.Id;
2728
}
29+
"""))
30+
.Then("generated source creates the repository", result =>
31+
{
32+
ScenarioExpect.Empty(result.Diagnostics);
33+
var source = ScenarioExpect.Single(result.GeneratedSources);
34+
ScenarioExpect.Contains("public static partial class OrderRepositoryFactory", source);
35+
ScenarioExpect.Contains("Build()", source);
36+
ScenarioExpect.Contains("InMemoryRepository<global::Demo.Order, string>.Create(SelectKey).Build()", source);
37+
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
38+
})
39+
.AssertPassed();
40+
41+
[Scenario("Generator reports invalid repository declarations")]
42+
[Theory]
43+
[InlineData("public static class OrderRepositoryFactory { [RepositoryKeySelector] private static string SelectKey(Order order) => order.Id; }", "PKREP001")]
44+
[InlineData("public static partial class OrderRepositoryFactory;", "PKREP002")]
45+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static string One(Order order) => order.Id; [RepositoryKeySelector] private static string Two(Order order) => order.Id; }", "PKREP002")]
46+
[InlineData("public partial class OrderRepositoryFactory { [RepositoryKeySelector] private string SelectKey(Order order) => order.Id; }", "PKREP003")]
47+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static T SelectKey<T>(Order order) => default!; }", "PKREP003")]
48+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static string SelectKey() => string.Empty; }", "PKREP003")]
49+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static string SelectKey(Order order, string tenant) => order.Id; }", "PKREP003")]
50+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static string SelectKey(string order) => order; }", "PKREP003")]
51+
[InlineData("public static partial class OrderRepositoryFactory { [RepositoryKeySelector] private static int SelectKey(Order order) => 1; }", "PKREP003")]
52+
public Task Generator_Reports_Invalid_Repository_Declarations(string declaration, string diagnosticId)
53+
=> Given("an invalid repository declaration", () => Compile($$"""
54+
using PatternKit.Generators.Repository;
55+
public sealed record Order(string Id);
56+
[GenerateRepository(typeof(Order), typeof(string))]
57+
{{declaration}}
58+
"""))
59+
.Then("the expected diagnostic is reported", result =>
60+
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
61+
.AssertPassed();
2862

29-
public static class Demo
30-
{
31-
public static object Run() => OrderRepositoryFactory.Build();
32-
}
33-
""";
34-
35-
var comp = CreateCompilation(source, nameof(GeneratesRepositoryFactory));
36-
var gen = new RepositoryGenerator();
37-
_ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);
38-
39-
ScenarioExpect.All(run.Results, result => ScenarioExpect.Empty(result.Diagnostics));
40-
var generated = ScenarioExpect.Single(run.Results.SelectMany(result => result.GeneratedSources));
41-
ScenarioExpect.Equal("OrderRepositoryFactory.Repository.g.cs", generated.HintName);
42-
var text = generated.SourceText.ToString();
43-
ScenarioExpect.Contains("Build", text);
44-
ScenarioExpect.Contains("InMemoryRepository<global::MyApp.Order, string>.Create(SelectKey).Build()", text);
45-
ScenarioExpect.True(updated.Emit(Stream.Null).Success);
46-
}
47-
48-
[Scenario("Reports diagnostic for non partial repository")]
63+
[Scenario("Generator emits repository defaults and type shapes")]
4964
[Fact]
50-
public void ReportsDiagnosticForNonPartialRepository()
51-
{
52-
var source = """
65+
public Task Generator_Emits_Repository_Defaults_And_Type_Shapes()
66+
=> Given("repository declarations using default names and different host shapes", () => Compile("""
5367
using PatternKit.Generators.Repository;
68+
69+
namespace Demo;
70+
5471
public sealed record Order(string Id);
55-
[GenerateRepository(typeof(Order), typeof(string))]
56-
public static class OrderRepositoryFactory;
57-
""";
5872
59-
var comp = CreateCompilation(source, nameof(ReportsDiagnosticForNonPartialRepository));
60-
var gen = new RepositoryGenerator();
61-
_ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
73+
[GenerateRepository(typeof(Order), typeof(string))]
74+
internal abstract partial class AbstractRepositoryFactory
75+
{
76+
[RepositoryKeySelector]
77+
private static string SelectKey(Order order) => order.Id;
78+
}
6279
63-
var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics));
64-
ScenarioExpect.Equal("PKREP001", diagnostic.Id);
65-
}
80+
[GenerateRepository(typeof(Order), typeof(string))]
81+
public sealed partial class SealedRepositoryFactory
82+
{
83+
[RepositoryKeySelector]
84+
private static string SelectKey(Order order) => order.Id;
85+
}
6686
67-
[Scenario("Reports diagnostic for missing repository key selector")]
87+
[GenerateRepository(typeof(Order), typeof(string))]
88+
internal partial struct StructRepositoryFactory
89+
{
90+
[RepositoryKeySelector]
91+
private static string SelectKey(Order order) => order.Id;
92+
}
93+
"""))
94+
.Then("generated sources preserve host shape and default factory names", result =>
95+
{
96+
ScenarioExpect.Empty(result.Diagnostics);
97+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
98+
99+
var combined = string.Join("\n", result.GeneratedSources);
100+
ScenarioExpect.Contains("internal abstract partial class AbstractRepositoryFactory", combined);
101+
ScenarioExpect.Contains("InMemoryRepository<global::Demo.Order, string> Create()", combined);
102+
ScenarioExpect.Contains("public sealed partial class SealedRepositoryFactory", combined);
103+
ScenarioExpect.Contains("internal partial struct StructRepositoryFactory", combined);
104+
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
105+
})
106+
.AssertPassed();
107+
108+
[Scenario("Generator emits nested repository host wrappers")]
68109
[Fact]
69-
public void ReportsDiagnosticForMissingRepositoryKeySelector()
70-
{
71-
var source = """
110+
public Task Generator_Emits_Nested_Repository_Host_Wrappers()
111+
=> Given("nested repository declarations with non-public accessibility", () => Compile("""
72112
using PatternKit.Generators.Repository;
73-
public sealed record Order(string Id);
74-
[GenerateRepository(typeof(Order), typeof(string))]
75-
public static partial class OrderRepositoryFactory;
76-
""";
77113
78-
var comp = CreateCompilation(source, nameof(ReportsDiagnosticForMissingRepositoryKeySelector));
79-
var gen = new RepositoryGenerator();
80-
_ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
114+
namespace Demo;
81115
82-
var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics));
83-
ScenarioExpect.Equal("PKREP002", diagnostic.Id);
84-
}
116+
public sealed record Order(string Id);
85117
86-
[Scenario("Reports diagnostic for invalid repository key selector")]
87-
[Fact]
88-
public void ReportsDiagnosticForInvalidRepositoryKeySelector()
89-
{
90-
var source = """
118+
public partial class RepositoryContainer
119+
{
120+
private partial class PrivateHost
121+
{
122+
[GenerateRepository(typeof(Order), typeof(string))]
123+
protected partial class ProtectedRepository
124+
{
125+
[RepositoryKeySelector]
126+
private static string SelectKey(Order order) => order.Id;
127+
}
128+
129+
[GenerateRepository(typeof(Order), typeof(string))]
130+
private protected partial class PrivateProtectedRepository
131+
{
132+
[RepositoryKeySelector]
133+
private static string SelectKey(Order order) => order.Id;
134+
}
135+
136+
[GenerateRepository(typeof(Order), typeof(string))]
137+
protected internal partial class ProtectedInternalRepository
138+
{
139+
[RepositoryKeySelector]
140+
private static string SelectKey(Order order) => order.Id;
141+
}
142+
}
143+
}
144+
"""))
145+
.Then("generated sources preserve containing partial type wrappers", result =>
146+
{
147+
ScenarioExpect.Empty(result.Diagnostics);
148+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
149+
150+
var combined = string.Join("\n", result.GeneratedSources);
151+
ScenarioExpect.Contains("public partial class RepositoryContainer", combined);
152+
ScenarioExpect.Contains("private partial class PrivateHost", combined);
153+
ScenarioExpect.Contains("protected partial class ProtectedRepository", combined);
154+
ScenarioExpect.Contains("private protected partial class PrivateProtectedRepository", combined);
155+
ScenarioExpect.Contains("protected internal partial class ProtectedInternalRepository", combined);
156+
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
157+
})
158+
.AssertPassed();
159+
160+
[Scenario("Generator skips malformed repository type arguments")]
161+
[Theory]
162+
[InlineData("null!", "typeof(string)")]
163+
[InlineData("typeof(Order)", "null!")]
164+
public Task Generator_Skips_Malformed_Repository_Type_Arguments(string entityType, string keyType)
165+
=> Given("a repository declaration with a null type argument", () => Compile($$"""
91166
using PatternKit.Generators.Repository;
167+
92168
public sealed record Order(string Id);
93-
[GenerateRepository(typeof(Order), typeof(string))]
169+
170+
[GenerateRepository({{entityType}}, {{keyType}})]
94171
public static partial class OrderRepositoryFactory
95172
{
96173
[RepositoryKeySelector]
97-
private static int SelectKey(Order order) => 1;
174+
private static string SelectKey(Order order) => order.Id;
98175
}
99-
""";
100-
101-
var comp = CreateCompilation(source, nameof(ReportsDiagnosticForInvalidRepositoryKeySelector));
102-
var gen = new RepositoryGenerator();
103-
_ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
176+
"""))
177+
.Then("no source is generated", result =>
178+
ScenarioExpect.Empty(result.GeneratedSources))
179+
.AssertPassed();
104180

105-
var diagnostic = ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics));
106-
ScenarioExpect.Equal("PKREP003", diagnostic.Id);
107-
}
108-
109-
private static CSharpCompilation CreateCompilation(string source, string assemblyName)
110-
=> RoslynTestHelpers.CreateCompilation(
181+
private static GeneratorResult Compile(string source)
182+
{
183+
var compilation = RoslynTestHelpers.CreateCompilation(
111184
source,
112-
assemblyName,
185+
"RepositoryGeneratorTests",
113186
extra: MetadataReference.CreateFromFile(typeof(InMemoryRepository<,>).Assembly.Location));
187+
_ = RoslynTestHelpers.Run(compilation, new RepositoryGenerator(), out var run, out var updated);
188+
var result = run.Results.Single();
189+
var emit = updated.Emit(Stream.Null);
190+
return new GeneratorResult(
191+
result.Diagnostics.ToArray(),
192+
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray(),
193+
emit.Success,
194+
string.Join("\n", emit.Diagnostics));
195+
}
196+
197+
private sealed record GeneratorResult(
198+
IReadOnlyList<Diagnostic> Diagnostics,
199+
IReadOnlyList<string> GeneratedSources,
200+
bool EmitSuccess,
201+
string EmitDiagnostics);
114202
}

0 commit comments

Comments
 (0)