Skip to content

Commit 85fa569

Browse files
authored
test: harden composer generator coverage (#481)
1 parent 6c0f5cf commit 85fa569

3 files changed

Lines changed: 150 additions & 5 deletions

File tree

docs/generators/composer.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ Main attribute for marking pipeline host types.
116116
|---|---|---|---|
117117
| `InvokeMethodName` | `string` | `"Invoke"` | Name of generated sync method |
118118
| `InvokeAsyncMethodName` | `string` | `"InvokeAsync"` | Name of generated async method |
119-
| `GenerateAsync` | `bool?` | `null` | Explicit async control; null = infer from steps |
119+
| `GenerateAsync` | `bool` | Inferred when omitted | Explicit async control |
120120
| `ForceAsync` | `bool` | `false` | Force async generation even if all steps are sync |
121121
| `WrapOrder` | `ComposerWrapOrder` | `OuterFirst` | Determines wrapping order |
122122

src/PatternKit.Generators.Abstractions/Composer/ComposerAttribute.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ public sealed class ComposerAttribute : Attribute
2121

2222
/// <summary>
2323
/// Gets or sets whether to generate async methods.
24-
/// When null (default), async generation is inferred from the presence of async steps or terminal.
25-
/// Note: Nullable bool in attributes is non-standard but supported by C#.
26-
/// Set to true/false explicitly to control async generation, or leave unset for inference.
24+
/// When omitted, async generation is inferred from the presence of async steps or terminal.
25+
/// Set to true/false explicitly to control async generation.
2726
/// </summary>
28-
public bool? GenerateAsync { get; set; }
27+
public bool GenerateAsync { get; set; }
2928

3029
/// <summary>
3130
/// Gets or sets whether to force async generation even if all steps are synchronous.

test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,4 +1111,150 @@ private Response Terminal(in Request req)
11111111
ScenarioExpect.Equal("PKCOM006", diagnostic.Id);
11121112
ScenarioExpect.Contains("Step", diagnostic.GetMessage(), StringComparison.Ordinal);
11131113
}
1114+
1115+
[Scenario("GenerateAsyncFalse WithAsyncStep ReportsDiagnostic")]
1116+
[Fact]
1117+
public void GenerateAsyncFalse_WithAsyncStep_ReportsDiagnostic()
1118+
{
1119+
var source = """
1120+
using System;
1121+
using System.Threading.Tasks;
1122+
using PatternKit.Generators.Composer;
1123+
1124+
public readonly record struct Request(string Path);
1125+
public readonly record struct Response(int Status);
1126+
1127+
[Composer(GenerateAsync = false)]
1128+
public partial class RequestPipeline
1129+
{
1130+
[ComposeStep(0)]
1131+
private ValueTask<Response> StepAsync(Request req, Func<Request, ValueTask<Response>> next)
1132+
=> next(req);
1133+
1134+
[ComposeTerminal]
1135+
private Response Terminal(in Request req) => new(200);
1136+
}
1137+
""";
1138+
1139+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateAsyncFalse_WithAsyncStep_ReportsDiagnostic));
1140+
var gen = new ComposerGenerator();
1141+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
1142+
1143+
var diagnostic = ScenarioExpect.Single(result.Results.SelectMany(r => r.Diagnostics));
1144+
ScenarioExpect.Equal("PKCOM008", diagnostic.Id);
1145+
ScenarioExpect.Contains("StepAsync", diagnostic.GetMessage(), StringComparison.Ordinal);
1146+
}
1147+
1148+
[Scenario("NamedOrderAndIgnoredStep GenerateDeterministicPipeline")]
1149+
[Fact]
1150+
public void NamedOrderAndIgnoredStep_GenerateDeterministicPipeline()
1151+
{
1152+
var source = """
1153+
using System;
1154+
using PatternKit.Generators.Composer;
1155+
1156+
public readonly record struct Request(string Path);
1157+
public readonly record struct Response(int Status);
1158+
1159+
[Composer]
1160+
public partial class RequestPipeline
1161+
{
1162+
[ComposeStep(99, Order = 2, Name = "Audit")]
1163+
private Response Audit(in Request req, Func<Request, Response> next) => next(req);
1164+
1165+
[ComposeStep(1)]
1166+
private Response Auth(in Request req, Func<Request, Response> next) => next(req);
1167+
1168+
[ComposeStep(0)]
1169+
[ComposeIgnore]
1170+
private Response Ignored(in Request req, Func<Request, Response> next) => next(req);
1171+
1172+
[ComposeTerminal]
1173+
private Response Terminal(in Request req) => new(200);
1174+
}
1175+
""";
1176+
1177+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(NamedOrderAndIgnoredStep_GenerateDeterministicPipeline));
1178+
var gen = new ComposerGenerator();
1179+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
1180+
1181+
ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));
1182+
1183+
var generatedSource = result.Results.SelectMany(r => r.GeneratedSources).Single().SourceText.ToString();
1184+
ScenarioExpect.Contains("Audit(in arg, pipeline)", generatedSource);
1185+
ScenarioExpect.Contains("Auth(in arg, pipeline)", generatedSource);
1186+
ScenarioExpect.DoesNotContain("Ignored", generatedSource);
1187+
1188+
var emit = updated.Emit(Stream.Null);
1189+
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
1190+
}
1191+
1192+
[Scenario("StructAsyncTerminalWithoutCancellationToken GeneratesAsyncPipeline")]
1193+
[Fact]
1194+
public void StructAsyncTerminalWithoutCancellationToken_GeneratesAsyncPipeline()
1195+
{
1196+
var source = """
1197+
using System;
1198+
using System.Threading.Tasks;
1199+
using PatternKit.Generators.Composer;
1200+
1201+
public readonly record struct Request(string Path);
1202+
public readonly record struct Response(int Status);
1203+
1204+
[Composer]
1205+
public partial struct RequestPipeline
1206+
{
1207+
[ComposeStep(0)]
1208+
private Response Audit(in Request req, Func<Request, Response> next) => next(req);
1209+
1210+
[ComposeTerminal]
1211+
private ValueTask<Response> TerminalAsync(Request req)
1212+
=> new(new Response(200));
1213+
}
1214+
""";
1215+
1216+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StructAsyncTerminalWithoutCancellationToken_GeneratesAsyncPipeline));
1217+
var gen = new ComposerGenerator();
1218+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
1219+
1220+
ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));
1221+
1222+
var generatedSource = result.Results.SelectMany(r => r.GeneratedSources).Single().SourceText.ToString();
1223+
ScenarioExpect.Contains("terminalFunc", generatedSource);
1224+
ScenarioExpect.Contains("self.TerminalAsync(arg)", generatedSource);
1225+
1226+
var emit = updated.Emit(Stream.Null);
1227+
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
1228+
}
1229+
1230+
[Scenario("InvalidStep WithFunctionPointerNext ReportsDiagnostic")]
1231+
[Fact]
1232+
public void InvalidStep_WithFunctionPointerNext_ReportsDiagnostic()
1233+
{
1234+
var source = """
1235+
using PatternKit.Generators.Composer;
1236+
1237+
public readonly record struct Request(string Path);
1238+
public readonly record struct Response(int Status);
1239+
1240+
[Composer]
1241+
public partial class RequestPipeline
1242+
{
1243+
[ComposeStep(0)]
1244+
private unsafe Response Step(in Request req, delegate*<Request, Response> next)
1245+
=> new(200);
1246+
1247+
[ComposeTerminal]
1248+
private Response Terminal(in Request req) => new(200);
1249+
}
1250+
""";
1251+
1252+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(InvalidStep_WithFunctionPointerNext_ReportsDiagnostic));
1253+
var gen = new ComposerGenerator();
1254+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
1255+
1256+
var diagnostic = ScenarioExpect.Single(result.Results.SelectMany(r => r.Diagnostics));
1257+
ScenarioExpect.Equal("PKCOM006", diagnostic.Id);
1258+
ScenarioExpect.Contains("Step", diagnostic.GetMessage(), StringComparison.Ordinal);
1259+
}
11141260
}

0 commit comments

Comments
 (0)