Skip to content

Commit

Permalink
Merge pull request #20 from ByronMayne/generator-host-unit-testing
Browse files Browse the repository at this point in the history
Added support for unit testing the SGF Source Generators
  • Loading branch information
ByronMayne authored Jul 3, 2024
2 parents 66218ee + d874c7a commit 5fc1540
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 17 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ConsoleApp.SourceGenerator\ConsoleApp.SourceGenerator.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
47 changes: 47 additions & 0 deletions src/Sandbox/ConsoleApp.SourceGenerator.Tests/TestCase.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
3 changes: 3 additions & 0 deletions src/Sandbox/ConsoleApp.SourceGenerator/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ConsoleApp.SourceGenerator.Tests")]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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));
Expand Down
11 changes: 11 additions & 0 deletions src/SourceGenerator.Foundations.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,38 @@ 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<object?> m_lazyGenerator;
/// <summary>
/// Creates a new generator host that will create an instance of {{dataModel.ClassName}} at runtime.
/// </summary>
public {{dataModel.ClassName}}Hoist() : base()
{
m_generator = null;
m_lazyGenerator = new Lazy<object?>(CreateInstance);
}
/// <summary>
/// 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.
/// </summary>
public {{dataModel.ClassName}}Hoist({{dataModel.ClassName}} generator): base()
{
m_lazyGenerator = new Lazy<object?>(() => generator);
}
/// <summary>
/// Initializes the source generator to make it simpler to work with
/// </summary>
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<ParameterModifier>());
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);
Expand All @@ -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<ParameterModifier>());
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();
}
Expand Down

0 comments on commit 5fc1540

Please sign in to comment.