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