11using Microsoft . CodeAnalysis ;
2- using Microsoft . CodeAnalysis . CSharp ;
32using PatternKit . Application . Repository ;
43using PatternKit . Generators . Repository ;
54using TinyBDD ;
5+ using TinyBDD . Xunit ;
6+ using Xunit . Abstractions ;
67
78namespace 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