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
58 changes: 58 additions & 0 deletions tests/SharedTestHelpers/GeneratorTestInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TUnit.Tests.Shared;

/// <summary>
/// Parse options and input checks shared by the source-generator test harnesses.
/// </summary>
/// <remarks>
/// The repository builds with <c>LangVersion=preview</c> (Directory.Build.props). Default parse options reject newer
/// syntax such as collection expressions, so a generator under test would see broken trees and the snapshots would
/// not match a real build (#7162, #7190).
/// </remarks>
internal static class GeneratorTestInput
{
/// <summary>Parse options matching the repository's language version.</summary>
public static CSharpParseOptions ParseOptions { get; } = new(LanguageVersion.Preview);

/// <summary>Parses <paramref name="source"/> with <see cref="ParseOptions"/>.</summary>
public static SyntaxTree Parse(string source, string path = "")
{
return CSharpSyntaxTree.ParseText(source, ParseOptions, path);
}

// Fixtures are not required to compile here: some reference packages or types that the harness
// does not load. Syntax errors and the binding errors that a too-old compiler reports for valid
// newer syntax are different, because the generator then sees a tree a real build never produces
// and the snapshot silently records the wrong output (#7162).
private static readonly HashSet<string> UnsupportedSyntaxDiagnosticIds =
[
"CS0182", // attribute argument must be constant: collection expressions in attributes on Roslyn < 4.8
"CS0225", // params parameter must be an array: params collections on Roslyn < 4.12
"CS8652", // feature is in preview
];

/// <summary>
/// Throws when the input has syntax errors, or binding errors that only a compiler too old for the syntax reports.
/// </summary>
public static void ThrowIfInputCannotBeParsed(Compilation compilation)
{
var errors = compilation.SyntaxTrees
.SelectMany(tree => tree.GetDiagnostics())
.Concat(compilation.GetDeclarationDiagnostics().Where(d => UnsupportedSyntaxDiagnosticIds.Contains(d.Id)))
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => d.ToString())
.Distinct()
.ToArray();

if (errors.Length > 0)
{
throw new InvalidOperationException(
$"The test input does not parse as a real build would:{Environment.NewLine}{string.Join(Environment.NewLine, errors)}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -433,9 +433,9 @@ private static async Task<string> RunGenerator(string userSource, [CallerMemberN
// Polyfill copy compiled into this test assembly is internal — invisible to the synthetic
// GeneratorTest compilation. Inject a public copy so the attribute resolves consistently
// across all TFMs the test multi-targets.
var inputTrees = new List<SyntaxTree> { CSharpSyntaxTree.ParseText(userSource) };
var inputTrees = new List<SyntaxTree> { GeneratorTestInput.Parse(userSource) };
#if NET8_0
inputTrees.Add(CSharpSyntaxTree.ParseText("""
inputTrees.Add(GeneratorTestInput.Parse("""
namespace System.Runtime.CompilerServices;
[System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Constructor | System.AttributeTargets.Property, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute : System.Attribute
Expand All @@ -452,7 +452,11 @@ public sealed class OverloadResolutionPriorityAttribute : System.Attribute
references: References,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var driver = CSharpGeneratorDriver.Create(new ShouldExtensionGenerator());
GeneratorTestInput.ThrowIfInputCannotBeParsed(compilation);

var driver = CSharpGeneratorDriver.Create(
[new ShouldExtensionGenerator().AsSourceGenerator()],
parseOptions: GeneratorTestInput.ParseOptions);
driver = (CSharpGeneratorDriver)driver.RunGeneratorsAndUpdateCompilation(
compilation, out var updatedCompilation, out var diagnostics);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<ItemGroup>
<Compile Include="..\SharedTestHelpers\TestHostReferences.cs" Link="Shared\TestHostReferences.cs" />
<Compile Include="..\SharedTestHelpers\GeneratorTestInput.cs" Link="Shared\GeneratorTestInput.cs" />
</ItemGroup>

<ItemGroup>
Expand All @@ -28,7 +29,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.SourceGenerators.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<!-- Host the generator (built against Roslyn 4.7) on the Roslyn that a current SDK loads.
Roslyn 4.7 cannot bind fixtures that use newer syntax, such as collection expressions in
attribute arguments, so it would snapshot what the generator does with broken input (#7190). -->
<PackageReference Include="Microsoft.CodeAnalysis.Common" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="Sourcy.DotNet">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Assertions.SourceGenerator.Tests.Options;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand Down Expand Up @@ -86,7 +87,7 @@ public Task ConcreteReceiverWithExtraGeneric() => RunTest(
// bugs (mis-paired brackets, wrong default rendering, adjacent generic blocks)
// regardless of specific diagnostic id.
var trees = generatedFiles
.Select(source => CSharpSyntaxTree.ParseText(source))
.Select(source => GeneratorTestInput.Parse(source))
.ToArray();

var compilation = CSharpCompilation.Create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand All @@ -26,7 +27,7 @@ public static (string[] Sources, ImmutableArray<Diagnostic> Diagnostics) Run(str
#else
string[] preprocessorSymbols = [];
#endif
var parseOptions = CSharpParseOptions.Default.WithPreprocessorSymbols(preprocessorSymbols);
var parseOptions = GeneratorTestInput.ParseOptions.WithPreprocessorSymbols(preprocessorSymbols);

var compilation = CSharpCompilation.Create(
nameof(AssertionMethodGeneratorRunner),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand Down Expand Up @@ -60,7 +61,7 @@ public static async Task AssertNoErrors(IEnumerable<string> generatedFiles)
#if NETFRAMEWORK
.Append(NetFrameworkPolyfills)
#endif
.Select(source => CSharpSyntaxTree.ParseText(source))
.Select(source => GeneratorTestInput.Parse(source))
.ToArray();

var compilation = CSharpCompilation.Create(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

/// <summary>
/// The harness must bind fixtures the way a real build does (#7190): newer syntax has to parse and bind on the
/// Roslyn this project hosts, and input a real build would reject has to fail the test instead of being snapshotted.
/// </summary>
public class GeneratorTestInputTests
{
[Test]
public async Task Newer_Syntax_Is_Accepted()
{
var compilation = Compile(
"""
using System;
using System.Collections.Generic;

[AttributeUsage(AttributeTargets.Class)]
public sealed class ValuesAttribute(int[] values) : Attribute
{
public int[] Values { get; } = values;
}

[Values([1, 2, 3])]
public class UsesCollectionExpressionInAttribute
{
public static int Sum(params IEnumerable<int> values)
{
var total = 0;
foreach (var value in values)
{
total += value;
}

return total;
}
}
""");

await Assert.That(() => GeneratorTestInput.ThrowIfInputCannotBeParsed(compilation)).ThrowsNothing();
}

[Test]
public async Task Syntax_Errors_Are_Rejected()
{
var compilation = Compile("public class Broken {");

await Assert.That(() => GeneratorTestInput.ThrowIfInputCannotBeParsed(compilation))
.Throws<InvalidOperationException>();
}

private static CSharpCompilation Compile(string source)
{
return CSharpCompilation.Create(
nameof(GeneratorTestInputTests),
[GeneratorTestInput.Parse(source)],
ReferencesHelper.References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Assertions.SourceGenerator.Tests.Options;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand Down Expand Up @@ -59,7 +60,7 @@ public Task CollectionShapeWrapper_InGlobalNamespace_EmitsValidTypeName() => Run

// The emitted forwarders call internal TUnit.Assertions constructors, so the output only compiles
// inside TUnit.Assertions itself. A syntax check is what catches `global::<global namespace>.X`.
var syntaxErrors = CSharpSyntaxTree.ParseText(generated)
var syntaxErrors = GeneratorTestInput.Parse(generated)
.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => $"{d.Id}: {d.GetMessage()}")
Expand Down Expand Up @@ -90,7 +91,7 @@ public static async Task AssertCompiles(string testDataFile, string[] generatedF

var trees = generatedFiles
.Prepend(source)
.Select(text => CSharpSyntaxTree.ParseText(text))
.Select(text => GeneratorTestInput.Parse(text))
.ToArray();

// TestData files are also compiled into this test assembly, so referencing it would declare
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

<ItemGroup>
<Compile Include="..\SharedTestHelpers\TestHostReferences.cs" Link="Shared\TestHostReferences.cs" />
<Compile Include="..\SharedTestHelpers\GeneratorTestInput.cs" Link="Shared\GeneratorTestInput.cs" />
</ItemGroup>

<ItemGroup>
Expand All @@ -23,7 +24,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.SourceGenerators.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<!-- Host the generator (built against Roslyn 4.7) on the Roslyn that a current SDK loads.
Roslyn 4.7 cannot bind fixtures that use newer syntax, such as collection expressions in
attribute arguments, so it would snapshot what the generator does with broken input (#7190). -->
<PackageReference Include="Microsoft.CodeAnalysis.Common" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" VersionOverride="4.14.0" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
<PackageReference Include="NuGet.Protocol" />
<PackageReference Include="Sourcy.DotNet">
Expand Down
16 changes: 10 additions & 6 deletions tests/TUnit.Assertions.SourceGenerator.Tests/TestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Assertions.SourceGenerator.Tests.Extensions;
using TUnit.Assertions.SourceGenerator.Tests.Options;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand Down Expand Up @@ -90,8 +91,13 @@ ..await Task.WhenAll(runTestOptions.AdditionalFiles.Select(x => File.ReadAllText
// Create an instance of the source generator.
var generator = new TGenerator();

// Parse as the repository builds (LangVersion=preview); generated trees must share these options.
var parseOptions = runTestOptions.PreprocessorSymbols is { Length: > 0 }
? GeneratorTestInput.ParseOptions.WithPreprocessorSymbols(runTestOptions.PreprocessorSymbols)
: GeneratorTestInput.ParseOptions;

// Source generators should be tested using 'GeneratorDriver'.
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions);

if (runTestOptions.BuildProperties != null)
{
Expand All @@ -103,10 +109,6 @@ ..await Task.WhenAll(runTestOptions.AdditionalFiles.Select(x => File.ReadAllText

// To run generators, we can use an empty compilation.

var parseOptions = runTestOptions.PreprocessorSymbols is { Length: > 0 }
? CSharpParseOptions.Default.WithPreprocessorSymbols(runTestOptions.PreprocessorSymbols)
: CSharpParseOptions.Default;

var compilation = CSharpCompilation.Create(
GetType().Name,
[
Expand All @@ -115,7 +117,7 @@ ..await Task.WhenAll(runTestOptions.AdditionalFiles.Select(x => File.ReadAllText
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
)
.WithReferences(ReferencesHelper.References)
.AddSyntaxTrees(additionalSources.Select(x => CSharpSyntaxTree.ParseText(x)));
.AddSyntaxTrees(additionalSources.Select(x => CSharpSyntaxTree.ParseText(x, parseOptions)));

foreach (var additionalPackage in runTestOptions.AdditionalPackages)
{
Expand All @@ -124,6 +126,8 @@ ..await Task.WhenAll(runTestOptions.AdditionalFiles.Select(x => File.ReadAllText
compilation = compilation.AddReferences(downloaded);
}

GeneratorTestInput.ThrowIfInputCannotBeParsed(compilation);

// Run generators. Don't forget to use the new compilation rather than the previous one.
driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ private static Diagnostic[] RunGenerator(string source)
ReferencesHelper.References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

GeneratorDriver driver = CSharpGeneratorDriver.Create(new AssertionMethodGenerator());
GeneratorDriver driver = CSharpGeneratorDriver.Create(
[new AssertionMethodGenerator().AsSourceGenerator()],
parseOptions: ParseOptions);
driver = driver.RunGenerators(compilation);

return driver.GetRunResult().Diagnostics.ToArray();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Assertions.SourceGenerator.Generators;
using TUnit.Tests.Shared;

namespace TUnit.Assertions.SourceGenerator.Tests;

Expand Down Expand Up @@ -42,7 +43,7 @@ private Task AssertNoCS1591(string testDataFile) => RunTest(
var trees = generatedFiles
.Select(source => CSharpSyntaxTree.ParseText(
source,
CSharpParseOptions.Default.WithDocumentationMode(DocumentationMode.Diagnose)))
GeneratorTestInput.ParseOptions.WithDocumentationMode(DocumentationMode.Diagnose)))
.ToArray();

var compilation = CSharpCompilation.Create(
Expand All @@ -68,7 +69,7 @@ private Task AssertNoCompilationErrors(string testDataFile) => RunTest(
await Assert.That(generatedFiles).IsNotEmpty();

var trees = generatedFiles
.Select(source => CSharpSyntaxTree.ParseText(source))
.Select(source => GeneratorTestInput.Parse(source))
.ToArray();

var compilation = CSharpCompilation.Create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ public void Tuple((Token Token, int Length) value) { }
var parseOptions = new CSharpParseOptions(LanguageVersion.Preview);
var compilation = CSharpCompilation.Create(
"ObsoleteConverters",
[CSharpSyntaxTree.ParseText(source, parseOptions)],
[
CSharpSyntaxTree.ParseText(source, parseOptions),
#if NETFRAMEWORK
// The record struct needs IsExternalInit, which .NET Framework lacks.
CSharpSyntaxTree.ParseText(ReferencesHelper.NetFrameworkPolyfills, parseOptions),
Comment thread
thomhurst marked this conversation as resolved.
Resolved
#endif
],
ReferencesHelper.References,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

<ItemGroup>
<Compile Include="..\SharedTestHelpers\TestHostReferences.cs" Link="Shared\TestHostReferences.cs" />
<Compile Include="..\SharedTestHelpers\GeneratorTestInput.cs" Link="Shared\GeneratorTestInput.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading