diff --git a/README.md b/README.md index 7673549..c702437 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,35 @@ public class MyGenerator : IncrementalGenerator } ``` +## Unit Tests +You can write unit test to validate that your source generators are working as expected. To do this for this library requires a very tiny amount of extra work. You can also look at the [example project](src\Sandbox\ConsoleApp.SourceGenerator.Tests\TestCase.cs) to see how it works. + +The generated class will all be internal so your unit test assembly will need to have viability. + +```csharp +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ConsoleApp.SourceGenerator.Tests")] +``` + +Then for your unit test functions the only difference will be that instead of creating an instance of your class you create an instance of the `Hoist` class. + +```c# +// Create the instance of our generator and set whatever properties +ConsoleAppSourceGenerator generator = new ConsoleAppSourceGenerator() +{ + Explode = true +}; +// Build the instance of the wrapper `Host` which takes in your generator. +ConsoleAppSourceGeneratorHoist host = new + +// From here it's just like testing any other generator +ConsoleAppSourceGeneratorHoist(generator); +GeneratorDriver driver = CSharpGeneratorDriver.Create(host); +driver = driver.RunGenerators(compilation); +``` +The only unique feature is the wrapper class `{YourGeneratorName}Host`. This class is an internal feature of `SGF` and is used to make sure all dependencies are resolved before calling into your source generator. + ## Project Layout This library is made up of quite a few different components leveraging various techniques to help diff --git a/src/Sandbox/ConsoleApp.SourceGenerator.Tests/ConsoleApp.SourceGenerator.Tests.csproj b/src/Sandbox/ConsoleApp.SourceGenerator.Tests/ConsoleApp.SourceGenerator.Tests.csproj new file mode 100644 index 0000000..10f6dd1 --- /dev/null +++ b/src/Sandbox/ConsoleApp.SourceGenerator.Tests/ConsoleApp.SourceGenerator.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + 12 + + false + true + + + + + + + + + + + + + + + + + + diff --git a/src/Sandbox/ConsoleApp.SourceGenerator.Tests/TestCase.cs b/src/Sandbox/ConsoleApp.SourceGenerator.Tests/TestCase.cs new file mode 100644 index 0000000..5da446e --- /dev/null +++ b/src/Sandbox/ConsoleApp.SourceGenerator.Tests/TestCase.cs @@ -0,0 +1,47 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace ConsoleApp.SourceGenerator.Tests +{ + public class TestCase + { + [Fact] + public void Compiles() + { + Compose(""" + namespace MyNamespace + { + public class MyClass + { + } + } + """); + } + + + private void Compose(string source) + { + // Create the generator, you can pass any parameters you want + ConsoleAppSourceGenerator generator = new ConsoleAppSourceGenerator() + { + WarningMessage = "I am running from a unit test!" // Change any settings you want + }; + // Create the 'host' which is the wrapper that is auto generated by SGF. + ConsoleAppSourceGeneratorHoist host = new ConsoleAppSourceGeneratorHoist(generator); + // Parse the source into syntax trees + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + // Setup the compilation settings + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "UniTests", + syntaxTrees: new[] { syntaxTree }); + // Create the driver the executes the generator + GeneratorDriver driver = CSharpGeneratorDriver.Create(host); + // Run it + driver = driver.RunGenerators(compilation); + // Get the results + GeneratorDriverRunResult results = driver.GetRunResult(); + // Test the results + Assert.NotEmpty(results.GeneratedTrees); + } + } +} \ No newline at end of file diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/AssemblyInfo.cs b/src/Sandbox/ConsoleApp.SourceGenerator/AssemblyInfo.cs new file mode 100644 index 0000000..b8821c7 --- /dev/null +++ b/src/Sandbox/ConsoleApp.SourceGenerator/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ConsoleApp.SourceGenerator.Tests")] \ No newline at end of file diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs index 89e56b2..62da2f9 100644 --- a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs +++ b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs @@ -16,9 +16,11 @@ public class Payload public string? Version { get; set; } } + public string WarningMessage { get; set; } + public ConsoleAppSourceGenerator() : base("ConsoleAppSourceGenerator") { - + WarningMessage = "Warnigs show up in the 'Build' pane along with the 'Source Generators' pane"; } public override void OnInitialize(SgfInitializationContext context) @@ -29,7 +31,7 @@ public override void OnInitialize(SgfInitializationContext context) Version = "13.0.1" }; - Logger.Warning("Warnigs show up in the 'Build' pane along with the 'Source Generators' pane"); + Logger.Warning(WarningMessage); Logger.Information("This is the output from the sournce generator assembly ConsoleApp.SourceGenerator"); Logger.Information("This generator references Newtonsoft.Json and it can just be referenced without any other boilerplate"); Logger.Information(JsonConvert.SerializeObject(payload)); diff --git a/src/SourceGenerator.Foundations.sln b/src/SourceGenerator.Foundations.sln index 7c4d601..f2a2f21 100644 --- a/src/SourceGenerator.Foundations.sln +++ b/src/SourceGenerator.Foundations.sln @@ -31,6 +31,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F012 EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SourceGenerator.Foundations.Shared", "SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.shproj", "{8AF3630C-2BF5-4854-A45D-0074C2787964}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp.SourceGenerator.Tests", "Sandbox\ConsoleApp.SourceGenerator.Tests\ConsoleApp.SourceGenerator.Tests.csproj", "{560C8028-2831-4697-9571-A9920FB972E7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,6 +89,14 @@ Global {E95587D6-78E8-48FE-9F98-371800A77B69}.Release|Any CPU.Build.0 = Release|Any CPU {E95587D6-78E8-48FE-9F98-371800A77B69}.Release|x64.ActiveCfg = Release|Any CPU {E95587D6-78E8-48FE-9F98-371800A77B69}.Release|x64.Build.0 = Release|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Debug|x64.ActiveCfg = Debug|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Debug|x64.Build.0 = Debug|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Release|Any CPU.Build.0 = Release|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Release|x64.ActiveCfg = Release|Any CPU + {560C8028-2831-4697-9571-A9920FB972E7}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -95,6 +105,7 @@ Global {DC8C5A1A-6269-4BA7-852A-D21CB0B2B5A0} = {A651676B-FBDB-4710-B010-D05AF3A56084} {F4BA95B9-0353-44CA-9502-C74B532321B7} = {6118BF32-23BA-4D33-946E-F7E8A6F5D758} {594AACB5-B550-46CF-B6E2-16EF826D655A} = {6118BF32-23BA-4D33-946E-F7E8A6F5D758} + {560C8028-2831-4697-9571-A9920FB972E7} = {6118BF32-23BA-4D33-946E-F7E8A6F5D758} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EDB10920-970A-43F9-A2B3-7F1270DD477B} diff --git a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs index d889bdf..543d42d 100644 --- a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs +++ b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs @@ -24,11 +24,23 @@ namespace {{dataModel.Namespace}} internal class {{dataModel.ClassName}}Hoist : SourceGeneratorHoist, IIncrementalGenerator { // Has to be untyped otherwise it will try to resolve at startup - private object? m_generator; + private Lazy m_lazyGenerator; + /// + /// Creates a new generator host that will create an instance of {{dataModel.ClassName}} at runtime. + /// public {{dataModel.ClassName}}Hoist() : base() { - m_generator = null; + m_lazyGenerator = new Lazy(CreateInstance); + } + + /// + /// Creates a new generator host that will reuse an existing instance of {{dataModel.ClassName}} instead of creating one dynamically. + /// This function would only ever be called from unit tests. + /// + public {{dataModel.ClassName}}Hoist({{dataModel.ClassName}} generator): base() + { + m_lazyGenerator = new Lazy(() => generator); } /// @@ -36,24 +48,14 @@ internal class {{dataModel.ClassName}}Hoist : SourceGeneratorHoist, IIncremental /// public void Initialize(IncrementalGeneratorInitializationContext context) { - // The expected arguments types for the generator being created - Type[] typeArguments = new Type[] { }; - - BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - Type generatorType = typeof(global::{{dataModel.QualifedName}}); - ConstructorInfo? constructor = generatorType.GetConstructor(bindingFlags, null, typeArguments, Array.Empty()); + IncrementalGenerator? generator = m_lazyGenerator.Value as IncrementalGenerator; - if(constructor == null) + if(generator == null) { return; } - - object[] constructorArguments = new object[]{}; - IncrementalGenerator generator = (global::{{dataModel.QualifedName}})constructor.Invoke(constructorArguments); ILogger logger = generator.Logger; - - m_generator = generator; try { SgfInitializationContext sgfContext = new(context, logger); @@ -66,9 +68,29 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } } + private object? CreateInstance() + { + // The expected arguments types for the generator being created + Type[] typeArguments = new Type[] { }; + + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + Type generatorType = typeof(global::{{dataModel.QualifedName}}); + ConstructorInfo? constructor = generatorType.GetConstructor(bindingFlags, null, typeArguments, Array.Empty()); + + if(constructor == null) + { + return null; + } + + object[] constructorArguments = new object[]{}; + IncrementalGenerator generator = (global::{{dataModel.QualifedName}})constructor.Invoke(constructorArguments); + + return generator; + } + public void Dispose() { - if(m_generator is IDisposable disposable) + if(m_lazyGenerator.Value is IDisposable disposable) { disposable.Dispose(); }