diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet8_0.verified.txt
new file mode 100644
index 0000000000..e0e2a7e2e9
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet8_0.verified.txt
@@ -0,0 +1,18 @@
+[
+//
+#pragma warning disable
+namespace AssemblyLoaderTests;
+
+public static class AssemblyLoader
+{
+ [global::System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize()
+ {
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.SequencePosition).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Xml.XmlNamedNodeMap).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Xml.Linq.XAttribute).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::TUnit.Core.AsyncEvent<>).Assembly);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet9_0.verified.txt
new file mode 100644
index 0000000000..556c15a76a
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.DotNet9_0.verified.txt
@@ -0,0 +1,19 @@
+[
+//
+#pragma warning disable
+namespace AssemblyLoaderTests;
+
+public static class AssemblyLoader
+{
+ [global::System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize()
+ {
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.SequencePosition).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Diagnostics.ActivityChangedEventArgs).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Xml.XmlNamedNodeMap).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Xml.Linq.XAttribute).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::TUnit.Core.AsyncEvent<>).Assembly);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt
new file mode 100644
index 0000000000..b099a991be
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.Test.Net4_7.verified.txt
@@ -0,0 +1,16 @@
+[
+//
+#pragma warning disable
+namespace AssemblyLoaderTests;
+
+public static class AssemblyLoader
+{
+ [global::System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize()
+ {
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::System.Runtime.CompilerServices.AsyncMethodBuilderAttribute).Assembly);
+ global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof(global::TUnit.Core.AsyncEvent<>).Assembly);
+ }
+}
+
+]
\ No newline at end of file
diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.cs b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.cs
new file mode 100644
index 0000000000..295d5415b8
--- /dev/null
+++ b/TUnit.Core.SourceGenerator.Tests/AssemblyLoaderTests.cs
@@ -0,0 +1,20 @@
+using TUnit.Core.SourceGenerator.CodeGenerators;
+using TUnit.Core.SourceGenerator.Tests.Options;
+
+namespace TUnit.Core.SourceGenerator.Tests;
+
+internal class AssemblyLoaderTests : TestsBase
+{
+ [Test]
+ public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName,
+ "TUnit.TestProject",
+ "BasicTests.cs"),
+ new RunTestOptions()
+ {
+ VerifyConfigurator = verify => verify.UniqueForTargetFrameworkAndVersion()
+ },
+ async generatedFiles =>
+ {
+ await Assert.That(generatedFiles.Length).IsEqualTo(1);
+ });
+}
\ No newline at end of file
diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs
new file mode 100644
index 0000000000..aedede8c59
--- /dev/null
+++ b/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs
@@ -0,0 +1,131 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using TUnit.Core.SourceGenerator.Extensions;
+
+namespace TUnit.Core.SourceGenerator.CodeGenerators;
+
+[Generator]
+public class AssemblyLoaderGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.CompilationProvider
+ .Select((x, _) => x.GetUsedAssemblyReferences())
+ .WithComparer(new AssemblyComparer())
+ .Combine(context.CompilationProvider);
+
+ context.RegisterSourceOutput(provider, (sourceProductionContext, source) => GenerateCode(sourceProductionContext, source.Right, source.Left));
+ }
+
+ private void GenerateCode(SourceProductionContext context, Compilation compilation, ImmutableArray metadataReferences)
+ {
+ var assemblyReferences = metadataReferences.Where(x => x.Properties.Kind == MetadataImageKind.Assembly);
+
+ var assemblySymbols = assemblyReferences
+ .Select(compilation.GetAssemblyOrModuleSymbol)
+ .OfType()
+ .Where(x => !IsSystemAssembly(x))
+ .ToArray();
+
+ var types = assemblySymbols
+ .Select(x => x.GlobalNamespace)
+ .Select(GetFirstType)
+ .OfType()
+ .Where(x => x.TypeKind is TypeKind.Class or TypeKind.Struct)
+ .ToArray();
+
+ var sourceBuilder = new SourceCodeWriter();
+
+ sourceBuilder.WriteLine("// ");
+ sourceBuilder.WriteLine("#pragma warning disable");
+
+ if(!string.IsNullOrEmpty(compilation.Assembly.Name))
+ {
+ sourceBuilder.WriteLine($"namespace {compilation.Assembly.Name};");
+ sourceBuilder.WriteLine();
+ }
+
+ sourceBuilder.WriteLine("public static class AssemblyLoader");
+ sourceBuilder.WriteLine("{");
+ sourceBuilder.WriteLine("[global::System.Runtime.CompilerServices.ModuleInitializer]");
+ sourceBuilder.WriteLine("public static void Initialize()");
+ sourceBuilder.WriteLine("{");
+
+ foreach (var type in types)
+ {
+ var typeName = type.GloballyQualifiedNonGeneric();
+
+ if (type.IsGenericType)
+ {
+ typeName += $"<{new string(',', type.TypeParameters.Length - 1)}>";
+ }
+
+ sourceBuilder.WriteLine($"global::TUnit.Core.SourceRegistrar.RegisterAssembly(() => typeof({typeName}).Assembly);");
+ }
+
+ sourceBuilder.WriteLine("}");
+ sourceBuilder.WriteLine("}");
+
+ context.AddSource("AssemblyLoader.g.cs", sourceBuilder.ToString());
+ }
+
+ private static INamedTypeSymbol? GetFirstType(INamespaceSymbol @namespace)
+ {
+ var typeMembers = @namespace.GetTypeMembers()
+ .Where(x => x.DeclaredAccessibility == Accessibility.Public && !x.IsStatic)
+ .ToImmutableArray();
+
+ if (!typeMembers.IsDefaultOrEmpty)
+ {
+ return typeMembers[0];
+ }
+
+ var namespaceMembers = @namespace.GetNamespaceMembers().ToImmutableArray();
+
+ if (!namespaceMembers.IsDefaultOrEmpty)
+ {
+ foreach (var namespaceMember in namespaceMembers)
+ {
+ if (GetFirstType(namespaceMember) is { } namedType)
+ {
+ return namedType;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsSystemAssembly(IAssemblySymbol assemblySymbol)
+ {
+ // Check for well-known public key tokens of system assemblies
+ var publicKeyToken = assemblySymbol.Identity.PublicKeyToken;
+
+ if (publicKeyToken == null)
+ {
+ return false;
+ }
+
+ return publicKeyToken.SequenceEqual(new byte[] { 0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89 }) // .NET Framework
+ || publicKeyToken.SequenceEqual(new byte[] { 0x7c, 0xec, 0x85, 0xd7, 0xbe, 0xa7, 0x79, 0x8e }) // .NET Core
+ || publicKeyToken.SequenceEqual(new byte[] { 0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a }); // mscorlib
+ }
+
+ private class AssemblyComparer : IEqualityComparer>
+ {
+ public bool Equals(ImmutableArray x, ImmutableArray y)
+ {
+ return GetAssemblyNamesString(x).Equals(GetAssemblyNamesString(y));
+ }
+
+ public int GetHashCode(ImmutableArray obj)
+ {
+ return GetAssemblyNamesString(obj).GetHashCode();
+ }
+
+ private static string GetAssemblyNamesString(ImmutableArray metadataReferences)
+ {
+ return string.Join("|", metadataReferences.Select(x => x.Display));
+ }
+ }
+}
\ No newline at end of file
diff --git a/TUnit.Core/SourceRegistrar.cs b/TUnit.Core/SourceRegistrar.cs
index 22ccab1ad2..069a2c0e3b 100644
--- a/TUnit.Core/SourceRegistrar.cs
+++ b/TUnit.Core/SourceRegistrar.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Reflection;
using TUnit.Core.Interfaces.SourceGenerator;
namespace TUnit.Core;
@@ -12,6 +13,15 @@ namespace TUnit.Core;
///
public class SourceRegistrar
{
+ ///
+ /// Registers an assembly loader.
+ ///
+ /// The assembly loader to register.
+ public static void RegisterAssembly(Func assemblyLoader)
+ {
+ Sources.AssemblyLoaders.Enqueue(assemblyLoader);
+ }
+
///
/// Registers a test source.
///
diff --git a/TUnit.Core/Sources.cs b/TUnit.Core/Sources.cs
index 343aa4f953..b07a24f48d 100644
--- a/TUnit.Core/Sources.cs
+++ b/TUnit.Core/Sources.cs
@@ -1,4 +1,6 @@
-using TUnit.Core.Interfaces.SourceGenerator;
+using System.Collections.Concurrent;
+using System.Reflection;
+using TUnit.Core.Interfaces.SourceGenerator;
namespace TUnit.Core;
@@ -7,6 +9,7 @@ namespace TUnit.Core;
#endif
internal static class Sources
{
+ public static readonly ConcurrentQueue> AssemblyLoaders = [];
public static readonly List TestSources = [];
public static readonly List DynamicTestSources = [];
diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs
index 292023e1e5..606c20b39e 100644
--- a/TUnit.Engine/Framework/TUnitTestFramework.cs
+++ b/TUnit.Engine/Framework/TUnitTestFramework.cs
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
+using System.Reflection;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
@@ -45,6 +46,11 @@ public TUnitTestFramework(IExtension extension,
public Task CreateTestSessionAsync(CreateTestSessionContext context)
{
+ while(Sources.AssemblyLoaders.TryDequeue(out var assemblyLoader))
+ {
+ TryLoadAssembly(assemblyLoader);
+ }
+
return Task.FromResult(new CreateTestSessionResult
{
IsSuccess = true
@@ -211,4 +217,16 @@ public async Task CloseTestSessionAsync(CloseTestSession
[
typeof(TestNodeUpdateMessage)
];
+
+ private static void TryLoadAssembly(Func assemblyLoader)
+ {
+ try
+ {
+ assemblyLoader.Invoke();
+ }
+ catch
+ {
+ // ignored
+ }
+ }
}
\ No newline at end of file
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt
index 6cc596d8eb..5f6531ed3a 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet2_0.verified.txt
@@ -738,6 +738,7 @@ namespace TUnit.Core
{
public SourceRegistrar() { }
public static void Register(TUnit.Core.Interfaces.SourceGenerator.ITestSource testSource) { }
+ public static void RegisterAssembly(System.Func assemblyLoader) { }
public static void RegisterAssemblyHookSource(TUnit.Core.Interfaces.SourceGenerator.IAssemblyHookSource testSource) { }
public static void RegisterClassHookSource(TUnit.Core.Interfaces.SourceGenerator.IClassHookSource testSource) { }
public static void RegisterDynamic(TUnit.Core.Interfaces.SourceGenerator.IDynamicTestSource testSource) { }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 096fdd613c..b8bf6a3930 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -808,6 +808,7 @@ namespace TUnit.Core
{
public SourceRegistrar() { }
public static void Register(TUnit.Core.Interfaces.SourceGenerator.ITestSource testSource) { }
+ public static void RegisterAssembly(System.Func assemblyLoader) { }
public static void RegisterAssemblyHookSource(TUnit.Core.Interfaces.SourceGenerator.IAssemblyHookSource testSource) { }
public static void RegisterClassHookSource(TUnit.Core.Interfaces.SourceGenerator.IClassHookSource testSource) { }
public static void RegisterDynamic(TUnit.Core.Interfaces.SourceGenerator.IDynamicTestSource testSource) { }
diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index f9e057e8f1..4385bf8ce5 100644
--- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -808,6 +808,7 @@ namespace TUnit.Core
{
public SourceRegistrar() { }
public static void Register(TUnit.Core.Interfaces.SourceGenerator.ITestSource testSource) { }
+ public static void RegisterAssembly(System.Func assemblyLoader) { }
public static void RegisterAssemblyHookSource(TUnit.Core.Interfaces.SourceGenerator.IAssemblyHookSource testSource) { }
public static void RegisterClassHookSource(TUnit.Core.Interfaces.SourceGenerator.IClassHookSource testSource) { }
public static void RegisterDynamic(TUnit.Core.Interfaces.SourceGenerator.IDynamicTestSource testSource) { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet2_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet2_0.verified.txt
index cc3dddec94..2fbc0fff73 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet2_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet2_0.verified.txt
@@ -1,6 +1,10 @@
[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
namespace TUnit.Playwright
{
+ public static class AssemblyLoader
+ {
+ public static void Initialize() { }
+ }
public class BrowserTest : TUnit.Playwright.PlaywrightTest
{
public BrowserTest() { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index 84460f9e13..bd3cf63c5b 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1,6 +1,11 @@
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
namespace TUnit.Playwright
{
+ public static class AssemblyLoader
+ {
+ [System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize() { }
+ }
public class BrowserTest : TUnit.Playwright.PlaywrightTest
{
public BrowserTest() { }
diff --git a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index d6db8d1678..12661f6562 100644
--- a/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Playwright_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1,6 +1,11 @@
[assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v9.0", FrameworkDisplayName=".NET 9.0")]
namespace TUnit.Playwright
{
+ public static class AssemblyLoader
+ {
+ [System.Runtime.CompilerServices.ModuleInitializer]
+ public static void Initialize() { }
+ }
public class BrowserTest : TUnit.Playwright.PlaywrightTest
{
public BrowserTest() { }
diff --git a/TUnit.PublicAPI/Tests.cs b/TUnit.PublicAPI/Tests.cs
index 24f22fa016..a4166344e6 100644
--- a/TUnit.PublicAPI/Tests.cs
+++ b/TUnit.PublicAPI/Tests.cs
@@ -65,7 +65,7 @@ private StringBuilder Scrub(StringBuilder text)
private string Scrub(string text)
{
- return Scrub(new StringBuilder(text)).ToString();
+ return Scrub(new StringBuilder(text.Replace("\r\n", "\n"))).ToString();
}
diff --git a/TUnit.TestProject/TUnit.TestProject.csproj b/TUnit.TestProject/TUnit.TestProject.csproj
index 805cc86425..d4129e9fd0 100644
--- a/TUnit.TestProject/TUnit.TestProject.csproj
+++ b/TUnit.TestProject/TUnit.TestProject.csproj
@@ -56,10 +56,7 @@
-
-
-
-
+
PreserveNewest
@@ -78,7 +75,20 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file