diff --git a/src/TUnit.Core.SourceGenerator/CodeGenerators/Equality/PreventCompilationTriggerOnEveryKeystrokeComparer.cs b/src/TUnit.Core.SourceGenerator/CodeGenerators/Equality/PreventCompilationTriggerOnEveryKeystrokeComparer.cs
deleted file mode 100644
index bf9148cd6b3..00000000000
--- a/src/TUnit.Core.SourceGenerator/CodeGenerators/Equality/PreventCompilationTriggerOnEveryKeystrokeComparer.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System.Collections.Immutable;
-using Microsoft.CodeAnalysis;
-
-namespace TUnit.Core.SourceGenerator.CodeGenerators.Equality;
-
-///
-/// Treats two compilations as equal when they would produce the same reference-derived output:
-/// same language, assembly name and metadata references. Syntax-only edits (ordinary keystrokes)
-/// keep the same instances, so they compare equal and the
-/// downstream reference walk is skipped. Adding, removing, rebuilding or editing a reference does not.
-///
-public class PreventCompilationTriggerOnEveryKeystrokeComparer : IEqualityComparer
-{
- public bool Equals(Compilation? x, Compilation? y)
- {
- if (ReferenceEquals(x, y))
- {
- return true;
- }
-
- if (x is null)
- {
- return false;
- }
-
- if (y is null)
- {
- return false;
- }
-
- if (x.GetType() != y.GetType())
- {
- return false;
- }
-
- return x.Language == y.Language
- && x.AssemblyName == y.AssemblyName
- && ReferencesEqual(x.ExternalReferences, y.ExternalReferences)
- && ReferencesEqual(x.DirectiveReferences, y.DirectiveReferences);
- }
-
- public int GetHashCode(Compilation obj)
- {
- unchecked
- {
- return (obj.Language.GetHashCode() * 397) ^ (obj.AssemblyName != null ? obj.AssemblyName.GetHashCode() : 0);
- }
- }
-
- private static bool ReferencesEqual(ImmutableArray x, ImmutableArray y)
- {
- // Syntax-only edits usually keep the same backing array, so this avoids the element walk.
- if (x == y)
- {
- return true;
- }
-
- if (x.Length != y.Length)
- {
- return false;
- }
-
- for (var i = 0; i < x.Length; i++)
- {
- // Hosts reuse reference instances while the referenced file or project is unchanged.
- // A new instance means the reference was added, rebuilt or (for an IDE project
- // reference) its source was edited, which can change the types and dependencies the
- // reference walk selects. Rerun extraction then; AssemblyInfoModel equality keeps the
- // generated source cached when the extracted model is unchanged.
- if (!ReferenceEquals(x[i], y[i]))
- {
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/src/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs b/src/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs
index a9746741288..ff310d78f3f 100644
--- a/src/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs
+++ b/src/TUnit.Core.SourceGenerator/CodeGenerators/InfrastructureGenerator.cs
@@ -1,5 +1,5 @@
+using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
-using TUnit.Core.SourceGenerator.CodeGenerators.Equality;
using TUnit.Core.SourceGenerator.Models;
using TUnit.Core.SourceGenerator.Models.Extracted;
@@ -57,11 +57,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase);
});
- // Extract assembly names as primitives in the transform step
- // This enables proper incremental caching
+ // Extract assembly names as primitives in the transform step. AssemblyInfoModel equality
+ // keeps the generated source cached across keystrokes. No custom Compilation comparer here:
+ // when a comparer reports "equal", the input node keeps the OLD compilation in its table,
+ // pinning it (trees, bound state) until references change. The reference walk is memoized
+ // per driver instead, so syntax-only edits still skip it.
+ var memo = new AssemblyInfoMemo();
var assemblyInfoProvider = context.CompilationProvider
- .WithComparer(new PreventCompilationTriggerOnEveryKeystrokeComparer())
- .Select((compilation, _) => ExtractAssemblyInfo(compilation))
+ .Select((compilation, _) => memo.GetAssemblyInfo(compilation))
.WithTrackingName(ExtractAssemblyInfoStep)
.Combine(enabledProvider);
@@ -77,13 +80,110 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
});
}
+ ///
+ /// Single-slot memo of the reference walk, created per and so owned by one
+ /// generator driver. The walk costs several milliseconds per run on a real test project (about
+ /// 6.5 ms for TUnit.TestProject's 211 references), and in the IDE it would otherwise run on every
+ /// keystroke.
+ ///
+ /// The key is every compilation input the walk reads except source:
+ /// (compared with
+ /// ==, which compares the backing array by reference; syntax-only edits reuse it, while any
+ /// reference change produces a new one), the instance and the
+ /// assembly name. Scripts are never memoized. The one source dependency, a source type shadowing a
+ /// selected type, is re-checked on every hit.
+ ///
+ ///
+ /// The slot only ever describes the latest compilation this driver saw: every miss replaces or
+ /// clears it. It holds that compilation's reference list and options (which the live compilation
+ /// holds anyway), never a or syntax tree, and it dies with the driver.
+ ///
+ ///
+ internal sealed class AssemblyInfoMemo
+ {
+ // Racy by design: concurrent runs may both miss, both extract and both store. Entries are
+ // immutable and each equals a fresh extraction for its key, so the only cost is duplicate work.
+ private volatile Entry? _last;
+
+ public AssemblyInfoModel GetAssemblyInfo(Compilation compilation)
+ {
+ // Script submissions read inputs outside the key (#r directive references, the previous
+ // submission), so they always extract fresh.
+ if (compilation.ScriptCompilationInfo is not null || !compilation.DirectiveReferences.IsEmpty)
+ {
+ _last = null;
+ return ExtractAssemblyInfo(compilation, out _);
+ }
+
+ var last = _last;
+ if (last is not null
+ && last.References == compilation.ExternalReferences
+ && ReferenceEquals(last.Options, compilation.Options)
+ && last.AssemblyName == compilation.AssemblyName
+ && !IsAnyShadowedBySource(compilation, last.SelectedMetadataNames))
+ {
+ return last.Model;
+ }
+
+ var model = ExtractAssemblyInfo(compilation, out var selectedMetadataNames);
+ _last = selectedMetadataNames is null
+ ? null
+ : new Entry(compilation.ExternalReferences, compilation.Options, compilation.AssemblyName, model, selectedMetadataNames);
+ return model;
+ }
+
+ private sealed class Entry(
+ ImmutableArray references,
+ CompilationOptions options,
+ string? assemblyName,
+ AssemblyInfoModel model,
+ string[] selectedMetadataNames)
+ {
+ public ImmutableArray References { get; } = references;
+
+ public CompilationOptions Options { get; } = options;
+
+ public string? AssemblyName { get; } = assemblyName;
+
+ public AssemblyInfoModel Model { get; } = model;
+
+ public string[] SelectedMetadataNames { get; } = selectedMetadataNames;
+ }
+ }
+
+ private static bool IsAnyShadowedBySource(Compilation compilation, string[] selectedMetadataNames)
+ {
+ var sourceAssembly = compilation.Assembly;
+ foreach (var metadataName in selectedMetadataNames)
+ {
+ if (sourceAssembly.GetTypeByMetadataName(metadataName) is not null)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Extracts all needed data as primitives in the transform step.
/// This enables proper incremental caching - the model contains only strings.
+ ///
+ /// The result is memoized by across syntax-only edits, so any
+ /// dependency on source must be reported through (as the
+ /// shadowing check does) so that reuse can re-check it.
+ ///
///
- private static AssemblyInfoModel ExtractAssemblyInfo(Compilation compilation)
+ /// The compilation to inspect.
+ ///
+ /// Metadata names of the selected types, or when a source type shadowed a
+ /// candidate, in which case the result depends on source and must not be memoized.
+ ///
+ private static AssemblyInfoModel ExtractAssemblyInfo(Compilation compilation, out string[]? selectedMetadataNames)
{
var assembliesToLoad = new List();
+ var metadataNames = new List();
+ var shadowedBySource = false;
// Find TUnit.Core assembly - only assemblies referencing this can contain tests
var tunitCoreAssembly = FindTUnitCoreAssembly(compilation);
@@ -122,14 +222,17 @@ private static AssemblyInfoModel ExtractAssemblyInfo(Compilation compilation)
{
if (ShouldLoadAssembly(assembly, compilation))
{
- var publicType = GetFirstUniquePublicType(assembly, compilation);
+ var publicType = GetFirstUniquePublicType(assembly, compilation, out var metadataName, ref shadowedBySource);
if (publicType != null)
{
assembliesToLoad.Add(publicType);
+ metadataNames.Add(metadataName!);
}
}
}
+ selectedMetadataNames = shadowedBySource ? null : [.. metadataNames];
+
return new AssemblyInfoModel
{
AssemblyName = compilation.Assembly.Name,
@@ -276,7 +379,11 @@ private static bool IsLoadableAtRuntime(IAssemblySymbol assembly, Compilation co
/// Gets the first public type from an assembly that can be uniquely resolved by the compilation.
/// This avoids CS0433 errors when multiple assemblies define types with the same fully-qualified name.
///
- private static string? GetFirstUniquePublicType(IAssemblySymbol assembly, Compilation compilation)
+ private static string? GetFirstUniquePublicType(
+ IAssemblySymbol assembly,
+ Compilation compilation,
+ out string? selectedMetadataName,
+ ref bool shadowedBySource)
{
foreach (var type in GetPublicTypesRecursive(assembly.GlobalNamespace))
{
@@ -293,10 +400,12 @@ private static bool IsLoadableAtRuntime(IAssemblySymbol assembly, Compilation co
// GetTypeByMetadataName returns null when the type name is ambiguous
if (SymbolEqualityComparer.Default.Equals(resolvedType, type))
{
+ selectedMetadataName = metadataName;
return type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
// null or different type = ambiguous, try next type
+ shadowedBySource |= IsFromSource(resolvedType, compilation);
}
// Fallback: try generic types if no non-generic unique type was found
@@ -327,13 +436,23 @@ private static bool IsLoadableAtRuntime(IAssemblySymbol assembly, Compilation co
typeName = typeName.Substring(0, genericStart) + openGenericSuffix;
}
+ selectedMetadataName = metadataName;
return typeName;
}
+
+ shadowedBySource |= IsFromSource(resolvedType, compilation);
}
+ selectedMetadataName = null;
return null; // No unique type found, skip this assembly
}
+ // GetTypeByMetadataName returns the compilation's own (source) type when one exists, hiding
+ // any referenced type with the same metadata name.
+ private static bool IsFromSource(INamedTypeSymbol? resolvedType, Compilation compilation) =>
+ resolvedType is not null
+ && SymbolEqualityComparer.Default.Equals(resolvedType.ContainingAssembly, compilation.Assembly);
+
///
/// Gets the full metadata name for a type (e.g., "Namespace.OuterClass+NestedClass").
/// This is the format expected by Compilation.GetTypeByMetadataName().
diff --git a/src/TUnit.Core.SourceGenerator/Properties/AssemblyInfo.cs b/src/TUnit.Core.SourceGenerator/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000000..ddb03cd92b2
--- /dev/null
+++ b/src/TUnit.Core.SourceGenerator/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("TUnit.SourceGenerator.IncrementalTests")]
diff --git a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs
index 2bd2c4f274a..cc67303266c 100644
--- a/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs
+++ b/tests/TUnit.SourceGenerator.IncrementalTests/InfrastructureGeneratorIncrementalTests.cs
@@ -1,3 +1,4 @@
+using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Core.SourceGenerator.CodeGenerators;
@@ -27,9 +28,12 @@ public void EditSource_ShouldNotRegenerate()
var driver1 = TestHelper.GenerateTracked(compilation1);
AssertRunReason(driver1, IncrementalStepRunReason.New);
+ // Syntax-only edits rerun the cheap Select, whose model is unchanged, so the generated source
+ // stays cached. Memo_SyntaxOnlyEdits_ReuseModel checks that the reference walk is skipped.
var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}"));
var driver2 = driver1.RunGenerators(compilation2);
- AssertRunReason(driver2, IncrementalStepRunReason.Cached);
+ AssertRunReason(driver2, IncrementalStepRunReason.Unchanged);
+ TestHelper.AssertSourceOutputsCached(driver2.GetRunResult().Results[0]);
var compilation3 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1",
"""
@@ -40,9 +44,52 @@ public void Test1()
}
""");
var driver3 = driver2.RunGenerators(compilation3);
- AssertRunReason(driver3, IncrementalStepRunReason.Cached);
+ AssertRunReason(driver3, IncrementalStepRunReason.Unchanged);
+ TestHelper.AssertSourceOutputsCached(driver3.GetRunResult().Results[0]);
}
+ [Fact]
+ public void EditSource_DoesNotRetainPreviousCompilation()
+ {
+ var (driver, firstCompilation, latestCompilation) = RunSourceEdits(10);
+
+ for (var i = 0; i < 3; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ // A comparer that reports the new compilation as equal makes the input node keep the old
+ // one, pinning its syntax trees and bound state for as long as the driver lives.
+ Xunit.Assert.False(firstCompilation.IsAlive, "The generator driver kept the first compilation alive after syntax-only edits.");
+ GC.KeepAlive(driver);
+ GC.KeepAlive(latestCompilation);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static (GeneratorDriver Driver, WeakReference FirstCompilation, Compilation LatestCompilation) RunSourceEdits(int edits)
+ {
+ Compilation compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var firstCompilation = new WeakReference(compilation);
+
+ var driver = CSharpGeneratorDriver
+ .Create([new InfrastructureGenerator().AsSourceGenerator()])
+ .RunGenerators(compilation);
+
+ for (var i = 0; i < edits; i++)
+ {
+ compilation = compilation.ReplaceSyntaxTree(
+ compilation.SyntaxTrees.First(),
+ CSharpSyntaxTree.ParseText(DefaultSource + $"\npublic class Edit{i} {{ }}", CSharpParseOptions.Default));
+ driver = driver.RunGenerators(compilation);
+ }
+
+ return (driver, firstCompilation, compilation);
+ }
+
+ private static object? GetExtractedModel(GeneratorDriver driver) =>
+ driver.GetRunResult().Results[0].TrackedSteps[InfrastructureGenerator.ExtractAssemblyInfoStep][0].Outputs[0].Value;
+
[Fact]
public void AddReference_ShouldRegenerate()
{
@@ -96,6 +143,161 @@ public void EditProjectReference_ShouldRegenerate()
AssertRunReason(driver3, IncrementalStepRunReason.Unchanged);
}
+ private const string ShadowingSource = "namespace OtherTestLibrary { public class FirstHooks { } }";
+
+ [Fact]
+ public void FreshDriver_SameReferences_DoesNotShareMemo()
+ {
+ var compilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+
+ // The memo belongs to one driver, so an unrelated driver over the same reference list
+ // extracts fresh instead of reusing another driver's result.
+ var first = GetExtractedModel(TestHelper.GenerateTracked(compilation));
+ var second = GetExtractedModel(TestHelper.GenerateTracked(compilation));
+ Xunit.Assert.NotSame(first, second);
+ Xunit.Assert.Equal(first, second);
+ }
+
+ [Fact]
+ public void EditSource_SourceSensitiveSelectionIsNotReused()
+ {
+ var baseCompilation = CreateWithTwoTypeLibrary();
+ var shadowedCompilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(ShadowingSource));
+
+ // The shadowed run must not memoize its (source-dependent) choice for later compilations.
+ var driver1 = TestHelper.GenerateTracked(shadowedCompilation);
+ Xunit.Assert.Contains("typeof(global::OtherTestLibrary.SecondHooks)", GenerateInfrastructure(driver1));
+
+ var driver2 = driver1.RunGenerators(baseCompilation);
+ AssertRunReason(driver2, IncrementalStepRunReason.Modified);
+ Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver2));
+ }
+
+ [Fact]
+ public void EditSource_ShadowingSelectedType_ShouldRegenerate()
+ {
+ var compilation1 = CreateWithTwoTypeLibrary();
+ var driver1 = TestHelper.GenerateTracked(compilation1);
+ Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver1));
+
+ var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText(ShadowingSource));
+ var driver2 = driver1.RunGenerators(compilation2);
+ AssertRunReason(driver2, IncrementalStepRunReason.Modified);
+ Xunit.Assert.Contains("typeof(global::OtherTestLibrary.SecondHooks)", GenerateInfrastructure(driver2));
+
+ var driver3 = driver2.RunGenerators(compilation1);
+ AssertRunReason(driver3, IncrementalStepRunReason.Modified);
+ Xunit.Assert.Contains("typeof(global::OtherTestLibrary.FirstHooks)", GenerateInfrastructure(driver3));
+ }
+
+ // The driver keeps the previous output instance whenever a rerun produces an equal model, so
+ // whether the walk was skipped is only observable on the memo itself: it returns the same
+ // instance on a hit and a new one after re-extracting.
+
+ [Fact]
+ public void Memo_SyntaxOnlyEdits_ReuseModel()
+ {
+ var memo = new InfrastructureGenerator.AssemblyInfoMemo();
+ var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var model1 = memo.GetAssemblyInfo(compilation1);
+
+ var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}"));
+ Xunit.Assert.Same(model1, memo.GetAssemblyInfo(compilation2));
+
+ var compilation3 = TestHelper.ReplaceMethodDeclaration(compilation1, "Test1",
+ """
+ [Test]
+ public void Test1()
+ {
+ var x = 1;
+ }
+ """);
+ Xunit.Assert.Same(model1, memo.GetAssemblyInfo(compilation3));
+ }
+
+ [Fact]
+ public void Memo_ReferenceChange_ReExtracts()
+ {
+ var memo = new InfrastructureGenerator.AssemblyInfoMemo();
+ var compilation1 = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var model1 = memo.GetAssemblyInfo(compilation1);
+
+ var compilation2 = compilation1.AddReferences(CreateHookLibrary(compilation1, "SharedHooks").ToMetadataReference());
+ var model2 = memo.GetAssemblyInfo(compilation2);
+ Xunit.Assert.NotSame(model1, model2);
+ Xunit.Assert.NotEqual(model1, model2);
+ }
+
+ [Fact]
+ public void Memo_OptionsOrAssemblyNameChange_ReExtracts()
+ {
+ var memo = new InfrastructureGenerator.AssemblyInfoMemo();
+ var baseCompilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var baseModel = memo.GetAssemblyInfo(baseCompilation);
+
+ // Same reference list, but options can change how references bind to assembly symbols,
+ // so a new options instance must not reuse the model extracted under the old one.
+ var withComparer = baseCompilation.WithOptions(
+ baseCompilation.Options.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default));
+ Xunit.Assert.True(baseCompilation.ExternalReferences == withComparer.ExternalReferences);
+ var comparerModel = memo.GetAssemblyInfo(withComparer);
+ Xunit.Assert.NotSame(baseModel, comparerModel);
+ Xunit.Assert.Equal(baseModel, comparerModel);
+
+ var withImportOptions = baseCompilation.WithOptions(
+ baseCompilation.Options.WithMetadataImportOptions(MetadataImportOptions.All));
+ var importModel = memo.GetAssemblyInfo(withImportOptions);
+ Xunit.Assert.NotSame(comparerModel, importModel);
+
+ // Syntax-only edits under the new options reuse the memo again.
+ Xunit.Assert.Same(importModel, memo.GetAssemblyInfo(
+ withImportOptions.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}"))));
+
+ var renamed = withImportOptions.WithAssemblyName("Renamed");
+ Xunit.Assert.True(withImportOptions.ExternalReferences == renamed.ExternalReferences);
+ var renamedModel = memo.GetAssemblyInfo(renamed);
+ Xunit.Assert.NotSame(importModel, renamedModel);
+ Xunit.Assert.Equal("Renamed", renamedModel.AssemblyName);
+ }
+
+ [Fact]
+ public void Memo_ScriptCompilation_IsNeverMemoized()
+ {
+ var memo = new InfrastructureGenerator.AssemblyInfoMemo();
+ var baseCompilation = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var baseModel = memo.GetAssemblyInfo(baseCompilation);
+
+ // Script submissions read inputs outside the memo key (#r directives, the previous
+ // submission and the internal ReferencesSupersedeLowerVersions option), so they always
+ // extract fresh.
+ var script = CSharpCompilation.CreateScriptCompilation(
+ baseCompilation.AssemblyName!,
+ CSharpSyntaxTree.ParseText("var x = 1;", CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)),
+ baseCompilation.ExternalReferences);
+ Xunit.Assert.True(baseCompilation.ExternalReferences == script.ExternalReferences);
+
+ var first = memo.GetAssemblyInfo(script);
+ var second = memo.GetAssemblyInfo(script.ReplaceSyntaxTree(
+ script.SyntaxTrees.Single(),
+ CSharpSyntaxTree.ParseText("var y = 2;", CSharpParseOptions.Default.WithKind(SourceCodeKind.Script))));
+ Xunit.Assert.NotSame(baseModel, first);
+ Xunit.Assert.NotSame(first, second);
+ }
+
+ private static CSharpCompilation CreateWithTwoTypeLibrary()
+ {
+ var consumer = Fixture.CreateLibrary(CSharpSyntaxTree.ParseText(DefaultSource, CSharpParseOptions.Default));
+ var library = CSharpCompilation.Create(
+ "OtherTestLibrary",
+ [CSharpSyntaxTree.ParseText("namespace OtherTestLibrary { public class FirstHooks { [TUnit.Core.Before(TUnit.Core.HookType.Assembly)] public static void Setup() { } } public class SecondHooks { } }")],
+ consumer.References,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ return consumer.AddReferences(library.ToMetadataReference());
+ }
+
+ private static string GenerateInfrastructure(GeneratorDriver driver) =>
+ driver.GetRunResult().GeneratedTrees.Single(t => t.FilePath.EndsWith("TUnitInfrastructure.g.cs")).ToString();
+
private static CSharpCompilation CreateHookLibrary(Compilation consumer, string typeName) =>
CSharpCompilation.Create(
"OtherTestLibrary",