Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for unit testing the SGF Source Generators #20

Merged
merged 1 commit into from
Jul 3, 2024
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
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
Loading