Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions src/PatternKit.Generators/Bulkhead/BulkheadPolicyGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -100,25 +101,61 @@ private static string GenerateSource(
sb.AppendLine();
}

var containingTypes = GetContainingTypes(type);
var indentLevel = 0;
foreach (var containingType in containingTypes)
{
AppendTypeDeclaration(sb, containingType, indentLevel);
Comment on lines +104 to +108
sb.AppendLine();
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
indentLevel++;
}

AppendTypeDeclaration(sb, type, indentLevel);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.Bulkhead.BulkheadPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.Bulkhead.BulkheadPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")");
sb.Append(bodyIndent).Append(" .WithMaxConcurrency(").Append(maxConcurrency).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.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<INamedTypeSymbol>();
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)
Comment on lines +148 to 152
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.Cloud.Bulkhead.BulkheadPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Cloud.Bulkhead.BulkheadPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")");
sb.Append(" .WithMaxConcurrency(").Append(maxConcurrency).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();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -98,24 +99,60 @@ private static string GenerateSource(
sb.AppendLine();
}

var containingTypes = GetContainingTypes(type);
var indentLevel = 0;
foreach (var containingType in containingTypes)
{
AppendTypeDeclaration(sb, containingType, indentLevel);
Comment on lines +102 to +106
sb.AppendLine();
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
indentLevel++;
}

AppendTypeDeclaration(sb, type, indentLevel);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.RateLimiting.RateLimitPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.RateLimiting.RateLimitPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")");
sb.Append(bodyIndent).Append(" .WithPermitLimit(").Append(permitLimit).AppendLine(")");
sb.Append(bodyIndent).Append(" .WithWindow(global::System.TimeSpan.FromMilliseconds(").Append(windowMilliseconds).AppendLine("))");
sb.Append(bodyIndent).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<INamedTypeSymbol>();
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)
Comment on lines +145 to 149
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.Cloud.RateLimiting.RateLimitPolicy<").Append(resultTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Cloud.RateLimiting.RateLimitPolicy<").Append(resultTypeName).Append(">.Create(\"").Append(Escape(policyName)).AppendLine("\")");
sb.Append(" .WithPermitLimit(").Append(permitLimit).AppendLine(")");
sb.Append(" .WithWindow(global::System.TimeSpan.FromMilliseconds(").Append(windowMilliseconds).AppendLine("))");
sb.AppendLine(" .Build();");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
Expand Down
206 changes: 127 additions & 79 deletions test/PatternKit.Generators.Tests/BulkheadPolicyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,100 +2,148 @@
using Microsoft.CodeAnalysis.CSharp;
using PatternKit.Generators.Bulkhead;
using TinyBDD;
using TinyBDD.Xunit;
using Xunit.Abstractions;

namespace PatternKit.Generators.Tests;

public sealed class BulkheadPolicyGeneratorTests
[Feature("Bulkhead Policy generator")]
public sealed partial class BulkheadPolicyGeneratorTests(ITestOutputHelper output) : TinyBddXunitBase(output)
{
[Scenario("Generates bulkhead policy factory")]
[Fact]
public void GeneratesBulkheadPolicyFactory()
{
var source = """
public Task Generates_Bulkhead_Policy_Factory()
=> Given("a configured bulkhead policy declaration", () => Compile("""
using PatternKit.Generators.Bulkhead;

namespace Demo;

[GenerateBulkheadPolicy(typeof(string), FactoryMethodName = "Build", PolicyName = "fulfillment", MaxConcurrency = 4, MaxQueueLength = 8, QueueTimeoutMilliseconds = 250)]
public static partial class FulfillmentBulkhead;
""";

var comp = CreateCompilation(source, nameof(GeneratesBulkheadPolicyFactory));
var gen = new BulkheadPolicyGenerator();
_ = 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("FulfillmentBulkhead.BulkheadPolicy.g.cs", generated.HintName);
ScenarioExpect.Contains("Build()", text);
ScenarioExpect.Contains("BulkheadPolicy<string>.Create(\"fulfillment\")", text);
ScenarioExpect.Contains(".WithMaxConcurrency(4)", text);
ScenarioExpect.Contains(".WithMaxQueueLength(8)", text);
ScenarioExpect.Contains(".WithQueueTimeout(global::System.TimeSpan.FromMilliseconds(250))", text);

var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}

[Scenario("Reports diagnostic for non-partial bulkhead host")]
"""))
.Then("generated source creates the configured policy", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
var source = ScenarioExpect.Single(result.GeneratedSources);
ScenarioExpect.Equal("FulfillmentBulkhead.BulkheadPolicy.g.cs", source.HintName);
ScenarioExpect.Contains("public static partial class FulfillmentBulkhead", source.Source);
ScenarioExpect.Contains("Build()", source.Source);
ScenarioExpect.Contains("BulkheadPolicy<string>.Create(\"fulfillment\")", source.Source);
ScenarioExpect.Contains(".WithMaxConcurrency(4)", source.Source);
ScenarioExpect.Contains(".WithMaxQueueLength(8)", source.Source);
ScenarioExpect.Contains(".WithQueueTimeout(global::System.TimeSpan.FromMilliseconds(250))", source.Source);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Reports diagnostics for invalid bulkhead declarations")]
[Theory]
[InlineData("public static class BulkheadHost;", "PKBH001")]
[InlineData("public static partial class BulkheadHost;", "PKBH002", "MaxConcurrency = 0")]
[InlineData("public static partial class BulkheadHost;", "PKBH002", "MaxQueueLength = -1")]
[InlineData("public static partial class BulkheadHost;", "PKBH002", "QueueTimeoutMilliseconds = -1")]
public Task Reports_Diagnostics_For_Invalid_Bulkhead_Declarations(string declaration, string diagnosticId, string configuration = "")
=> Given("an invalid bulkhead policy declaration", () => Compile($$"""
using PatternKit.Generators.Bulkhead;
[GenerateBulkheadPolicy(typeof(string){{(string.IsNullOrWhiteSpace(configuration) ? "" : ", " + configuration)}})]
{{declaration}}
"""))
.Then("the expected diagnostic is reported", result =>
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
.AssertPassed();

[Scenario("Generates bulkhead defaults and host shapes")]
[Fact]
public void ReportsDiagnosticForNonPartialBulkheadHost()
{
var source = """
public Task Generates_Bulkhead_Defaults_And_Host_Shapes()
=> Given("bulkhead policy declarations with default names and host shapes", () => Compile("""
using PatternKit.Generators.Bulkhead;

namespace Demo;

[GenerateBulkheadPolicy(typeof(string))]
public static class BulkheadHost;
""";

var diagnostic = RunAndGetSingleDiagnostic(source, nameof(ReportsDiagnosticForNonPartialBulkheadHost));

ScenarioExpect.Equal("PKBH001", diagnostic.Id);
}

[Scenario("Reports diagnostic for invalid bulkhead configuration")]
internal abstract partial class AbstractBulkhead;

[GenerateBulkheadPolicy(typeof(string), PolicyName = "tenant\\\"bulkhead")]
public sealed partial class SealedBulkhead;

[GenerateBulkheadPolicy(typeof(int))]
internal partial struct StructBulkhead;
"""))
.Then("generated sources preserve host shape and configured defaults", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Equal(3, result.GeneratedSources.Count);

var combined = string.Join("\n", result.GeneratedSources.Select(static source => source.Source));
ScenarioExpect.Contains("internal abstract partial class AbstractBulkhead", combined);
ScenarioExpect.Contains("public sealed partial class SealedBulkhead", combined);
ScenarioExpect.Contains("internal partial struct StructBulkhead", combined);
ScenarioExpect.Contains("Create(\"bulkhead\")", combined);
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"bulkhead\")", combined);
ScenarioExpect.Contains(".WithMaxConcurrency(8)", combined);
ScenarioExpect.Contains(".WithMaxQueueLength(0)", combined);
ScenarioExpect.Contains("FromMilliseconds(0)", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generates nested bulkhead host wrappers")]
[Fact]
public void ReportsDiagnosticForInvalidBulkheadConfiguration()
{
var source = """
public Task Generates_Nested_Bulkhead_Host_Wrappers()
=> Given("nested bulkhead policy declarations", () => Compile("""
using PatternKit.Generators.Bulkhead;

namespace Demo;

[GenerateBulkheadPolicy(typeof(string), MaxConcurrency = 0)]
public static partial class BulkheadHost;
""";

var diagnostic = RunAndGetSingleDiagnostic(source, nameof(ReportsDiagnosticForInvalidBulkheadConfiguration));

ScenarioExpect.Equal("PKBH002", diagnostic.Id);
}

[Scenario("Generates bulkhead policy factory for global struct host")]
public partial class BulkheadContainer
{
private partial class PrivateHost
{
[GenerateBulkheadPolicy(typeof(string))]
protected partial class ProtectedBulkhead;

[GenerateBulkheadPolicy(typeof(string))]
private protected partial class PrivateProtectedBulkhead;

[GenerateBulkheadPolicy(typeof(string))]
protected internal partial class ProtectedInternalBulkhead;
}
}
"""))
.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.Select(static source => source.Source));
ScenarioExpect.Contains("public partial class BulkheadContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedBulkhead", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedBulkhead", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalBulkhead", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Skips malformed bulkhead result type")]
[Fact]
public void GeneratesBulkheadPolicyFactoryForGlobalStructHost()
{
var source = """
public Task Skips_Malformed_Bulkhead_Result_Type()
=> Given("a bulkhead policy declaration with a null result type", () => Compile("""
using PatternKit.Generators.Bulkhead;
[GenerateBulkheadPolicy(null!)]
public static partial class BulkheadHost;
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

[GenerateBulkheadPolicy(typeof(int), FactoryMethodName = "CreateNumbers", PolicyName = "numbers")]
internal partial struct BulkheadHost;
""";

var comp = CreateCompilation(source, nameof(GeneratesBulkheadPolicyFactoryForGlobalStructHost));
var gen = new BulkheadPolicyGenerator();
_ = 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.Contains("internal partial struct BulkheadHost", text);
ScenarioExpect.Contains("CreateNumbers()", text);
ScenarioExpect.DoesNotContain("namespace Demo;", text);
ScenarioExpect.True(updated.Emit(Stream.Null).Success);
private static GeneratorResult Compile(string source)
{
var compilation = CreateCompilation(source, "BulkheadPolicyGeneratorTests");
_ = RoslynTestHelpers.Run(compilation, new BulkheadPolicyGenerator(), 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 CSharpCompilation CreateCompilation(string source, string assemblyName)
Expand All @@ -113,11 +161,11 @@ private static string GetAbstractionsAssemblyPath()
Path.GetDirectoryName(typeof(BulkheadPolicyGenerator).Assembly.Location)!,
"PatternKit.Generators.Abstractions.dll");

private static Diagnostic RunAndGetSingleDiagnostic(string source, string assemblyName)
{
var comp = CreateCompilation(source, assemblyName);
var gen = new BulkheadPolicyGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var run, out _);
return ScenarioExpect.Single(run.Results.SelectMany(result => result.Diagnostics));
}
private sealed record GeneratorResult(
IReadOnlyList<Diagnostic> Diagnostics,
IReadOnlyList<GeneratedSource> GeneratedSources,
bool EmitSuccess,
IReadOnlyList<string> EmitDiagnostics);

private sealed record GeneratedSource(string HintName, string Source);
}
Loading
Loading