Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Microsoft.CodeAnalysis.CSharp;

namespace TUnit.Assertions.SourceGenerator.IncrementalTests;

public class AssertionMethodGeneratorIncrementalTests
{
private const string DefaultAssertion =
"""
#nullable enabled
using System.ComponentModel;
using TUnit.Assertions.Attributes;

public static partial class IntAssertionExtensions
{
[GenerateAssertion(ExpectationMessage = "to be positive")]
public static bool IsPositive(this int value)
{
return value > 0;
}

public static bool IsNegative(this int value)
{
return value < 0;
}
}
""";

[Fact]
public void AddUnrelatedMethodShouldNotRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}"));
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached);
}

[Fact]
public void AddNewTypeAssertionShouldRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText(
"""
using TUnit.Assertions.Attributes;

public static partial class LongAssertionExtensions
{
[GenerateAssertion(ExpectationMessage = "to be positive")]
public static bool IsPositive(this long value)
{
return value > 0;
}
}
"""));
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1);
}

[Fact]
public void AddNewSameTypeAssertionShouldRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "IsNegative",
"""
[GenerateAssertion(ExpectationMessage = "to be less than zero")]
public static bool IsNegative(this int value)
{
return value < 0;
}
"""
);
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1);
}

[Fact]
public void ModifyMessageShouldRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(DefaultAssertion, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

var compilation2 = TestHelper.ReplaceMethodDeclaration(compilation1, "IsPositive",
"""
[GenerateAssertion(ExpectationMessage = "to be more than zero")]
public static bool IsPositive(this int value)
{
return value > 0;
}
"""
);
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified);
}

}
77 changes: 77 additions & 0 deletions TUnit.Assertions.SourceGenerator.IncrementalTests/Fixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.Attributes;

namespace TUnit.Assertions.SourceGenerator.IncrementalTests;

public static class Fixture
{
public static readonly Assembly[] ImportantAssemblies = new[]
{
typeof(object).Assembly,
typeof(Console).Assembly,
typeof(GenerateAssertionAttribute).Assembly,
typeof(MulticastDelegate).Assembly,
typeof(IServiceProvider).Assembly,
};

public static Assembly[] AssemblyReferencesForCodegen =>
AppDomain
.CurrentDomain.GetAssemblies()
.Concat(ImportantAssemblies)
.Distinct()
.Where(a => !a.IsDynamic)
.ToArray();

public static DirectoryInfo GetSolutionDirectoryInfo()
{
var slnDir = SolutionDir();
var directory = new DirectoryInfo(slnDir);
// Assert.True(directory.Exists);
return directory;
}

private static string SolutionDir([CallerFilePath] string thisFilePath = "") =>
Path.GetFullPath(Path.Join(thisFilePath, "../../../"));

public static CSharpCompilation CreateLibrary(params string[] source) =>
CreateLibrary(source.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray());

public static CSharpCompilation CreateLibrary(params SyntaxTree[] source)
{
var references = new List<MetadataReference>();
var assemblies = AssemblyReferencesForCodegen;
foreach (Assembly assembly in assemblies)
{
if (!assembly.IsDynamic)
{
references.Add(MetadataReference.CreateFromFile(assembly.Location));
}
}

var compilation = CSharpCompilation.Create(
"Library",
source,
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

return compilation;
}

public static async Task<string> SourceFromResourceFile(string file)
{
var currentDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
Assert.NotNull(currentDir);
var resourcesDir = Path.Combine(currentDir, "resources");

return await File.ReadAllTextAsync(Path.Combine(resourcesDir, file));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Verify" />
<PackageReference Include="Verify.TUnit" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" VersionOverride="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

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

<ItemGroup>
<ProjectReference Include="..\TUnit.Assertions.SourceGenerator\TUnit.Assertions.SourceGenerator.csproj" />
<ProjectReference Include="..\TUnit.Assertions\TUnit.Assertions.csproj" />
</ItemGroup>
</Project>
149 changes: 149 additions & 0 deletions TUnit.Assertions.SourceGenerator.IncrementalTests/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using TUnit.Assertions.SourceGenerator.Generators;

namespace TUnit.Assertions.SourceGenerator.IncrementalTests;

internal static class TestHelper
{
private static readonly GeneratorDriverOptions EnableIncrementalTrackingDriverOptions = new(
IncrementalGeneratorOutputKind.None,
trackIncrementalGeneratorSteps: true
);

internal static GeneratorDriver GenerateTracked(Compilation compilation)
{
var generator = new MethodAssertionGenerator();

var driver = CSharpGeneratorDriver.Create(
new[] { generator.AsSourceGenerator() },
driverOptions: EnableIncrementalTrackingDriverOptions
);
return driver.RunGenerators(compilation);
}

internal static CSharpCompilation ReplaceMemberDeclaration(
CSharpCompilation compilation,
string memberName,
string newMember
)
{
var syntaxTree = compilation.SyntaxTrees.Single();
var memberDeclaration = syntaxTree
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<TypeDeclarationSyntax>()
.Single(x => x.Identifier.Text == memberName);
var updatedMemberDeclaration = SyntaxFactory.ParseMemberDeclaration(newMember)!;

var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration);
var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree);
}

internal static CSharpCompilation ReplaceLocalDeclaration(
CSharpCompilation compilation,
string variableName,
string newDeclaration
)
{
var syntaxTree = compilation.SyntaxTrees.Single();

var memberDeclaration = syntaxTree
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<LocalDeclarationStatementSyntax>()
.Single(x => x.Declaration.Variables.Any(x => x.Identifier.ToString() == variableName));
var updatedMemberDeclaration = SyntaxFactory.ParseStatement(newDeclaration)!;

var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration);
var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree);
}

internal static CSharpCompilation ReplaceMethodDeclaration(
CSharpCompilation compilation,
string methodName,
string newDeclaration
)
{
var syntaxTree = compilation.SyntaxTrees.Single();

var memberDeclaration = syntaxTree
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.First(x => x.Identifier.Text == methodName);
var updatedMemberDeclaration = SyntaxFactory.ParseMemberDeclaration(newDeclaration)!;

var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration);
var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree);
}


internal static void AssertRunReasons(
GeneratorDriver driver,
IncrementalGeneratorRunReasons reasons,
int outputIndex = 0
)
{
var runResult = driver.GetRunResult().Results[0];

AssertRunReason(runResult, MethodAssertionGenerator.BuildAssertion, reasons.BuildMethodAssertionStep, outputIndex);
}

private static void AssertRunReason(
GeneratorRunResult runResult,
string stepName,
IncrementalStepRunReason expectedStepReason,
int outputIndex
)
{
var actualStepReason = runResult
.TrackedSteps[stepName]
.SelectMany(x => x.Outputs)
.ElementAt(outputIndex)
.Reason;

if (actualStepReason != expectedStepReason)
{
throw new Exception($"Incremental generator step {stepName} at index {outputIndex} failed " +
$"with the expected reason: {expectedStepReason}, with the actual reason: {actualStepReason}.");
}
}
}

internal record IncrementalGeneratorRunReasons(
IncrementalStepRunReason BuildMethodAssertionStep,
IncrementalStepRunReason ReportDiagnosticsStep
)
{
public static readonly IncrementalGeneratorRunReasons New = new(
IncrementalStepRunReason.New,
IncrementalStepRunReason.New
);

public static readonly IncrementalGeneratorRunReasons Cached = new(
// compilation step should always be modified as each time a new compilation is passed
IncrementalStepRunReason.Cached,
IncrementalStepRunReason.Cached
);

public static readonly IncrementalGeneratorRunReasons Modified = Cached with
{
ReportDiagnosticsStep = IncrementalStepRunReason.Modified,
BuildMethodAssertionStep = IncrementalStepRunReason.Modified,
};

public static readonly IncrementalGeneratorRunReasons ModifiedSource = Cached with
{
ReportDiagnosticsStep = IncrementalStepRunReason.Unchanged,
BuildMethodAssertionStep = IncrementalStepRunReason.Modified,
};
}

Loading
Loading