diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c480ad7b4..a83b526eda 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -81,9 +81,9 @@ - - - + + + diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyAfterTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyAfterTests.Test.verified.txt index f4d052aceb..ec6b3bd938 100644 --- a/TUnit.Core.SourceGenerator.Tests/AssemblyAfterTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/AssemblyAfterTests.Test.verified.txt @@ -22,7 +22,7 @@ internal static class AssemblyBase1_AfterAll1_After_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyBase1).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -97,7 +97,7 @@ internal static class AssemblyBase1_AfterEach1_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase1), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase1), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyBase1)].Add( new InstanceHookMethod { @@ -173,7 +173,7 @@ internal static class AssemblyBase2_AfterAll2_After_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyBase2).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -248,7 +248,7 @@ internal static class AssemblyBase2_AfterEach2_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase2), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase2), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyBase2)].Add( new InstanceHookMethod { @@ -324,7 +324,7 @@ internal static class AssemblyBase3_AfterAll3_After_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyBase3).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -399,7 +399,7 @@ internal static class AssemblyBase3_AfterEach3_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase3), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyBase3), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyBase3)].Add( new InstanceHookMethod { @@ -475,7 +475,7 @@ internal static class AssemblyCleanupTests_AfterAllCleanUp_After_Assembly_GUIDIn public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -551,7 +551,7 @@ internal static class AssemblyCleanupTests_AfterAllCleanUpWithContext_After_Asse public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -636,7 +636,7 @@ internal static class AssemblyCleanupTests_AfterAllCleanUp2_After_Assembly_GUIDI public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -712,7 +712,7 @@ internal static class AssemblyCleanupTests_AfterAllCleanUpWithContextAndToken_Af public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests).Assembly; - global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterAssemblyHooks[TestsBase`1_assembly].Add( new AfterAssemblyHookMethod { @@ -803,7 +803,7 @@ internal static class AssemblyCleanupTests_Cleanup_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests)].Add( new InstanceHookMethod { @@ -878,7 +878,7 @@ internal static class AssemblyCleanupTests_Cleanup_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests)].Add( new InstanceHookMethod { @@ -962,7 +962,7 @@ internal static class AssemblyCleanupTests_CleanupWithContext_After_Test_GUIDIni [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests)].Add( new InstanceHookMethod { @@ -1046,7 +1046,7 @@ internal static class AssemblyCleanupTests_CleanupWithContext_After_Test_GUIDIni [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.AssemblyCleanupTests)].Add( new InstanceHookMethod { diff --git a/TUnit.Core.SourceGenerator.Tests/AssemblyBeforeTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/AssemblyBeforeTests.Test.verified.txt index 6341fa3557..592bf7f927 100644 --- a/TUnit.Core.SourceGenerator.Tests/AssemblyBeforeTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/AssemblyBeforeTests.Test.verified.txt @@ -22,7 +22,7 @@ internal static class AssemblyBase1_BeforeAll1_Before_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase1).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -97,7 +97,7 @@ internal static class AssemblyBase1_BeforeEach1_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase1), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase1), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase1)].Add( new InstanceHookMethod { @@ -173,7 +173,7 @@ internal static class AssemblyBase2_BeforeAll2_Before_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase2).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -248,7 +248,7 @@ internal static class AssemblyBase2_BeforeEach2_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase2), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase2), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase2)].Add( new InstanceHookMethod { @@ -324,7 +324,7 @@ internal static class AssemblyBase3_BeforeAll3_Before_Assembly_GUIDInitializer public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase3).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -399,7 +399,7 @@ internal static class AssemblyBase3_BeforeEach3_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase3), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase3), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblyBase3)].Add( new InstanceHookMethod { @@ -475,7 +475,7 @@ internal static class AssemblySetupTests_BeforeAllSetUp_Before_Assembly_GUIDInit public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -551,7 +551,7 @@ internal static class AssemblySetupTests_BeforeAllSetUpWithContext_Before_Assemb public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -636,7 +636,7 @@ internal static class AssemblySetupTests_BeforeAllSetUp2_Before_Assembly_GUIDIni public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -712,7 +712,7 @@ internal static class AssemblySetupTests_BeforeAllSetUpWithContext_Before_Assemb public static void Initialize() { var TestsBase`1_assembly = typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests).Assembly; - global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeAssemblyHooks.GetOrAdd(TestsBase`1_assembly, static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeAssemblyHooks[TestsBase`1_assembly].Add( new BeforeAssemblyHookMethod { @@ -803,7 +803,7 @@ internal static class AssemblySetupTests_Setup_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests)].Add( new InstanceHookMethod { @@ -878,7 +878,7 @@ internal static class AssemblySetupTests_Setup_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests)].Add( new InstanceHookMethod { @@ -962,7 +962,7 @@ internal static class AssemblySetupTests_SetupWithContext_Before_Test_GUIDInitia [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests)].Add( new InstanceHookMethod { @@ -1046,7 +1046,7 @@ internal static class AssemblySetupTests_SetupWithContext_Before_Test_GUIDInitia [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.AssemblySetupTests)].Add( new InstanceHookMethod { diff --git a/TUnit.Core.SourceGenerator.Tests/GlobalStaticAfterEachTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/GlobalStaticAfterEachTests.Test.verified.txt index ade513497d..eb67b6c9cf 100644 --- a/TUnit.Core.SourceGenerator.Tests/GlobalStaticAfterEachTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/GlobalStaticAfterEachTests.Test.verified.txt @@ -21,7 +21,7 @@ internal static class GlobalBase1_AfterEach1_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase1), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase1), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalBase1)].Add( new InstanceHookMethod { @@ -96,7 +96,7 @@ internal static class GlobalBase2_AfterEach2_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase2), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase2), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalBase2)].Add( new InstanceHookMethod { @@ -171,7 +171,7 @@ internal static class GlobalBase3_AfterEach3_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase3), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalBase3), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalBase3)].Add( new InstanceHookMethod { @@ -246,7 +246,7 @@ internal static class GlobalCleanUpTests_CleanUp_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests)].Add( new InstanceHookMethod { @@ -321,7 +321,7 @@ internal static class GlobalCleanUpTests_CleanUp_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests)].Add( new InstanceHookMethod { @@ -405,7 +405,7 @@ internal static class GlobalCleanUpTests_CleanUpWithContext_After_Test_GUIDIniti [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests)].Add( new InstanceHookMethod { @@ -489,7 +489,7 @@ internal static class GlobalCleanUpTests_CleanUpWithContext_After_Test_GUIDIniti [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.AfterTests.GlobalCleanUpTests)].Add( new InstanceHookMethod { diff --git a/TUnit.Core.SourceGenerator.Tests/GlobalStaticBeforeEachTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/GlobalStaticBeforeEachTests.Test.verified.txt index 0941f23f3b..b4f8390141 100644 --- a/TUnit.Core.SourceGenerator.Tests/GlobalStaticBeforeEachTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/GlobalStaticBeforeEachTests.Test.verified.txt @@ -21,7 +21,7 @@ internal static class GlobalBase1_BeforeEach1_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase1), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase1), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalBase1)].Add( new InstanceHookMethod { @@ -96,7 +96,7 @@ internal static class GlobalBase2_BeforeEach2_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase2), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase2), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalBase2)].Add( new InstanceHookMethod { @@ -171,7 +171,7 @@ internal static class GlobalBase3_BeforeEach3_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase3), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalBase3), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalBase3)].Add( new InstanceHookMethod { @@ -246,7 +246,7 @@ internal static class GlobalSetUpTests_SetUp_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests)].Add( new InstanceHookMethod { @@ -321,7 +321,7 @@ internal static class GlobalSetUpTests_SetUp_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests)].Add( new InstanceHookMethod { @@ -405,7 +405,7 @@ internal static class GlobalSetUpTests_SetUpWithContext_Before_Test_GUIDInitiali [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests)].Add( new InstanceHookMethod { @@ -489,7 +489,7 @@ internal static class GlobalSetUpTests_SetUpWithContext_Before_Test_GUIDInitiali [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.BeforeTests.GlobalSetUpTests)].Add( new InstanceHookMethod { diff --git a/TUnit.Core.SourceGenerator.Tests/HooksTests.DisposableFieldTests.verified.txt b/TUnit.Core.SourceGenerator.Tests/HooksTests.DisposableFieldTests.verified.txt index dc6db06f66..e920c70d68 100644 --- a/TUnit.Core.SourceGenerator.Tests/HooksTests.DisposableFieldTests.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/HooksTests.DisposableFieldTests.verified.txt @@ -21,7 +21,7 @@ internal static class DisposableFieldTests_Setup_Before_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.DisposableFieldTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.BeforeTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.DisposableFieldTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.BeforeTestHooks[typeof(global::TUnit.TestProject.DisposableFieldTests)].Add( new InstanceHookMethod { @@ -96,7 +96,7 @@ internal static class DisposableFieldTests_Blah_After_Test_GUIDInitializer [global::System.Runtime.CompilerServices.ModuleInitializer] public static void Initialize() { - global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.DisposableFieldTests), _ => new global::System.Collections.Concurrent.ConcurrentBag()); + global::TUnit.Core.Sources.AfterTestHooks.GetOrAdd(typeof(global::TUnit.TestProject.DisposableFieldTests), static _ => new global::System.Collections.Concurrent.ConcurrentBag()); global::TUnit.Core.Sources.AfterTestHooks[typeof(global::TUnit.TestProject.DisposableFieldTests)].Add( new InstanceHookMethod { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs index 173a21d1a2..02acfca105 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Formatting/TypedConstantFormatter.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.Extensions; @@ -386,10 +387,46 @@ private bool AreValuesEqual(object? enumValue, object? providedValue) private static string EscapeForTestId(string str) { - return str.Replace("\\", "\\\\") - .Replace("\r", "\\r") - .Replace("\n", "\\n") - .Replace("\t", "\\t") - .Replace("\"", "\\\""); + var needsEscape = false; + foreach (var c in str) + { + if (c == '\\' || c == '\r' || c == '\n' || c == '\t' || c == '"') + { + needsEscape = true; + break; + } + } + + if (!needsEscape) + { + return str; + } + + var builder = new StringBuilder(str.Length + 10); + foreach (var c in str) + { + switch (c) + { + case '\\': + builder.Append("\\\\"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\t': + builder.Append("\\t"); + break; + case '"': + builder.Append("\\\""); + break; + default: + builder.Append(c); + break; + } + } + return builder.ToString(); } } diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs index 40deca4e17..935fec6b69 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/DataSourceAttributeHelper.cs @@ -1,5 +1,5 @@ using Microsoft.CodeAnalysis; -using TUnit.Core.SourceGenerator.Extensions; +using TUnit.Core.SourceGenerator.Helpers; namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; @@ -12,10 +12,10 @@ public static bool IsDataSourceAttribute(INamedTypeSymbol? attributeClass) return false; } - // Check if the attribute implements IDataSourceAttribute - return attributeClass.AllInterfaces.Any(i => i.GloballyQualified() == "global::TUnit.Core.IDataSourceAttribute"); + // Check if the attribute implements IDataSourceAttribute (using cache) + return InterfaceCache.ImplementsInterface(attributeClass, "global::TUnit.Core.IDataSourceAttribute"); } - + public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) { if (attributeClass == null) @@ -23,12 +23,10 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) return false; } - // Check if the attribute implements ITypedDataSourceAttribute - return attributeClass.AllInterfaces.Any(i => - i.IsGenericType && - i.ConstructedFrom.GloballyQualified() == "global::TUnit.Core.ITypedDataSourceAttribute`1"); + // Check if the attribute implements ITypedDataSourceAttribute (using cache) + return InterfaceCache.ImplementsGenericInterface(attributeClass, "global::TUnit.Core.ITypedDataSourceAttribute`1"); } - + public static ITypeSymbol? GetTypedDataSourceType(INamedTypeSymbol? attributeClass) { if (attributeClass == null) @@ -36,10 +34,8 @@ public static bool IsTypedDataSourceAttribute(INamedTypeSymbol? attributeClass) return null; } - var typedInterface = attributeClass.AllInterfaces - .FirstOrDefault(i => i.IsGenericType && - i.ConstructedFrom.GloballyQualified() == "global::TUnit.Core.ITypedDataSourceAttribute`1"); - + var typedInterface = InterfaceCache.GetGenericInterface(attributeClass, "global::TUnit.Core.ITypedDataSourceAttribute`1"); + return typedInterface?.TypeArguments.FirstOrDefault(); } } \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs index 1fe8a151a5..38fb99fadc 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs @@ -86,32 +86,32 @@ public static void GenerateInstanceFactory(CodeWriter writer, ITypeSymbol typeSy private static IMethodSymbol? GetPrimaryConstructor(ITypeSymbol typeSymbol) { + // Materialize constructors once to avoid multiple enumerations var constructors = typeSymbol.GetMembers() .OfType() .Where(m => m.MethodKind == MethodKind.Constructor && !m.IsStatic) - .ToList(); + .ToArray(); // First, check for constructors marked with [TestConstructor] - var testConstructorMarked = constructors - .Where(c => c.GetAttributes().Any(a => - a.AttributeClass?.ToDisplayString() == WellKnownFullyQualifiedClassNames.TestConstructorAttribute.WithoutGlobalPrefix)) - .ToList(); - - if (testConstructorMarked.Count > 0) + foreach (var constructor in constructors) { - return testConstructorMarked[0]; + if (constructor.GetAttributes().Any(a => + a.AttributeClass?.ToDisplayString() == WellKnownFullyQualifiedClassNames.TestConstructorAttribute.WithoutGlobalPrefix)) + { + return constructor; + } } // If no [TestConstructor] found, use existing logic - constructors = constructors.OrderByDescending(c => c.Parameters.Length).ToList(); + var orderedConstructors = constructors.OrderByDescending(c => c.Parameters.Length).ToArray(); - if (constructors.Count == 1) + if (orderedConstructors.Length == 1) { - return constructors[0]; + return orderedConstructors[0]; } - var publicConstructors = constructors.Where(c => c.DeclaredAccessibility == Accessibility.Public).ToList(); - return publicConstructors.Count == 1 ? publicConstructors[0] : publicConstructors.FirstOrDefault(); + var publicConstructors = orderedConstructors.Where(c => c.DeclaredAccessibility == Accessibility.Public).ToArray(); + return publicConstructors.Length == 1 ? publicConstructors[0] : publicConstructors.FirstOrDefault(); } private static void GenerateTypedConstructorCall(CodeWriter writer, string className, IMethodSymbol constructor, TestMethodMetadata? testMethod) diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TypedDataSourceOptimizer.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TypedDataSourceOptimizer.cs index c571175423..48f1c2a16b 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TypedDataSourceOptimizer.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/TypedDataSourceOptimizer.cs @@ -10,11 +10,8 @@ internal static class TypedDataSourceOptimizer /// public static bool CanOptimizeTypedDataSource(AttributeData dataSourceAttribute, IMethodSymbol testMethod) { - if (!dataSourceAttribute.IsTypedDataSourceAttribute()) - { - return false; - } - + // GetTypedDataSourceType already checks if it's a typed data source (returns null if not) + // This avoids enumerating AllInterfaces twice var dataSourceType = dataSourceAttribute.GetTypedDataSourceType(); if (dataSourceType == null) { diff --git a/TUnit.Core.SourceGenerator/CodeWriter.cs b/TUnit.Core.SourceGenerator/CodeWriter.cs index bf392288f5..a6efffc07c 100644 --- a/TUnit.Core.SourceGenerator/CodeWriter.cs +++ b/TUnit.Core.SourceGenerator/CodeWriter.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Text; namespace TUnit.Core.SourceGenerator; @@ -13,10 +14,18 @@ public class CodeWriter : ICodeWriter internal int _indentLevel; // Keep old name for compatibility private bool _isNewLine = true; + private static readonly ConcurrentDictionary<(string, int), string> _indentCache = new(); + public CodeWriter(string indentString = " ", bool includeHeader = true) { _indentString = indentString; + for (var i = 0; i <= 10; i++) + { + var key = (_indentString, i); + _indentCache.TryAdd(key, string.Concat(Enumerable.Repeat(_indentString, i))); + } + if (includeHeader) { _builder.AppendLine("// "); @@ -26,7 +35,7 @@ public CodeWriter(string indentString = " ", bool includeHeader = true) } else { - _isNewLine = true; // Fix: Always start at new line state for proper indentation + _isNewLine = true; } } @@ -39,6 +48,15 @@ public ICodeWriter SetIndentLevel(int level) return this; } + /// + /// Gets the cached indentation string for the specified level, building it if necessary. + /// + private string GetIndentation(int level) + { + var key = (_indentString, level); + return _indentCache.GetOrAdd(key, static k => string.Concat(Enumerable.Repeat(k.Item1, k.Item2))); + } + /// /// Appends text to the current line, applying indentation if at the start of a new line. /// @@ -51,7 +69,7 @@ public ICodeWriter Append(string text) if (_isNewLine) { - _builder.Append(string.Concat(Enumerable.Repeat(_indentString, _indentLevel))); + _builder.Append(GetIndentation(_indentLevel)); _isNewLine = false; } _builder.Append(text); diff --git a/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs b/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs index ef50b74657..e13ab939d7 100644 --- a/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/TUnit.Core.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -45,21 +45,21 @@ public static bool IsTypedDataSourceAttribute(this AttributeData? attributeData) public static bool IsNonGlobalHook(this AttributeData attributeData, Compilation compilation) { - return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, - compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.BeforeAttribute - .WithoutGlobalPrefix)) - || SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, - compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.AfterAttribute - .WithoutGlobalPrefix)); + // Cache type symbols to avoid repeated GetTypeByMetadataName calls + var beforeAttribute = compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.BeforeAttribute.WithoutGlobalPrefix); + var afterAttribute = compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.AfterAttribute.WithoutGlobalPrefix); + + return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, beforeAttribute) + || SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, afterAttribute); } public static bool IsGlobalHook(this AttributeData attributeData, Compilation compilation) { - return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, - compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.BeforeEveryAttribute - .WithoutGlobalPrefix)) - || SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, - compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.AfterEveryAttribute - .WithoutGlobalPrefix)); + // Cache type symbols to avoid repeated GetTypeByMetadataName calls + var beforeEveryAttribute = compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.BeforeEveryAttribute.WithoutGlobalPrefix); + var afterEveryAttribute = compilation.GetTypeByMetadataName(WellKnownFullyQualifiedClassNames.AfterEveryAttribute.WithoutGlobalPrefix); + + return SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, beforeEveryAttribute) + || SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, afterEveryAttribute); } } diff --git a/TUnit.Core.SourceGenerator/Extensions/StringExtensions.cs b/TUnit.Core.SourceGenerator/Extensions/StringExtensions.cs index e47921ea91..3eee610ddc 100644 --- a/TUnit.Core.SourceGenerator/Extensions/StringExtensions.cs +++ b/TUnit.Core.SourceGenerator/Extensions/StringExtensions.cs @@ -1,13 +1,12 @@ -using System.Text.RegularExpressions; - -namespace TUnit.Core.SourceGenerator.Extensions; +namespace TUnit.Core.SourceGenerator.Extensions; public static class StringExtensions { public static string ReplaceFirstOccurrence(this string source, string find, string replace) { - var regex = new Regex(Regex.Escape(find)); - return regex.Replace(source, replace, 1); + var place = source.IndexOf(find, StringComparison.Ordinal); + + return place == -1 ? source : source.Remove(place, find.Length).Insert(place, replace); } public static string ReplaceLastOccurrence(this string source, string find, string replace) diff --git a/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs b/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs index 58a62157e6..d4b2266807 100644 --- a/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs +++ b/TUnit.Core.SourceGenerator/Extensions/TypeExtensions.cs @@ -35,32 +35,53 @@ public static string GetMetadataName(this Type type) public static IEnumerable GetMembersIncludingBase(this ITypeSymbol namedTypeSymbol, bool reverse = true) { - var list = new List(); - - var symbol = namedTypeSymbol; - - while (symbol is not null) + if (!reverse) { - if (symbol is IErrorTypeSymbol) + // Forward traversal - yield directly without allocations + var symbol = namedTypeSymbol; + while (symbol is not null && symbol.SpecialType != SpecialType.System_Object) { - throw new Exception($"ErrorTypeSymbol for {symbol.Name} - Have you added any missing file sources to the compilation?"); + if (symbol is IErrorTypeSymbol) + { + throw new Exception($"ErrorTypeSymbol for {symbol.Name} - Have you added any missing file sources to the compilation?"); + } + + foreach (var member in symbol.GetMembers()) + { + yield return member; + } + + symbol = symbol.BaseType; } - if (symbol.SpecialType == SpecialType.System_Object) + yield break; + } + + // Reverse traversal - collect hierarchy, then yield from base to derived + // Use stack to collect types (base to derived), then iterate members in forward order + var typeStack = new Stack(); + var current = namedTypeSymbol; + + while (current is not null && current.SpecialType != SpecialType.System_Object) + { + if (current is IErrorTypeSymbol) { - break; + throw new Exception($"ErrorTypeSymbol for {current.Name} - Have you added any missing file sources to the compilation?"); } - list.AddRange(reverse ? symbol.GetMembers().Reverse() : symbol.GetMembers()); - symbol = symbol.BaseType; + typeStack.Push(current); + current = current.BaseType; } - if (reverse) + // Yield members from base to derived + while (typeStack.Count > 0) { - list.Reverse(); + var type = typeStack.Pop(); + foreach (var member in type.GetMembers()) + { + yield return member; + } } - - return list; } public static IEnumerable GetSelfAndBaseTypes(this INamedTypeSymbol namedTypeSymbol) @@ -109,9 +130,12 @@ public static bool IsIEnumerable(this ITypeSymbol namedTypeSymbol, Compilation c ? [(INamedTypeSymbol) namedTypeSymbol, .. namedTypeSymbol.AllInterfaces] : namedTypeSymbol.AllInterfaces.AsEnumerable(); + // Cache the special type lookup to avoid repeated calls + var enumerableT = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T); + foreach (var enumerable in interfaces .Where(x => x.IsGenericType) - .Where(x => SymbolEqualityComparer.Default.Equals(x.OriginalDefinition, compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)))) + .Where(x => SymbolEqualityComparer.Default.Equals(x.OriginalDefinition, enumerableT))) { innerType = enumerable.TypeArguments[0]; return true; diff --git a/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs index e380fd36ce..c49e6dd639 100644 --- a/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/HookMetadataGenerator.cs @@ -272,7 +272,7 @@ private static void GenerateHookRegistration(CodeWriter writer, HookMethodMetada private static void GenerateInstanceHookRegistration(CodeWriter writer, string dictionaryName, string typeDisplay, HookMethodMetadata hook) { var hookType = GetConcreteHookType(dictionaryName, true); - writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd(typeof({typeDisplay}), _ => new global::System.Collections.Concurrent.ConcurrentBag());"); + writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd(typeof({typeDisplay}), static _ => new global::System.Collections.Concurrent.ConcurrentBag());"); writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}[typeof({typeDisplay})].Add("); writer.Indent(); GenerateHookObject(writer, hook, true); @@ -283,7 +283,7 @@ private static void GenerateInstanceHookRegistration(CodeWriter writer, string d private static void GenerateTypeHookRegistration(CodeWriter writer, string dictionaryName, string typeDisplay, HookMethodMetadata hook) { var hookType = GetConcreteHookType(dictionaryName, false); - writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd(typeof({typeDisplay}), _ => new global::System.Collections.Concurrent.ConcurrentBag());"); + writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd(typeof({typeDisplay}), static _ => new global::System.Collections.Concurrent.ConcurrentBag());"); writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}[typeof({typeDisplay})].Add("); writer.Indent(); GenerateHookObject(writer, hook, false); @@ -295,7 +295,7 @@ private static void GenerateAssemblyHookRegistration(CodeWriter writer, string d { var assemblyVar = assemblyVarName + "_assembly"; var hookType = GetConcreteHookType(dictionaryName, false); - writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd({assemblyVar}, _ => new global::System.Collections.Concurrent.ConcurrentBag());"); + writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}.GetOrAdd({assemblyVar}, static _ => new global::System.Collections.Concurrent.ConcurrentBag());"); writer.AppendLine($"global::TUnit.Core.Sources.{dictionaryName}[{assemblyVar}].Add("); writer.Indent(); GenerateHookObject(writer, hook, false); diff --git a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs index 23084bee35..c25c168c84 100644 --- a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs @@ -457,3 +457,20 @@ internal sealed class PropertyWithDataSourceAttribute public required IPropertySymbol Property { get; init; } public required AttributeData DataSourceAttribute { get; init; } } + +internal sealed class ClassWithDataSourcePropertiesComparer : IEqualityComparer +{ + public bool Equals(ClassWithDataSourceProperties? x, ClassWithDataSourceProperties? y) + { + if (ReferenceEquals(x, y)) return true; + if (x is null || y is null) return false; + + // Compare based on the class symbol - this handles partial classes correctly + return SymbolEqualityComparer.Default.Equals(x.ClassSymbol, y.ClassSymbol); + } + + public int GetHashCode(ClassWithDataSourceProperties obj) + { + return SymbolEqualityComparer.Default.GetHashCode(obj.ClassSymbol); + } +} diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index c620f96855..46bd3dd1ec 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -9,6 +9,7 @@ using TUnit.Core.SourceGenerator.CodeGenerators.Helpers; using TUnit.Core.SourceGenerator.CodeGenerators.Writers; using TUnit.Core.SourceGenerator.Extensions; +using TUnit.Core.SourceGenerator.Helpers; using TUnit.Core.SourceGenerator.Models; namespace TUnit.Core.SourceGenerator.Generators; @@ -1273,17 +1274,8 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper private static bool IsAsyncEnumerable(ITypeSymbol type) { - // Check if the type itself is an IAsyncEnumerable - if (type is INamedTypeSymbol { IsGenericType: true } namedType && - namedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable") - { - return true; - } - - // Check if the type implements IAsyncEnumerable - return type.AllInterfaces.Any(i => - i.IsGenericType && - i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable"); + // Use cached interface check + return InterfaceCache.IsAsyncEnumerable(type); } private static bool IsTask(ITypeSymbol type) @@ -1295,14 +1287,8 @@ private static bool IsTask(ITypeSymbol type) private static bool IsEnumerable(ITypeSymbol type) { - if (type.SpecialType == SpecialType.System_String) - { - return false; - } - - return type.AllInterfaces.Any(i => - i.OriginalDefinition.ToDisplayString() == "System.Collections.IEnumerable" || - (i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable")); + // Use cached interface check (already handles string exclusion) + return InterfaceCache.IsEnumerable(type); } private static void WriteTypedConstant(CodeWriter writer, TypedConstant constant) @@ -2421,6 +2407,32 @@ private static string GenerateTypeReference(INamedTypeSymbol typeSymbol, bool is return $"typeof({fullyQualifiedName})"; } + private static string BuildTypeKey(IEnumerable types) + { + var typesList = types as IList ?? types.ToArray(); + if (typesList.Count == 0) + { + return string.Empty; + } + + var formattedTypes = new string[typesList.Count]; + for (var i = 0; i < typesList.Count; i++) + { + formattedTypes[i] = typesList[i].ToDisplayString(DisplayFormats.FullyQualifiedGenericWithoutGlobalPrefix); + } + return string.Join(",", formattedTypes); + } + + /// + /// Generates code that resolves the type name at runtime (FullName ?? Name). + /// Caches ToDisplayString result to avoid calling it twice. + /// + private static string FormatTypeForRuntimeName(ITypeSymbol type) + { + var typeString = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return $"(typeof({typeString}).FullName ?? typeof({typeString}).Name)"; + } + private static void GenerateGenericTypeInfo(CodeWriter writer, INamedTypeSymbol typeSymbol) { writer.AppendLine("GenericTypeInfo = new global::TUnit.Core.GenericTypeInfo"); @@ -2657,7 +2669,7 @@ private static void GenerateGenericTestWithConcreteTypes( Array.Copy(classTypes, 0, combinedTypes, 0, classTypes.Length); Array.Copy(methodTypes, 0, combinedTypes, classTypes.Length, methodTypes.Length); - var typeKey = string.Join(",", combinedTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(combinedTypes); // Skip if we've already processed this type combination if (!processedTypeCombinations.Add(typeKey)) @@ -2678,7 +2690,7 @@ private static void GenerateGenericTestWithConcreteTypes( } // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", combinedTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", combinedTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, combinedTypes, classAttr); writer.AppendLine(","); } @@ -2715,7 +2727,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (!processedTypeCombinations.Add(typeKey)) @@ -2742,7 +2754,7 @@ private static void GenerateGenericTestWithConcreteTypes( } // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes, argAttr); writer.AppendLine(","); } @@ -2758,7 +2770,7 @@ private static void GenerateGenericTestWithConcreteTypes( var inferredTypes = InferClassTypesFromMethodArguments(testMethod.TypeSymbol, testMethod.MethodSymbol, methodArgAttr, compilation); if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (!processedTypeCombinations.Add(typeKey)) @@ -2778,7 +2790,7 @@ private static void GenerateGenericTestWithConcreteTypes( } // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes, methodArgAttr); writer.AppendLine(","); } @@ -2795,7 +2807,7 @@ private static void GenerateGenericTestWithConcreteTypes( var inferredTypes = InferTypesFromDataSourceAttribute(testMethod.MethodSymbol, dataSourceAttr); if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (!processedTypeCombinations.Add(typeKey)) @@ -2822,7 +2834,7 @@ private static void GenerateGenericTestWithConcreteTypes( } // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes, dataSourceAttr); writer.AppendLine(","); } @@ -2834,7 +2846,7 @@ private static void GenerateGenericTestWithConcreteTypes( var inferredTypes = InferTypesFromTypeInferringAttributes(testMethod.MethodSymbol); if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2843,7 +2855,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (ValidateTypeConstraints(testMethod.MethodSymbol, inferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes); writer.AppendLine(","); } @@ -2864,7 +2876,7 @@ private static void GenerateGenericTestWithConcreteTypes( var inferredTypes = InferClassTypesFromMethodDataSource(compilation, testMethod, mdsAttr); if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2873,7 +2885,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (ValidateClassTypeConstraints(testMethod.TypeSymbol, inferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes); writer.AppendLine(","); } @@ -2888,7 +2900,7 @@ private static void GenerateGenericTestWithConcreteTypes( var typedDataSourceInferredTypes = InferTypesFromTypedDataSourceForClass(testMethod.TypeSymbol, testMethod.MethodSymbol); if (typedDataSourceInferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", typedDataSourceInferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(typedDataSourceInferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2897,7 +2909,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (ValidateClassTypeConstraints(testMethod.TypeSymbol, typedDataSourceInferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", typedDataSourceInferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", typedDataSourceInferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, typedDataSourceInferredTypes); writer.AppendLine(","); } @@ -2918,7 +2930,7 @@ private static void GenerateGenericTestWithConcreteTypes( var inferredTypes = InferTypesFromMethodDataSource(compilation, testMethod, mdsAttr); if (inferredTypes is { Length: > 0 }) { - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2927,7 +2939,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (ValidateTypeConstraints(testMethod.MethodSymbol, inferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes); writer.AppendLine(","); } @@ -2965,7 +2977,7 @@ private static void GenerateGenericTestWithConcreteTypes( { // Combine class types and method types var combinedTypes = classInferredTypes.Concat(methodInferredTypes).ToArray(); - var typeKey = string.Join(",", combinedTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(combinedTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2975,7 +2987,7 @@ private static void GenerateGenericTestWithConcreteTypes( ValidateTypeConstraints(testMethod.MethodSymbol, methodInferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", combinedTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", combinedTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, combinedTypes, argAttr); writer.AppendLine(","); } @@ -2986,7 +2998,7 @@ private static void GenerateGenericTestWithConcreteTypes( else { // For non-generic methods, just use class types - var typeKey = string.Join(",", classInferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(classInferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -2995,7 +3007,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (ValidateClassTypeConstraints(testMethod.TypeSymbol, classInferredTypes)) { // Generate a concrete instantiation for this type combination - writer.AppendLine($"[{string.Join(" + \",\" + ", classInferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", classInferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, classInferredTypes, argAttr); writer.AppendLine(","); } @@ -3111,7 +3123,7 @@ private static void GenerateGenericTestWithConcreteTypes( if (typeArgs.Count > 0) { var inferredTypes = typeArgs.ToArray(); - var typeKey = string.Join(",", inferredTypes.Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""))); + var typeKey = BuildTypeKey(inferredTypes); // Skip if we've already processed this type combination if (processedTypeCombinations.Add(typeKey)) @@ -3121,7 +3133,7 @@ private static void GenerateGenericTestWithConcreteTypes( { // Generate a concrete instantiation for this type combination // Use the same key format as runtime: FullName ?? Name - writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(t => $"(typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).FullName ?? typeof({t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}).Name)"))}] = "); + writer.AppendLine($"[{string.Join(" + \",\" + ", inferredTypes.Select(FormatTypeForRuntimeName))}] = "); GenerateConcreteTestMetadata(writer, compilation, testMethod, className, inferredTypes); writer.AppendLine(","); } diff --git a/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs b/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs new file mode 100644 index 0000000000..0064250efa --- /dev/null +++ b/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs @@ -0,0 +1,99 @@ +using System.Collections.Concurrent; +using Microsoft.CodeAnalysis; +using TUnit.Core.SourceGenerator.Extensions; + +namespace TUnit.Core.SourceGenerator.Helpers; + +/// +/// Caches interface implementation checks to avoid repeated AllInterfaces traversals +/// +internal static class InterfaceCache +{ + private static readonly ConcurrentDictionary<(ITypeSymbol Type, string InterfaceName), bool> _implementsCache = new(TypeStringTupleComparer.Default); + private static readonly ConcurrentDictionary<(ITypeSymbol Type, string GenericInterfacePattern), INamedTypeSymbol?> _genericInterfaceCache = new(TypeStringTupleComparer.Default); + + /// + /// Checks if a type implements a specific interface + /// + public static bool ImplementsInterface(ITypeSymbol type, string fullyQualifiedInterfaceName) + { + return _implementsCache.GetOrAdd((type, fullyQualifiedInterfaceName), key => + key.Type.AllInterfaces.Any(i => i.GloballyQualified() == key.InterfaceName)); + } + + /// + /// Checks if a type implements a generic interface and returns the matching interface symbol + /// + public static INamedTypeSymbol? GetGenericInterface(ITypeSymbol type, string fullyQualifiedGenericPattern) + { + return _genericInterfaceCache.GetOrAdd((type, fullyQualifiedGenericPattern), key => + key.Type.AllInterfaces.FirstOrDefault(i => + i.IsGenericType && + i.ConstructedFrom.GloballyQualified() == key.GenericInterfacePattern)); + } + + /// + /// Checks if a type implements a generic interface + /// + public static bool ImplementsGenericInterface(ITypeSymbol type, string fullyQualifiedGenericPattern) + { + return GetGenericInterface(type, fullyQualifiedGenericPattern) != null; + } + + /// + /// Checks if a type implements IAsyncEnumerable<T> + /// + public static bool IsAsyncEnumerable(ITypeSymbol type) + { + return _implementsCache.GetOrAdd((type, "System.Collections.Generic.IAsyncEnumerable"), key => + { + // Check if the type itself is an IAsyncEnumerable + if (key.Type is INamedTypeSymbol { IsGenericType: true } namedType && + namedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable") + { + return true; + } + + // Check if the type implements IAsyncEnumerable + return key.Type.AllInterfaces.Any(i => + i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable"); + }); + } + + /// + /// Checks if a type implements IEnumerable (excluding string) + /// + public static bool IsEnumerable(ITypeSymbol type) + { + if (type.SpecialType == SpecialType.System_String) + { + return false; + } + + return _implementsCache.GetOrAdd((type, "System.Collections.IEnumerable"), key => + key.Type.AllInterfaces.Any(i => + i.OriginalDefinition.ToDisplayString() == "System.Collections.IEnumerable" || + (i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable"))); + } +} + +internal sealed class TypeStringTupleComparer : IEqualityComparer<(ITypeSymbol Type, string Name)> +{ + public static readonly TypeStringTupleComparer Default = new(); + + private TypeStringTupleComparer() { } + + public bool Equals((ITypeSymbol Type, string Name) x, (ITypeSymbol Type, string Name) y) + { + return Microsoft.CodeAnalysis.SymbolEqualityComparer.Default.Equals(x.Type, y.Type) && x.Name == y.Name; + } + + public int GetHashCode((ITypeSymbol Type, string Name) obj) + { + unchecked + { + return (Microsoft.CodeAnalysis.SymbolEqualityComparer.Default.GetHashCode(obj.Type) * 397) ^ obj.Name.GetHashCode(); + } + } +} diff --git a/TUnit.Core.SourceGenerator/Models/TypeWithDataSourceProperties.cs b/TUnit.Core.SourceGenerator/Models/TypeWithDataSourceProperties.cs index b95834a9f4..9b52e5e691 100644 --- a/TUnit.Core.SourceGenerator/Models/TypeWithDataSourceProperties.cs +++ b/TUnit.Core.SourceGenerator/Models/TypeWithDataSourceProperties.cs @@ -7,3 +7,17 @@ public struct TypeWithDataSourceProperties public INamedTypeSymbol TypeSymbol { get; init; } public List Properties { get; init; } } + +public sealed class TypeWithDataSourcePropertiesComparer : IEqualityComparer +{ + public bool Equals(TypeWithDataSourceProperties x, TypeWithDataSourceProperties y) + { + // Compare based on the type symbol - this handles partial classes correctly + return SymbolEqualityComparer.Default.Equals(x.TypeSymbol, y.TypeSymbol); + } + + public int GetHashCode(TypeWithDataSourceProperties obj) + { + return SymbolEqualityComparer.Default.GetHashCode(obj.TypeSymbol); + } +} diff --git a/TUnit.Core/AotCompatibility/GenericTestRegistry.cs b/TUnit.Core/AotCompatibility/GenericTestRegistry.cs index 0afcdd7588..3e5a8f1b74 100644 --- a/TUnit.Core/AotCompatibility/GenericTestRegistry.cs +++ b/TUnit.Core/AotCompatibility/GenericTestRegistry.cs @@ -23,7 +23,7 @@ public static void RegisterGenericMethod(Type declaringType, string methodName, _compiledMethods[key] = compiledMethod; // Track registered combinations - var combinations = _registeredCombinations.GetOrAdd(declaringType, _ => new HashSet(new TypeArrayComparer())); + var combinations = _registeredCombinations.GetOrAdd(declaringType, static _ => new HashSet(new TypeArrayComparer())); combinations.Add(typeArguments); } diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index 24536b466a..f4bcc3dcb2 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -15,7 +15,7 @@ private ClassDataSources() public static ClassDataSources Get(string sessionId) { - return SourcesPerSession.GetOrAdd(sessionId, _ => new ClassDataSources()); + return SourcesPerSession.GetOrAdd(sessionId, static _ => new ClassDataSources()); } public (T, SharedType, string) GetItemForIndexAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] T>(int index, Type testClassType, SharedType[] sharedTypes, string[] keys, DataGeneratorMetadata dataGeneratorMetadata) where T : new() diff --git a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs index 7eda317f4f..6c41636817 100644 --- a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs @@ -157,11 +157,11 @@ public MethodDataSourceAttribute( hasAnyItems = true; yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(p => p.Type).ToArray(); + var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); }; } - + // If the async enumerable was empty, yield one empty result like NoDataSource does if (!hasAnyItems) { @@ -182,11 +182,11 @@ public MethodDataSourceAttribute( hasAnyItems = true; yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(p => p.Type).ToArray(); + var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); }; } - + // If the enumerable was empty, yield one empty result like NoDataSource does if (!hasAnyItems) { @@ -197,7 +197,7 @@ public MethodDataSourceAttribute( { yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(p => p.Type).ToArray(); + var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(taskResult.ToObjectArrayWithTypes(paramTypes)); }; } @@ -210,10 +210,10 @@ public MethodDataSourceAttribute( foreach (var item in enumerable) { hasAnyItems = true; - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(p => p.Type).ToArray(); + var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); yield return () => Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); } - + // If the enumerable was empty, yield one empty result like NoDataSource does if (!hasAnyItems) { @@ -222,7 +222,7 @@ public MethodDataSourceAttribute( } else { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(p => p.Type).ToArray(); + var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); yield return async () => { return await Task.FromResult(methodResult.ToObjectArrayWithTypes(paramTypes)); diff --git a/TUnit.Core/Data/ScopedDictionary.cs b/TUnit.Core/Data/ScopedDictionary.cs index ebb3a1ab1b..7a751429ff 100644 --- a/TUnit.Core/Data/ScopedDictionary.cs +++ b/TUnit.Core/Data/ScopedDictionary.cs @@ -9,7 +9,7 @@ public class ScopedDictionary public object? GetOrCreate(TScope scope, Type type, Func factory) { - var innerDictionary = _scopedContainers.GetOrAdd(scope, _ => new ThreadSafeDictionary()); + var innerDictionary = _scopedContainers.GetOrAdd(scope, static _ => new ThreadSafeDictionary()); var obj = innerDictionary.GetOrAdd(type, factory); diff --git a/TUnit.Core/Data/ThreadSafeDictionary.cs b/TUnit.Core/Data/ThreadSafeDictionary.cs index bf3b5762a6..18f1ba252f 100644 --- a/TUnit.Core/Data/ThreadSafeDictionary.cs +++ b/TUnit.Core/Data/ThreadSafeDictionary.cs @@ -16,7 +16,9 @@ public class ThreadSafeDictionary Keys => _innerDictionary.Keys; - public ICollection Values => _innerDictionary.Values.Select(lazy => lazy.Value).ToList(); + // Return IEnumerable to avoid allocating a List on every access + // Callers can materialize if needed + public IEnumerable Values => _innerDictionary.Values.Select(lazy => lazy.Value); public TValue GetOrAdd(TKey key, Func func) { diff --git a/TUnit.Core/DataSources/TestDataFormatter.cs b/TUnit.Core/DataSources/TestDataFormatter.cs index 4356845487..028b5fb586 100644 --- a/TUnit.Core/DataSources/TestDataFormatter.cs +++ b/TUnit.Core/DataSources/TestDataFormatter.cs @@ -26,7 +26,11 @@ public static string FormatArguments(object?[] arguments, List ArgumentFormatter.Format(arg, formatters)).ToArray(); + var formattedArgs = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(arguments[i], formatters); + } return string.Join(", ", formattedArgs); } @@ -40,8 +44,11 @@ public static string FormatArguments(object?[] arguments) return string.Empty; } - var formattedArgs = arguments.Select(arg => ArgumentFormatter.Format(arg, [ - ])).ToArray(); + var formattedArgs = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(arguments[i], []); + } return string.Join(", ", formattedArgs); } @@ -77,7 +84,12 @@ public static string CreateGenericDisplayName(TestMetadata metadata, Type[] gene if (genericTypes.Length > 0) { - var genericPart = string.Join(", ", genericTypes.Select(GetSimpleTypeName)); + var genericTypeNames = new string[genericTypes.Length]; + for (var i = 0; i < genericTypes.Length; i++) + { + genericTypeNames[i] = GetSimpleTypeName(genericTypes[i]); + } + var genericPart = string.Join(", ", genericTypeNames); testName = $"{testName}<{genericPart}>"; } @@ -105,7 +117,12 @@ private static string GetSimpleTypeName(Type type) } var genericArgs = type.GetGenericArguments(); - var genericArgsText = string.Join(", ", genericArgs.Select(GetSimpleTypeName)); + var genericArgNames = new string[genericArgs.Length]; + for (var i = 0; i < genericArgs.Length; i++) + { + genericArgNames[i] = GetSimpleTypeName(genericArgs[i]); + } + var genericArgsText = string.Join(", ", genericArgNames); return $"{genericTypeName}<{genericArgsText}>"; } diff --git a/TUnit.Core/Extensions/MetadataExtensions.cs b/TUnit.Core/Extensions/MetadataExtensions.cs index 46bfb8d6ff..46291edd5b 100644 --- a/TUnit.Core/Extensions/MetadataExtensions.cs +++ b/TUnit.Core/Extensions/MetadataExtensions.cs @@ -15,7 +15,13 @@ public static class MetadataExtensions public static MethodInfo GetReflectionInfo(this MethodMetadata method) { - return GetMethodFromType(method.Type, method.Name, method.Parameters.Select(x => x.Type).ToArray())!; + // Optimize: Use for-loop instead of LINQ to reduce allocations + var paramTypes = new Type[method.Parameters.Length]; + for (int i = 0; i < method.Parameters.Length; i++) + { + paramTypes[i] = method.Parameters[i].Type; + } + return GetMethodFromType(method.Type, method.Name, paramTypes)!; } public static IEnumerable GetCustomAttributes(this MethodMetadata method) @@ -36,10 +42,33 @@ public static IEnumerable GetCustomAttributes(this MethodMetadata met string name, Type[] parameters) { - return type - .GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy) - .SingleOrDefault(x => x.Name == name && x.GetParameters().Select(p => p.ParameterType).SequenceEqual(parameters)) - ?? type.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy) + // Optimize: Avoid LINQ Select in hot path - use manual parameter comparison + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy); + + foreach (var method in methods) + { + if (method.Name != name) + continue; + + var methodParams = method.GetParameters(); + if (methodParams.Length != parameters.Length) + continue; + + bool parametersMatch = true; + for (int i = 0; i < parameters.Length; i++) + { + if (methodParams[i].ParameterType != parameters[i]) + { + parametersMatch = false; + break; + } + } + + if (parametersMatch) + return method; + } + + return type.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy) ?? throw new InvalidOperationException($"Method '{name}' with parameters {string.Join(", ", parameters.Select(p => p.Name))} not found in type '{type.FullName}'."); } } diff --git a/TUnit.Core/Extensions/TestContextExtensions.cs b/TUnit.Core/Extensions/TestContextExtensions.cs index 0e1e05984e..7dfa3eabb1 100644 --- a/TUnit.Core/Extensions/TestContextExtensions.cs +++ b/TUnit.Core/Extensions/TestContextExtensions.cs @@ -20,7 +20,15 @@ public static string GetClassTypeName(this TestContext context) return context.TestDetails.ClassType.Name; } - return $"{context.TestDetails.ClassType.Name}({string.Join(", ", context.TestDetails.TestClassArguments.Select(a => ArgumentFormatter.Format(a, context.ArgumentDisplayFormatters)))})"; + // Optimize: Use array instead of LINQ Select to reduce allocations + var args = context.TestDetails.TestClassArguments; + var formattedArgs = new string[args.Length]; + for (int i = 0; i < args.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(args[i], context.ArgumentDisplayFormatters); + } + + return $"{context.TestDetails.ClassType.Name}({string.Join(", ", formattedArgs)})"; } #if NET6_0_OR_GREATER diff --git a/TUnit.Core/GenericTestMetadata.cs b/TUnit.Core/GenericTestMetadata.cs index d8f4949554..09d677610d 100644 --- a/TUnit.Core/GenericTestMetadata.cs +++ b/TUnit.Core/GenericTestMetadata.cs @@ -31,10 +31,16 @@ public override Func 0 }) { // Create a key from the inferred types - must match source generator format - var typeKey = string.Join(",", inferredTypes.Select(t => t.FullName ?? t.Name)); + var typeNames = new string[inferredTypes.Length]; + for (var i = 0; i < inferredTypes.Length; i++) + { + typeNames[i] = inferredTypes[i].FullName ?? inferredTypes[i].Name; + } + typeKey = string.Join(",", typeNames); // Find the matching concrete instantiation if (genericMetadata.ConcreteInstantiations.TryGetValue(typeKey, out var concreteMetadata)) @@ -43,12 +49,16 @@ public override Func 0 ? string.Join(",", inferredTypes.Select(t => t.FullName ?? t.Name)) : "unknown")}. " + + $"with type arguments: {typeKey}. " + $"Available: {availableKeys}"); } @@ -130,8 +140,17 @@ public override Func p.Type).ToArray(); - var hasCancellationToken = parameterTypes.Any(t => t == typeof(CancellationToken)); + var parameters = metadata.MethodMetadata.Parameters; + var parameterTypes = new Type[parameters.Length]; + var hasCancellationToken = false; + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].Type; + if (parameters[i].Type == typeof(CancellationToken)) + { + hasCancellationToken = true; + } + } if (hasCancellationToken) { diff --git a/TUnit.Core/Helpers/ArgumentFormatter.cs b/TUnit.Core/Helpers/ArgumentFormatter.cs index bf674de695..dd811f79b4 100644 --- a/TUnit.Core/Helpers/ArgumentFormatter.cs +++ b/TUnit.Core/Helpers/ArgumentFormatter.cs @@ -44,8 +44,11 @@ private static string FormatDefault(object? o) return "null"; } + // Cache GetType() result to avoid repeated virtual method calls + var type = o.GetType(); + // Handle tuples specially - if (TupleHelper.IsTupleType(o.GetType())) + if (TupleHelper.IsTupleType(type)) { return FormatTuple(o); } @@ -63,14 +66,14 @@ private static string FormatDefault(object? o) return toString; } - if (o.GetType().IsPrimitive || o is string) + if (type.IsPrimitive || o is string) { return toString; } - if (toString == o.GetType().FullName || toString == o.GetType().AssemblyQualifiedName) + if (toString == type.FullName || toString == type.AssemblyQualifiedName) { - return o.GetType().Name; + return type.Name; } return toString; diff --git a/TUnit.Core/Logging/DefaultLogger.cs b/TUnit.Core/Logging/DefaultLogger.cs index 8917a086ce..76fc7c18e1 100644 --- a/TUnit.Core/Logging/DefaultLogger.cs +++ b/TUnit.Core/Logging/DefaultLogger.cs @@ -20,7 +20,7 @@ public void PushProperties(IDictionary> dictionary) public void PushProperty(string name, object? value) { - var list = _values.GetOrAdd(name, _ => + var list = _values.GetOrAdd(name, static _ => [ ]); var formattedValue = FormatValue(value); diff --git a/TUnit.Core/Models/AssemblyHookContext.cs b/TUnit.Core/Models/AssemblyHookContext.cs index 9387037a49..a7f43367b3 100644 --- a/TUnit.Core/Models/AssemblyHookContext.cs +++ b/TUnit.Core/Models/AssemblyHookContext.cs @@ -27,23 +27,31 @@ internal AssemblyHookContext(TestSessionContext testSessionContext) : base(testS public required Assembly Assembly { get; init; } private readonly List _testClasses = []; + private TestContext[]? _cachedAllTests; public void AddClass(ClassHookContext classHookContext) { _testClasses.Add(classHookContext); + InvalidateCache(); } public IReadOnlyList TestClasses => _testClasses; - public IReadOnlyList AllTests => TestClasses.SelectMany(x => x.Tests).ToArray(); + public IReadOnlyList AllTests => _cachedAllTests ??= TestClasses.SelectMany(x => x.Tests).ToArray(); public int TestCount => AllTests.Count; + private void InvalidateCache() + { + _cachedAllTests = null; + } + internal bool FirstTestStarted { get; set; } internal void RemoveClass(ClassHookContext classContext) { _testClasses.Remove(classContext); + InvalidateCache(); if (_testClasses.Count == 0) { diff --git a/TUnit.Core/Models/TestSessionContext.cs b/TUnit.Core/Models/TestSessionContext.cs index 09598aa39d..01d838115a 100644 --- a/TUnit.Core/Models/TestSessionContext.cs +++ b/TUnit.Core/Models/TestSessionContext.cs @@ -58,17 +58,26 @@ internal TestSessionContext(TestDiscoveryContext beforeTestDiscoveryContext) : b public required string? TestFilter { get; init; } private readonly List _assemblies = []; + private ClassHookContext[]? _cachedTestClasses; + private TestContext[]? _cachedAllTests; public void AddAssembly(AssemblyHookContext assemblyHookContext) { _assemblies.Add(assemblyHookContext); + InvalidateCaches(); } public IReadOnlyList Assemblies => _assemblies; - public IReadOnlyList TestClasses => Assemblies.SelectMany(x => x.TestClasses).ToArray(); + public IReadOnlyList TestClasses => _cachedTestClasses ??= Assemblies.SelectMany(x => x.TestClasses).ToArray(); - public IReadOnlyList AllTests => TestClasses.SelectMany(x => x.Tests).ToArray(); + public IReadOnlyList AllTests => _cachedAllTests ??= TestClasses.SelectMany(x => x.Tests).ToArray(); + + private void InvalidateCaches() + { + _cachedTestClasses = null; + _cachedAllTests = null; + } internal bool FirstTestStarted { get; set; } @@ -82,6 +91,7 @@ public void AddArtifact(Artifact artifact) internal void RemoveAssembly(AssemblyHookContext assemblyContext) { _assemblies.Remove(assemblyContext); + InvalidateCaches(); } internal override void SetAsyncLocalContext() diff --git a/TUnit.Core/Services/TestExecutionRegistry.cs b/TUnit.Core/Services/TestExecutionRegistry.cs index 1d3da65140..438994bff6 100644 --- a/TUnit.Core/Services/TestExecutionRegistry.cs +++ b/TUnit.Core/Services/TestExecutionRegistry.cs @@ -38,7 +38,7 @@ public void RegisterTest(string testId, TestExecutionData data) /// public TestExecutionData GetOrCreateTestData(string testId) { - return _testData.GetOrAdd(testId, _ => new TestExecutionData()); + return _testData.GetOrAdd(testId, static _ => new TestExecutionData()); } // ISourceGeneratedTestRegistry implementation for backward compatibility diff --git a/TUnit.Core/SourceRegistrar.cs b/TUnit.Core/SourceRegistrar.cs index 5d12c62c3f..b9c71d4c26 100644 --- a/TUnit.Core/SourceRegistrar.cs +++ b/TUnit.Core/SourceRegistrar.cs @@ -40,10 +40,10 @@ public static void RegisterAssembly(Func assemblyLoader) public static void Register(ITestSource testSource) { // For backward compatibility, add to all types queue if no type specified - var allTypesQueue = Sources.TestSources.GetOrAdd(typeof(object), _ => new ConcurrentQueue()); + var allTypesQueue = Sources.TestSources.GetOrAdd(typeof(object), static _ => new ConcurrentQueue()); allTypesQueue.Enqueue(testSource); } - + /// /// Registers a test source for a specific test class type. /// @@ -51,7 +51,7 @@ public static void Register(ITestSource testSource) /// The test source to register. public static void Register(Type testClassType, ITestSource testSource) { - var queue = Sources.TestSources.GetOrAdd(testClassType, _ => new ConcurrentQueue()); + var queue = Sources.TestSources.GetOrAdd(testClassType, static _ => new ConcurrentQueue()); queue.Enqueue(testSource); } diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 75248495a6..e2edf48255 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -219,8 +219,12 @@ public string GetDisplayName() return TestName; } - var arguments = string.Join(", ", TestDetails.TestMethodArguments - .Select(arg => ArgumentFormatter.Format(arg, ArgumentDisplayFormatters))); + var formattedArgs = new string[TestDetails.TestMethodArguments.Length]; + for (var i = 0; i < TestDetails.TestMethodArguments.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(TestDetails.TestMethodArguments[i], ArgumentDisplayFormatters); + } + var arguments = string.Join(", ", formattedArgs); if (string.IsNullOrEmpty(arguments)) { @@ -251,7 +255,9 @@ public void AddLinkedCancellationToken(CancellationToken cancellationToken) else { var existingToken = LinkedCancellationTokens.Token; + var oldCts = LinkedCancellationTokens; LinkedCancellationTokens = CancellationTokenSource.CreateLinkedTokenSource(existingToken, cancellationToken); + oldCts.Dispose(); } CancellationToken = LinkedCancellationTokens.Token; diff --git a/TUnit.Core/Tracking/ObjectTracker.cs b/TUnit.Core/Tracking/ObjectTracker.cs index e357dd3daa..251a8fa4a7 100644 --- a/TUnit.Core/Tracking/ObjectTracker.cs +++ b/TUnit.Core/Tracking/ObjectTracker.cs @@ -73,7 +73,7 @@ private void TrackObject(object? obj) return; } - var counter = _trackedObjects.GetOrAdd(obj, _ => new Counter()); + var counter = _trackedObjects.GetOrAdd(obj, static _ => new Counter()); counter.Increment(); } @@ -115,7 +115,7 @@ public static void OnDisposed(object? o, Action action) return; } - _trackedObjects.GetOrAdd(o, _ => new Counter()) + _trackedObjects.GetOrAdd(o, static _ => new Counter()) .OnCountChanged += (_, count) => { if (count == 0) @@ -132,7 +132,7 @@ public static void OnDisposedAsync(object? o, Func asyncAction) return; } - _trackedObjects.GetOrAdd(o, _ => new Counter()) + _trackedObjects.GetOrAdd(o, static _ => new Counter()) .OnCountChanged += async (_, count) => { if (count == 0) diff --git a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs index 01b78e1d4e..09b3a4aa5f 100644 --- a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs +++ b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs @@ -16,6 +16,11 @@ internal static class ReflectionAttributeExtractor /// private static readonly ConcurrentDictionary _attributeCache = new(); + /// + /// Cache for multiple attributes lookups to avoid repeated reflection calls + /// + private static readonly ConcurrentDictionary _attributesCache = new(); + /// /// Composite cache key combining type, method, and attribute type information /// @@ -103,17 +108,24 @@ public static IEnumerable GetAttributes(Type testClass, MethodInfo? testMe } #endif - var attributes = new List(); - - attributes.AddRange(testClass.Assembly.GetCustomAttributes()); - attributes.AddRange(testClass.GetCustomAttributes()); + var cacheKey = new AttributeCacheKey(testClass, testMethod, typeof(T)); - if (testMethod != null) + var cachedAttributes = _attributesCache.GetOrAdd(cacheKey, key => { - attributes.AddRange(testMethod.GetCustomAttributes()); - } + var attributes = new List(); + + attributes.AddRange(key.TestClass.Assembly.GetCustomAttributes(key.AttributeType)); + attributes.AddRange(key.TestClass.GetCustomAttributes(key.AttributeType)); + + if (key.TestMethod != null) + { + attributes.AddRange(key.TestMethod.GetCustomAttributes(key.AttributeType)); + } + + return attributes.ToArray(); + }); - return attributes; + return cachedAttributes.Cast(); } public static string[] ExtractCategories(Type testClass, MethodInfo testMethod) diff --git a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs index 02f5965d18..69b9b0e389 100644 --- a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs +++ b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs @@ -20,13 +20,28 @@ internal sealed class ReflectionHookDiscoveryService { private static readonly ConcurrentDictionary _scannedAssemblies = new(); private static readonly ConcurrentDictionary _registeredMethods = new(); + private static readonly ConcurrentDictionary _methodKeyCache = new(); private static int _registrationIndex = 0; private static int _discoveryRunCount = 0; private static string GetMethodKey(MethodInfo method) { - // Create a unique key for the method based on its signature - return $"{method.DeclaringType?.FullName}.{method.Name}({string.Join(",", method.GetParameters().Select(p => p.ParameterType.FullName))})"; + // Cache method keys to avoid repeated string allocations during discovery + return _methodKeyCache.GetOrAdd(method, m => + { + var parameters = m.GetParameters(); + if (parameters.Length == 0) + { + return $"{m.DeclaringType?.FullName}.{m.Name}()"; + } + + var paramTypes = new string[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + paramTypes[i] = parameters[i].ParameterType.FullName ?? "unknown"; + } + return $"{m.DeclaringType?.FullName}.{m.Name}({string.Join(",", paramTypes)})"; + }); } private static void ClearSourceGeneratedHooks() @@ -105,9 +120,10 @@ public static void DiscoverInstanceHooksForType(Type closedGenericType) if (beforeEveryAttr != null) orders.Add(beforeEveryAttr.Order); if (afterEveryAttr != null) orders.Add(afterEveryAttr.Order); - return orders.Any() ? orders.Min() : 0; + // Use Count instead of Any() to avoid double enumeration + return orders.Count > 0 ? orders.Min() : 0; }) - .ThenBy(m => m.MetadataToken) // Then sort by MetadataToken to preserve source file order + .ThenBy(static m => m.MetadataToken) // Then sort by MetadataToken to preserve source file order .ToArray(); foreach (var method in methods) @@ -291,9 +307,10 @@ private static void DiscoverHooksInType([DynamicallyAccessedMembers(DynamicallyA if (beforeEveryAttr != null) orders.Add(beforeEveryAttr.Order); if (afterEveryAttr != null) orders.Add(afterEveryAttr.Order); - return orders.Any() ? orders.Min() : 0; + // Use Count instead of Any() to avoid double enumeration + return orders.Count > 0 ? orders.Min() : 0; }) - .ThenBy(m => m.MetadataToken) // Then sort by MetadataToken to preserve source file order + .ThenBy(static m => m.MetadataToken) // Then sort by MetadataToken to preserve source file order .ToArray(); foreach (var method in methods) @@ -681,7 +698,7 @@ private static void RegisterInstanceBeforeHook( return; } - var bag = Sources.BeforeTestHooks.GetOrAdd(type, _ => new ConcurrentBag()); + var bag = Sources.BeforeTestHooks.GetOrAdd(type, static _ => new ConcurrentBag()); var hook = new InstanceHookMethod { InitClassType = type, @@ -706,7 +723,7 @@ private static void RegisterInstanceAfterHook( return; } - var bag = Sources.AfterTestHooks.GetOrAdd(type, _ => new ConcurrentBag()); + var bag = Sources.AfterTestHooks.GetOrAdd(type, static _ => new ConcurrentBag()); var hook = new InstanceHookMethod { InitClassType = type, @@ -725,7 +742,7 @@ private static void RegisterBeforeClassHook( MethodInfo method, int order) { - var bag = Sources.BeforeClassHooks.GetOrAdd(type, _ => new ConcurrentBag()); + var bag = Sources.BeforeClassHooks.GetOrAdd(type, static _ => new ConcurrentBag()); var hook = new BeforeClassHookMethod { MethodInfo = CreateMethodMetadata(type, method), @@ -745,7 +762,7 @@ private static void RegisterAfterClassHook( MethodInfo method, int order) { - var bag = Sources.AfterClassHooks.GetOrAdd(type, _ => new ConcurrentBag()); + var bag = Sources.AfterClassHooks.GetOrAdd(type, static _ => new ConcurrentBag()); var hook = new AfterClassHookMethod { MethodInfo = CreateMethodMetadata(type, method), @@ -766,7 +783,7 @@ private static void RegisterBeforeAssemblyHook( MethodInfo method, int order) { - var bag = Sources.BeforeAssemblyHooks.GetOrAdd(assembly, _ => new ConcurrentBag()); + var bag = Sources.BeforeAssemblyHooks.GetOrAdd(assembly, static _ => new ConcurrentBag()); var hook = new BeforeAssemblyHookMethod { MethodInfo = CreateMethodMetadata(type, method), @@ -787,7 +804,7 @@ private static void RegisterAfterAssemblyHook( MethodInfo method, int order) { - var bag = Sources.AfterAssemblyHooks.GetOrAdd(assembly, _ => new ConcurrentBag()); + var bag = Sources.AfterAssemblyHooks.GetOrAdd(assembly, static _ => new ConcurrentBag()); var hook = new AfterAssemblyHookMethod { MethodInfo = CreateMethodMetadata(type, method), diff --git a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index 890a6e4b8b..b61e758d35 100644 --- a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -22,8 +22,8 @@ namespace TUnit.Engine.Discovery; internal sealed class ReflectionTestDataCollector : ITestDataCollector { private static readonly ConcurrentDictionary _scannedAssemblies = new(); - private static readonly ConcurrentBag _discoveredTests = new(); - private static readonly Lock _resultsLock = new(); // Only for final results aggregation + private static readonly List _discoveredTests = new(capacity: 1000); // Pre-sized for typical test suites + private static readonly Lock _discoveredTestsLock = new(); // Lock for thread-safe access to _discoveredTests private static readonly ConcurrentDictionary _assemblyTypesCache = new(); private static readonly ConcurrentDictionary _typeMethodsCache = new(); @@ -41,7 +41,10 @@ private static Assembly[] GetCachedAssemblies() public static void ClearCaches() { _scannedAssemblies.Clear(); - while (_discoveredTests.TryTake(out _)) { } + lock (_discoveredTestsLock) + { + _discoveredTests.Clear(); + } _assemblyTypesCache.Clear(); _typeMethodsCache.Clear(); lock (_assemblyCacheLock) @@ -132,16 +135,11 @@ public async Task> CollectTestsAsync(string testSessio var dynamicTests = await DiscoverDynamicTests(testSessionId).ConfigureAwait(false); newTests.AddRange(dynamicTests); - // Add to concurrent collection without locking - foreach (var test in newTests) - { - _discoveredTests.Add(test); - } - - // Only lock when creating the final result list - lock (_resultsLock) + // Add to discovered tests with lock (better enumeration performance than ConcurrentBag) + lock (_discoveredTestsLock) { - return _discoveredTests.ToList(); + _discoveredTests.AddRange(newTests); + return new List(_discoveredTests); } } @@ -179,8 +177,10 @@ public async IAsyncEnumerable CollectTestsStreamingAsync( // Stream tests from this assembly await foreach (var test in DiscoverTestsInAssemblyStreamingAsync(assembly, cancellationToken)) { - // Use lock-free ConcurrentBag - _discoveredTests.Add(test); + lock (_discoveredTestsLock) + { + _discoveredTests.Add(test); + } yield return test; } } @@ -188,7 +188,10 @@ public async IAsyncEnumerable CollectTestsStreamingAsync( // Stream dynamic tests await foreach (var dynamicTest in DiscoverDynamicTestsStreamingAsync(testSessionId, cancellationToken)) { - _discoveredTests.Add(dynamicTest); + lock (_discoveredTestsLock) + { + _discoveredTests.Add(dynamicTest); + } yield return dynamicTest; } } diff --git a/TUnit.Engine/Discovery/ReflectionTestMetadata.cs b/TUnit.Engine/Discovery/ReflectionTestMetadata.cs index 52dd8d7c26..d52c6fcba9 100644 --- a/TUnit.Engine/Discovery/ReflectionTestMetadata.cs +++ b/TUnit.Engine/Discovery/ReflectionTestMetadata.cs @@ -81,7 +81,7 @@ async Task CreateInstance(TestContext testContext) // Create test invoker with CancellationToken support // Determine if the test method has a CancellationToken parameter - var parameterTypes = MethodMetadata.Parameters.Select(p => p.Type).ToArray(); + var parameterTypes = MethodMetadata.Parameters.Select(static p => p.Type).ToArray(); var hasCancellationToken = parameterTypes.Any(t => t == typeof(CancellationToken)); var cancellationTokenIndex = hasCancellationToken ? Array.IndexOf(parameterTypes, typeof(CancellationToken)) diff --git a/TUnit.Engine/Extensions/JsonExtensions.cs b/TUnit.Engine/Extensions/JsonExtensions.cs index afc7abe1b2..aae632e86b 100644 --- a/TUnit.Engine/Extensions/JsonExtensions.cs +++ b/TUnit.Engine/Extensions/JsonExtensions.cs @@ -9,7 +9,7 @@ public static TestSessionJson ToJsonModel(this TestSessionContext context) { return new TestSessionJson { - Assemblies = context.Assemblies.Select(x => x.ToJsonModel()).ToArray() + Assemblies = context.Assemblies.Select(static x => x.ToJsonModel()).ToArray() }; } @@ -18,7 +18,7 @@ public static TestAssemblyJson ToJsonModel(this AssemblyHookContext context) return new TestAssemblyJson { AssemblyName = context.Assembly.GetName().FullName, - Classes = context.TestClasses.Select(x => x.ToJsonModel()).ToArray() + Classes = context.TestClasses.Select(static x => x.ToJsonModel()).ToArray() }; } @@ -27,7 +27,7 @@ public static TestClassJson ToJsonModel(this ClassHookContext context) return new TestClassJson { Type = context.ClassType.FullName, - Tests = context.Tests.Select(x => x.ToJsonModel()).ToArray() + Tests = context.Tests.Select(static x => x.ToJsonModel()).ToArray() }; } @@ -58,8 +58,8 @@ public static TestJson ToJsonModel(this TestContext context) TestFilePath = testDetails.TestFilePath, TestLineNumber = testDetails.TestLineNumber, TestMethodArguments = testDetails.TestMethodArguments, - TestClassParameterTypes = testDetails.TestClassParameterTypes?.Select(x => x.FullName ?? "Unknown").ToArray() ?? [], - TestMethodParameterTypes = testDetails.MethodMetadata.Parameters.Select(p => p.Type.FullName ?? "Unknown").ToArray(), + TestClassParameterTypes = testDetails.TestClassParameterTypes?.Select(static x => x.FullName ?? "Unknown").ToArray() ?? [], + TestMethodParameterTypes = testDetails.MethodMetadata.Parameters.Select(static p => p.Type.FullName ?? "Unknown").ToArray(), }; } diff --git a/TUnit.Engine/Extensions/TestExtensions.cs b/TUnit.Engine/Extensions/TestExtensions.cs index ec86e1e42f..491822a443 100644 --- a/TUnit.Engine/Extensions/TestExtensions.cs +++ b/TUnit.Engine/Extensions/TestExtensions.cs @@ -26,17 +26,17 @@ internal static TestNode ToTestNode(this TestContext testContext) AssemblyFullName: testDetails.MethodMetadata.Class.Type.Assembly.GetName().FullName, TypeName: testContext.GetClassTypeName(), MethodName: testDetails.MethodName, - ParameterTypeFullNames: CreateParameterTypeArray(testDetails.MethodMetadata.Parameters.Select(p => p.Type).ToArray()), + ParameterTypeFullNames: CreateParameterTypeArray(testDetails.MethodMetadata.Parameters.Select(static p => p.Type).ToArray()), ReturnTypeFullName: testDetails.ReturnType.FullName ?? typeof(void).FullName!, MethodArity: testDetails.MethodMetadata.GenericTypeCount ), // Custom TUnit Properties - ..testDetails.Categories.Select(category => new TestMetadataProperty(category)), + ..testDetails.Categories.Select(static category => new TestMetadataProperty(category)), ..ExtractProperties(testDetails), // Artifacts - ..testContext.Artifacts.Select(x => new FileArtifactProperty(x.File, x.DisplayName, x.Description)), + ..testContext.Artifacts.Select(static x => new FileArtifactProperty(x.File, x.DisplayName, x.Description)), // TRX Report Properties new TrxFullyQualifiedTypeNameProperty(testDetails.MethodMetadata.Class?.Type.FullName ?? testDetails.ClassType?.FullName ?? "UnknownType"), diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index a7cf851525..3f27f6cde0 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -76,7 +76,6 @@ public TUnitServiceProvider(IExtension extension, TestContext.Configuration = new ConfigurationAdapter(configuration); VerbosityService = Register(new VerbosityService(CommandLineOptions, frameworkServiceProvider)); - DiscoveryDiagnostics.Initialize(VerbosityService); var logLevelProvider = Register(new LogLevelProvider(CommandLineOptions)); diff --git a/TUnit.Engine/Helpers/DiscoveryDiagnostics.cs b/TUnit.Engine/Helpers/DiscoveryDiagnostics.cs deleted file mode 100644 index 696c66867b..0000000000 --- a/TUnit.Engine/Helpers/DiscoveryDiagnostics.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System.Text.RegularExpressions; -using TUnit.Engine.Services; - -namespace TUnit.Engine.Helpers; - -/// -/// Provides diagnostics and monitoring for test discovery to detect hanging issues -/// -internal static class DiscoveryDiagnostics -{ - private static readonly object _lock = new(); - private static readonly List _events = - [ - ]; - private static VerbosityService? _verbosityService; - - public static bool IsEnabled { get; set; } = EnvironmentVariableCache.Get("TUNIT_DISCOVERY_DIAGNOSTICS") == "1"; - - public static void Initialize(VerbosityService verbosityService) - { - _verbosityService = verbosityService; - // Override environment variable setting with verbosity service - IsEnabled = IsEnabled || (_verbosityService?.EnableDiscoveryDiagnostics ?? false); - } - - public static void RecordEvent(string eventName, string details = "") - { - if (!IsEnabled) - { - return; - } - - lock (_lock) - { - _events.Add(new DiscoveryEvent - { - Timestamp = DateTime.UtcNow, - EventName = eventName, - Details = details, - ThreadId = Environment.CurrentManagedThreadId - }); - } - } - - public static void RecordDataSourceStart(string sourceName, int itemCount = -1) - { - RecordEvent("DataSourceStart", $"Source: {sourceName}, Items: {itemCount}"); - } - - public static void RecordDataSourceEnd(string sourceName, int itemCount) - { - RecordEvent("DataSourceEnd", $"Source: {sourceName}, Items: {itemCount}"); - } - - public static void RecordTestExpansion(string testName, int combinationCount) - { - RecordEvent("TestExpansion", $"Test: {testName}, Combinations: {combinationCount}"); - } - - public static void RecordCartesianProductDepth(int depth, int setCount) - { - RecordEvent("CartesianProduct", $"Depth: {depth}, Sets: {setCount}"); - } - - public static void RecordHangDetection(string location, int elapsedSeconds) - { - RecordEvent("PotentialHang", $"Location: {location}, Elapsed: {elapsedSeconds}s"); - - // Also write to console for immediate visibility if verbosity allows - if (_verbosityService == null || !_verbosityService.HideTestOutput) - { - Console.Error.WriteLine($"[TUnit] WARNING: Potential hang detected at {location} after {elapsedSeconds} seconds"); - } - } - - public static void DumpDiagnostics() - { - if (!IsEnabled) - { - return; - } - - // Only output to console if verbosity allows - if (_verbosityService is { HideTestOutput: true }) - { - return; - } - - lock (_lock) - { - Console.Error.WriteLine("[TUnit] Discovery Diagnostics:"); - Console.Error.WriteLine($"Total events: {_events.Count}"); - - foreach (var evt in _events.OrderBy(e => e.Timestamp)) - { - Console.Error.WriteLine($" [{evt.Timestamp:HH:mm:ss.fff}] Thread {evt.ThreadId}: {evt.EventName} - {evt.Details}"); - } - - // Analyze for potential issues - var dataSourceStarts = _events.Where(e => e.EventName == "DataSourceStart").ToList(); - var dataSourceEnds = _events.Where(e => e.EventName == "DataSourceEnd").ToList(); - - if (dataSourceStarts.Count > dataSourceEnds.Count) - { - Console.Error.WriteLine($"[TUnit] WARNING: {dataSourceStarts.Count - dataSourceEnds.Count} data sources did not complete!"); - } - - var largeExpansions = _events - .Where(e => e.EventName == "TestExpansion") - .Where(e => - { - var match = Regex.Match(e.Details, @"Combinations: (\d+)"); - return match.Success && int.Parse(match.Groups[1].Value) > 1000; - }) - .ToList(); - - if (largeExpansions.Any()) - { - Console.Error.WriteLine($"[TUnit] WARNING: {largeExpansions.Count} tests generated over 1000 combinations!"); - } - } - } - - private record DiscoveryEvent - { - public DateTime Timestamp { get; init; } - public string EventName { get; init; } = ""; - public string Details { get; init; } = ""; - public int ThreadId { get; init; } - } -} diff --git a/TUnit.Engine/Helpers/DisplayNameBuilder.cs b/TUnit.Engine/Helpers/DisplayNameBuilder.cs index b3df683bc8..90b831754a 100644 --- a/TUnit.Engine/Helpers/DisplayNameBuilder.cs +++ b/TUnit.Engine/Helpers/DisplayNameBuilder.cs @@ -50,8 +50,11 @@ public static string FormatArguments(object?[] arguments) return string.Empty; } - var formattedArgs = arguments.Select(arg => ArgumentFormatter.Format(arg, [ - ])).ToArray(); + var formattedArgs = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(arguments[i], []); + } return string.Join(", ", formattedArgs); } @@ -65,7 +68,11 @@ public static string FormatArguments(object?[] arguments, List ArgumentFormatter.Format(arg, formatters)).ToArray(); + var formattedArgs = new string[arguments.Length]; + for (var i = 0; i < arguments.Length; i++) + { + formattedArgs[i] = ArgumentFormatter.Format(arguments[i], formatters); + } return string.Join(", ", formattedArgs); } @@ -75,10 +82,15 @@ public static string FormatArguments(object?[] arguments, List 0) { - var genericPart = string.Join(", ", genericTypes.Select(t => GetSimpleTypeName(t))); + var genericTypeNames = new string[genericTypes.Length]; + for (var i = 0; i < genericTypes.Length; i++) + { + genericTypeNames[i] = GetSimpleTypeName(genericTypes[i]); + } + var genericPart = string.Join(", ", genericTypeNames); testName = $"{testName}<{genericPart}>"; } @@ -106,8 +118,13 @@ private static string GetSimpleTypeName(Type type) } var genericArgs = type.GetGenericArguments(); - var genericArgsText = string.Join(", ", genericArgs.Select(GetSimpleTypeName)); - + var genericArgNames = new string[genericArgs.Length]; + for (var i = 0; i < genericArgs.Length; i++) + { + genericArgNames[i] = GetSimpleTypeName(genericArgs[i]); + } + var genericArgsText = string.Join(", ", genericArgNames); + return $"{genericTypeName}<{genericArgsText}>"; } diff --git a/TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs b/TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs index de540d98a2..dcdcb4e4f1 100644 --- a/TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs +++ b/TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs @@ -35,7 +35,7 @@ public async ValueTask ExecuteTestsWithConstraintsAsync( } // Sort tests by priority - var sortedTests = tests.OrderBy(t => t.Priority).ToArray(); + var sortedTests = tests.OrderBy(static t => t.Priority).ToArray(); // Track which constraint keys are currently in use var lockedKeys = new HashSet(); diff --git a/TUnit.Engine/Services/CircularDependencyDetector.cs b/TUnit.Engine/Services/CircularDependencyDetector.cs index aa37fe6dd4..70bd5cf353 100644 --- a/TUnit.Engine/Services/CircularDependencyDetector.cs +++ b/TUnit.Engine/Services/CircularDependencyDetector.cs @@ -18,7 +18,7 @@ internal sealed class CircularDependencyDetector { var testList = tests.ToList(); var circularDependencies = new List<(AbstractExecutableTest Test, List DependencyChain)>(); - var visitedStates = new Dictionary(); + var visitedStates = new Dictionary(capacity: testList.Count); foreach (var test in testList) { diff --git a/TUnit.Engine/Services/DataSourceInitializer.cs b/TUnit.Engine/Services/DataSourceInitializer.cs index f0dd58ec96..06093c07b7 100644 --- a/TUnit.Engine/Services/DataSourceInitializer.cs +++ b/TUnit.Engine/Services/DataSourceInitializer.cs @@ -74,7 +74,7 @@ private async Task InitializeDataSourceAsync( try { // Ensure we have required context - objectBag ??= new Dictionary(); + objectBag ??= new Dictionary(capacity: 8); events ??= new TestContextEvents(); // Initialize the data source directly here @@ -111,7 +111,7 @@ await _propertyInjectionService.InjectPropertiesIntoObjectAsync( #endif private async Task InitializeNestedObjectsAsync(object rootObject) { - var objectsByDepth = new Dictionary>(); + var objectsByDepth = new Dictionary>(capacity: 4); var visitedObjects = new HashSet(); // Collect all nested property-injected objects grouped by depth diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index b26b433d2c..fc491fad84 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -103,13 +103,11 @@ public async ValueTask InvokeTestStartEventReceiversAsync(TestContext context, C #endif private async ValueTask InvokeTestStartEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - var receivers = context.GetEligibleEventObjects() - .OfType() - .OrderBy(r => r.Order) - .ToList(); - - // Filter scoped attributes - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); + // Filter scoped attributes - FilterScopedAttributes will materialize the collection + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( + context.GetEligibleEventObjects() + .OfType() + .OrderBy(static r => r.Order)); // Batch invocation for multiple receivers if (filteredReceivers.Count > 3) @@ -145,13 +143,11 @@ public async ValueTask InvokeTestEndEventReceiversAsync(TestContext context, Can #endif private async ValueTask InvokeTestEndEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - var receivers = context.GetEligibleEventObjects() - .OfType() - .OrderBy(r => r.Order) - .ToList(); - - // Filter scoped attributes - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); + // Filter scoped attributes - FilterScopedAttributes will materialize the collection + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( + context.GetEligibleEventObjects() + .OfType() + .OrderBy(static r => r.Order)); foreach (var receiver in filteredReceivers) { @@ -185,13 +181,11 @@ public async ValueTask InvokeTestSkippedEventReceiversAsync(TestContext context, #endif private async ValueTask InvokeTestSkippedEventReceiversCore(TestContext context, CancellationToken cancellationToken) { - var receivers = context.GetEligibleEventObjects() - .OfType() - .OrderBy(r => r.Order) - .ToList(); - - // Filter scoped attributes - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(receivers); + // Filter scoped attributes - FilterScopedAttributes will materialize the collection + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( + context.GetEligibleEventObjects() + .OfType() + .OrderBy(static r => r.Order)); foreach (var receiver in filteredReceivers) { @@ -206,13 +200,13 @@ public async ValueTask InvokeTestDiscoveryEventReceiversAsync(TestContext contex { var eventReceivers = context.GetEligibleEventObjects() .OfType() - .OrderBy(r => r.Order) + .OrderBy(static r => r.Order) .ToList(); // Filter scoped attributes to ensure only the highest priority one of each type is invoked var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(eventReceivers); - foreach (var receiver in filteredReceivers.OrderBy(r => r.Order)) + foreach (var receiver in filteredReceivers.OrderBy(static r => r.Order)) { await receiver.OnTestDiscovered(discoveredContext); } @@ -223,16 +217,13 @@ public async ValueTask InvokeTestDiscoveryEventReceiversAsync(TestContext contex #endif public async ValueTask InvokeHookRegistrationEventReceiversAsync(HookRegisteredContext hookContext, CancellationToken cancellationToken) { - // Get event receivers from the hook method's attributes - var eventReceivers = hookContext.HookMethod.Attributes - .OfType() - .OrderBy(r => r.Order) - .ToList(); - // Filter scoped attributes to ensure only the highest priority one of each type is invoked - var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes(eventReceivers); + var filteredReceivers = ScopedAttributeFilter.FilterScopedAttributes( + hookContext.HookMethod.Attributes + .OfType() + .OrderBy(static r => r.Order)); - foreach (var receiver in filteredReceivers.OrderBy(r => r.Order)) + foreach (var receiver in filteredReceivers.OrderBy(static r => r.Order)) { await receiver.OnHookRegistered(hookContext); } @@ -389,7 +380,7 @@ public async ValueTask InvokeLastTestInAssemblyEventReceiversAsync( var assemblyName = assemblyContext.Assembly.GetName().FullName ?? ""; - var assemblyCount = _assemblyTestCounts.GetOrAdd(assemblyName, _ => new Counter()).Decrement(); + var assemblyCount = _assemblyTestCounts.GetOrAdd(assemblyName, static _ => new Counter()).Decrement(); if (assemblyCount == 0) { @@ -430,7 +421,7 @@ public async ValueTask InvokeLastTestInClassEventReceiversAsync( var classType = classContext.ClassType; - var classCount = _classTestCounts.GetOrAdd(classType, _ => new Counter()).Decrement(); + var classCount = _classTestCounts.GetOrAdd(classType, static _ => new Counter()).Decrement(); if (classCount == 0) { @@ -473,7 +464,7 @@ public void InitializeTestCounts(IEnumerable allTestContexts) foreach (var group in contexts.GroupBy(c => c.ClassContext.AssemblyContext.Assembly.GetName().FullName)) { - var counter = _assemblyTestCounts.GetOrAdd(group.Key, _ => new Counter()); + var counter = _assemblyTestCounts.GetOrAdd(group.Key, static _ => new Counter()); for (var i = 0; i < group.Count(); i++) { @@ -483,7 +474,7 @@ public void InitializeTestCounts(IEnumerable allTestContexts) foreach (var group in contexts.GroupBy(c => c.ClassContext.ClassType)) { - var counter = _classTestCounts.GetOrAdd(group.Key, _ => new Counter()); + var counter = _classTestCounts.GetOrAdd(group.Key, static _ => new Counter()); for (var i = 0; i < group.Count(); i++) { diff --git a/TUnit.Engine/Services/HookCollectionService.cs b/TUnit.Engine/Services/HookCollectionService.cs index f06c043756..b99929bd5c 100644 --- a/TUnit.Engine/Services/HookCollectionService.cs +++ b/TUnit.Engine/Services/HookCollectionService.cs @@ -115,7 +115,7 @@ private async Task>> Bu foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex - finalHooks.AddRange(typeHooks.OrderBy(h => h.order).ThenBy(h => h.registrationIndex).Select(h => h.hook)); + finalHooks.AddRange(typeHooks.OrderBy(static h => h.order).ThenBy(static h => h.registrationIndex).Select(static h => h.hook)); } return finalHooks; @@ -187,7 +187,7 @@ private async Task>> Bu foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex - finalHooks.AddRange(typeHooks.OrderBy(h => h.order).ThenBy(h => h.registrationIndex).Select(h => h.hook)); + finalHooks.AddRange(typeHooks.OrderBy(static h => h.order).ThenBy(static h => h.registrationIndex).Select(static h => h.hook)); } return finalHooks; @@ -223,9 +223,9 @@ private async Task>> Bu } return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) + .OrderBy(static h => h.order) + .ThenBy(static h => h.registrationIndex) + .Select(static h => h.hook) .ToList(); } @@ -259,9 +259,9 @@ private async Task>> Bu } return allHooks - .OrderBy(h => h.order) - .ThenBy(h => h.registrationIndex) - .Select(h => h.hook) + .OrderBy(static h => h.order) + .ThenBy(static h => h.registrationIndex) + .Select(static h => h.hook) .ToList(); } @@ -316,7 +316,7 @@ public ValueTask>> foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex - finalHooks.AddRange(typeHooks.OrderBy(h => h.order).ThenBy(h => h.registrationIndex).Select(h => h.hook)); + finalHooks.AddRange(typeHooks.OrderBy(static h => h.order).ThenBy(static h => h.registrationIndex).Select(static h => h.hook)); } return finalHooks; @@ -375,7 +375,7 @@ public ValueTask>> foreach (var (_, typeHooks) in hooksByType) { // Within each type level, sort by Order then by RegistrationIndex - finalHooks.AddRange(typeHooks.OrderBy(h => h.order).ThenBy(h => h.registrationIndex).Select(h => h.hook)); + finalHooks.AddRange(typeHooks.OrderBy(static h => h.order).ThenBy(static h => h.registrationIndex).Select(static h => h.hook)); } return finalHooks; diff --git a/TUnit.Engine/Services/TestDependencyResolver.cs b/TUnit.Engine/Services/TestDependencyResolver.cs index 011adac15b..42c97a1e2c 100644 --- a/TUnit.Engine/Services/TestDependencyResolver.cs +++ b/TUnit.Engine/Services/TestDependencyResolver.cs @@ -9,6 +9,7 @@ internal sealed class TestDependencyResolver ]; private readonly Dictionary> _testsByType = new(); private readonly Dictionary> _testsByMethodName = new(); + private readonly Dictionary<(Type ClassType, string MethodName), AbstractExecutableTest> _testLookupCache = new(); private readonly List _testsWithPendingDependencies = [ ]; @@ -22,7 +23,7 @@ public void RegisterTest(AbstractExecutableTest test) lock (_resolutionLock) { _allTests.Add(test); - + var testType = test.Metadata.TestClassType; if (!_testsByType.TryGetValue(testType, out var testsForType)) { @@ -32,7 +33,7 @@ public void RegisterTest(AbstractExecutableTest test) _testsByType[testType] = testsForType; } testsForType.Add(test); - + var methodName = test.Metadata.TestMethodName; if (!_testsByMethodName.TryGetValue(methodName, out var testsForMethod)) { @@ -42,7 +43,10 @@ public void RegisterTest(AbstractExecutableTest test) _testsByMethodName[methodName] = testsForMethod; } testsForMethod.Add(test); - + + // Cache test by composite key for fast lookups in GetTransitiveDependencies + _testLookupCache[(testType, methodName)] = test; + ResolvePendingDependencies(); } } @@ -99,7 +103,7 @@ private bool ResolveDependenciesForTest(AbstractExecutableTest test) if (allResolved) { - var uniqueDependencies = new Dictionary(); + var uniqueDependencies = new Dictionary(capacity: 8); foreach (var dep in resolvedDependencies) { if (dep.Test == test) @@ -228,12 +232,14 @@ void CollectDependencies(TestDetails current) { return; } - - var test = _allTests.FirstOrDefault(t => - t.Metadata.TestClassType == current.ClassType && - t.Metadata.TestMethodName == current.TestName); - - if (test?.Dependencies != null) + + // Use cached lookup instead of linear search through all tests + if (!_testLookupCache.TryGetValue((current.ClassType, current.TestName), out var test)) + { + return; + } + + if (test.Dependencies != null) { foreach (var dep in test.Dependencies) { diff --git a/TUnit.Engine/Services/TestFinder.cs b/TUnit.Engine/Services/TestFinder.cs index aa573513fe..bf1e782825 100644 --- a/TUnit.Engine/Services/TestFinder.cs +++ b/TUnit.Engine/Services/TestFinder.cs @@ -20,8 +20,14 @@ public TestFinder(TestDiscoveryService discoveryService) /// public IEnumerable GetTests(Type classType) { - return _discoveryService.GetCachedTestContexts() - .Where(t => t.TestDetails?.ClassType == classType); + var allTests = _discoveryService.GetCachedTestContexts(); + foreach (var test in allTests) + { + if (test.TestDetails?.ClassType == classType) + { + yield return test; + } + } } /// @@ -30,26 +36,48 @@ public IEnumerable GetTests(Type classType) public TestContext[] GetTestsByNameAndParameters(string testName, IEnumerable methodParameterTypes, Type classType, IEnumerable classParameterTypes, IEnumerable classArguments) { - var paramTypes = methodParameterTypes?.ToArray() ?? [ - ]; - var classParamTypes = classParameterTypes?.ToArray() ?? [ - ]; + var paramTypes = methodParameterTypes?.ToArray() ?? []; + var classParamTypes = classParameterTypes?.ToArray() ?? []; var allTests = _discoveryService.GetCachedTestContexts(); + var results = new List(); // If no parameter types are specified, match by name and class type only if (paramTypes.Length == 0 && classParamTypes.Length == 0) { - return allTests.Where(t => - t.TestName == testName && - t.TestDetails?.ClassType == classType).ToArray(); + foreach (var test in allTests) + { + if (test.TestName == testName && test.TestDetails?.ClassType == classType) + { + results.Add(test); + } + } + return results.ToArray(); + } + + // Match with parameter types + foreach (var test in allTests) + { + if (test.TestName != testName || test.TestDetails?.ClassType != classType) + { + continue; + } + + var testParams = test.TestDetails.MethodMetadata.Parameters.ToArray(); + var testParamTypes = new Type[testParams.Length]; + for (int i = 0; i < testParams.Length; i++) + { + testParamTypes[i] = testParams[i].Type; + } + + if (ParameterTypesMatch(testParamTypes, paramTypes) && + ClassParametersMatch(test, classParamTypes, classArguments)) + { + results.Add(test); + } } - return allTests.Where(t => - t.TestName == testName && - t.TestDetails?.ClassType == classType && - ParameterTypesMatch(t.TestDetails.MethodMetadata.Parameters.Select(p => p.Type).ToArray(), paramTypes) && - ClassParametersMatch(t, classParamTypes, classArguments)).ToArray(); + return results.ToArray(); } private bool ParameterTypesMatch(Type[]? testParamTypes, Type[] expectedParamTypes) diff --git a/TUnit.Engine/Services/TestGroupingService.cs b/TUnit.Engine/Services/TestGroupingService.cs index 4ac3655d5b..786ec9131f 100644 --- a/TUnit.Engine/Services/TestGroupingService.cs +++ b/TUnit.Engine/Services/TestGroupingService.cs @@ -17,7 +17,7 @@ internal sealed class TestGroupingService : ITestGroupingService private struct TestSortKey { public int ExecutionPriority { get; init; } - public string? ClassFullName { get; init; } + public string ClassFullName { get; init; } // Cached to avoid repeated property access public int NotInParallelOrder { get; init; } public NotInParallelConstraint? NotInParallelConstraint { get; init; } } @@ -40,7 +40,7 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable GroupTestsByConstraintsAsync(IEnumerable(); - var keyedNotInParallelList = new List<(AbstractExecutableTest Test, IReadOnlyList ConstraintKeys, TestPriority Priority)>(); + var notInParallelList = new List<(AbstractExecutableTest Test, string ClassName, TestPriority Priority)>(); + var keyedNotInParallelList = new List<(AbstractExecutableTest Test, string ClassName, IReadOnlyList ConstraintKeys, TestPriority Priority)>(); var parallelTests = new List(); - var parallelGroups = new Dictionary>>(); - var constrainedParallelGroups = new Dictionary Unconstrained, List<(AbstractExecutableTest, IReadOnlyList, TestPriority)> Keyed)>(); + var parallelGroups = new Dictionary>>(capacity: 16); + var constrainedParallelGroups = new Dictionary Unconstrained, List<(AbstractExecutableTest, string, IReadOnlyList, TestPriority)> Keyed)>(capacity: 16); foreach (var (test, sortKey) in testsWithKeys) { @@ -77,11 +77,11 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable GroupTestsByConstraintsAsync(IEnumerable GroupTestsByConstraintsAsync(IEnumerable { - var classA = a.Test.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classB = b.Test.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classCompare = string.CompareOrdinal(classA, classB); + var classCompare = string.CompareOrdinal(a.ClassName, b.ClassName); if (classCompare != 0) return classCompare; - + var priorityCompare = b.Priority.Priority.CompareTo(a.Priority.Priority); if (priorityCompare != 0) return priorityCompare; - + return a.Priority.Order.CompareTo(b.Priority.Order); }); - + var sortedNotInParallel = new AbstractExecutableTest[notInParallelList.Count]; for (int i = 0; i < notInParallelList.Count; i++) { @@ -121,17 +119,15 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable { - var classA = a.Test.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classB = b.Test.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classCompare = string.CompareOrdinal(classA, classB); + var classCompare = string.CompareOrdinal(a.ClassName, b.ClassName); if (classCompare != 0) return classCompare; - + var priorityCompare = b.Priority.Priority.CompareTo(a.Priority.Priority); if (priorityCompare != 0) return priorityCompare; - + return a.Priority.Order.CompareTo(b.Priority.Order); }); - + var keyedArrays = new (AbstractExecutableTest, IReadOnlyList, int)[keyedNotInParallelList.Count]; for (int i = 0; i < keyedNotInParallelList.Count; i++) { @@ -140,7 +136,7 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable(); + var finalConstrainedGroups = new Dictionary(capacity: constrainedParallelGroups.Count); foreach (var kvp in constrainedParallelGroups) { var groupName = kvp.Key; @@ -149,22 +145,20 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable { - var classA = a.Item1.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classB = b.Item1.Context.ClassContext?.ClassType?.FullName ?? string.Empty; - var classCompare = string.CompareOrdinal(classA, classB); + var classCompare = string.CompareOrdinal(a.Item2, b.Item2); if (classCompare != 0) return classCompare; - - var priorityCompare = b.Item3.Priority.CompareTo(a.Item3.Priority); + + var priorityCompare = b.Item4.Priority.CompareTo(a.Item4.Priority); if (priorityCompare != 0) return priorityCompare; - - return a.Item3.Order.CompareTo(b.Item3.Order); + + return a.Item4.Order.CompareTo(b.Item4.Order); }); - + var sortedKeyed = new (AbstractExecutableTest, IReadOnlyList, int)[keyed.Count]; for (int i = 0; i < keyed.Count; i++) { var item = keyed[i]; - sortedKeyed[i] = (item.Item1, item.Item2, item.Item3.GetHashCode()); + sortedKeyed[i] = (item.Item1, item.Item3, item.Item4.GetHashCode()); } finalConstrainedGroups[groupName] = new GroupedConstrainedTests @@ -188,9 +182,10 @@ public ValueTask GroupTestsByConstraintsAsync(IEnumerable notInParallelList, - List<(AbstractExecutableTest Test, IReadOnlyList ConstraintKeys, TestPriority Priority)> keyedNotInParallelList) + List<(AbstractExecutableTest Test, string ClassName, TestPriority Priority)> notInParallelList, + List<(AbstractExecutableTest Test, string ClassName, IReadOnlyList ConstraintKeys, TestPriority Priority)> keyedNotInParallelList) { var order = constraint.Order; var priority = test.Context.ExecutionPriority; @@ -198,12 +193,12 @@ private static void ProcessNotInParallelConstraint( if (constraint.NotInParallelConstraintKeys.Count == 0) { - notInParallelList.Add((test, testPriority)); + notInParallelList.Add((test, className, testPriority)); } else { // Add test only once with all its constraint keys - keyedNotInParallelList.Add((test, constraint.NotInParallelConstraintKeys, testPriority)); + keyedNotInParallelList.Add((test, className, constraint.NotInParallelConstraintKeys, testPriority)); } } @@ -229,29 +224,30 @@ private static void ProcessParallelGroupConstraint( private static void ProcessCombinedConstraints( AbstractExecutableTest test, + string className, ParallelGroupConstraint parallelGroup, NotInParallelConstraint notInParallel, - Dictionary Unconstrained, List<(AbstractExecutableTest, IReadOnlyList, TestPriority)> Keyed)> constrainedGroups) + Dictionary Unconstrained, List<(AbstractExecutableTest, string, IReadOnlyList, TestPriority)> Keyed)> constrainedGroups) { if (!constrainedGroups.TryGetValue(parallelGroup.Group, out var group)) { - group = (new List(), new List<(AbstractExecutableTest, IReadOnlyList, TestPriority)>()); + group = (new List(), new List<(AbstractExecutableTest, string, IReadOnlyList, TestPriority)>()); constrainedGroups[parallelGroup.Group] = group; } - + // Add to keyed tests within the parallel group var order = notInParallel.Order; var priority = test.Context.ExecutionPriority; var testPriority = new TestPriority(priority, order); - + if (notInParallel.NotInParallelConstraintKeys.Count > 0) { - group.Keyed.Add((test, notInParallel.NotInParallelConstraintKeys, testPriority)); + group.Keyed.Add((test, className, notInParallel.NotInParallelConstraintKeys, testPriority)); } else { // NotInParallel without keys means sequential within the group - group.Keyed.Add((test, new List { "__global__" }, testPriority)); + group.Keyed.Add((test, className, new List { "__global__" }, testPriority)); } } } diff --git a/TUnit.Engine/Services/TestLifecycleCoordinator.cs b/TUnit.Engine/Services/TestLifecycleCoordinator.cs index 22fcb94238..c1da01ddbe 100644 --- a/TUnit.Engine/Services/TestLifecycleCoordinator.cs +++ b/TUnit.Engine/Services/TestLifecycleCoordinator.cs @@ -35,14 +35,14 @@ public void RegisterTests(List testList) // Initialize assembly counters foreach (var assemblyGroup in testList.GroupBy(t => t.Metadata.TestClassType.Assembly)) { - var counter = _assemblyTestCounts.GetOrAdd(assemblyGroup.Key, _ => new Counter()); + var counter = _assemblyTestCounts.GetOrAdd(assemblyGroup.Key, static _ => new Counter()); counter.Add(assemblyGroup.Count()); } // Initialize class counters foreach (var classGroup in testList.GroupBy(t => t.Metadata.TestClassType)) { - var counter = _classTestCounts.GetOrAdd(classGroup.Key, _ => new Counter()); + var counter = _classTestCounts.GetOrAdd(classGroup.Key, static _ => new Counter()); counter.Add(classGroup.Count()); } } @@ -61,7 +61,7 @@ public AfterHookExecutionFlags DecrementAndCheckAfterHooks( if (_classTestCounts.TryGetValue(testClass, out var classCounter)) { var remainingClassTests = classCounter.Decrement(); - if (remainingClassTests == 0 && _afterClassExecuted.GetOrAdd(testClass, _ => true)) + if (remainingClassTests == 0 && _afterClassExecuted.GetOrAdd(testClass, static _ => true)) { flags.ShouldExecuteAfterClass = true; } @@ -71,7 +71,7 @@ public AfterHookExecutionFlags DecrementAndCheckAfterHooks( if (_assemblyTestCounts.TryGetValue(testAssembly, out var assemblyCounter)) { var remainingAssemblyTests = assemblyCounter.Decrement(); - if (remainingAssemblyTests == 0 && _afterAssemblyExecuted.GetOrAdd(testAssembly, _ => true)) + if (remainingAssemblyTests == 0 && _afterAssemblyExecuted.GetOrAdd(testAssembly, static _ => true)) { flags.ShouldExecuteAfterAssembly = true; } diff --git a/TUnit.Engine/Services/VerbosityService.cs b/TUnit.Engine/Services/VerbosityService.cs index 9339f1b88d..cb25e7a1a9 100644 --- a/TUnit.Engine/Services/VerbosityService.cs +++ b/TUnit.Engine/Services/VerbosityService.cs @@ -32,41 +32,6 @@ public VerbosityService(ICommandLineOptions commandLineOptions, IServiceProvider /// public bool HideTestOutput => !_isDetailedOutput; - /// - /// Whether to show the TUnit logo - /// - public bool ShowLogo => true; - - /// - /// Whether to enable discovery diagnostics (enabled with Debug/Trace log level) - /// - public bool EnableDiscoveryDiagnostics => _logLevel <= LogLevel.Debug; - - /// - /// Whether to enable verbose source generator diagnostics (enabled with Debug/Trace log level) - /// - public bool EnableVerboseSourceGeneratorDiagnostics => _logLevel <= LogLevel.Debug; - - /// - /// Whether to show execution timing details (enabled with Debug/Trace log level) - /// - public bool ShowExecutionTiming => _logLevel <= LogLevel.Debug; - - /// - /// Whether to show parallel execution details (enabled with Debug/Trace log level) - /// - public bool ShowParallelExecutionDetails => _logLevel <= LogLevel.Debug; - - /// - /// Whether to show test discovery progress (enabled with Debug/Trace log level) - /// - public bool ShowDiscoveryProgress => _logLevel <= LogLevel.Debug; - - /// - /// Whether to show memory and resource usage (enabled with Debug/Trace log level) - /// - public bool ShowResourceUsage => _logLevel <= LogLevel.Debug; - /// /// Creates a summary of current output and diagnostic settings /// @@ -74,8 +39,7 @@ public string CreateVerbositySummary() { var outputMode = _isDetailedOutput ? "Detailed" : "Normal"; return $"Output: {outputMode}, Log Level: {_logLevel} " + - $"(Stack traces: {ShowDetailedStackTrace}, " + - $"Discovery diagnostics: {EnableDiscoveryDiagnostics})"; + $"(Stack traces: {ShowDetailedStackTrace}, "; } // Use centralized environment variable cache @@ -122,4 +86,4 @@ private static bool IsConsoleEnvironment(IServiceProvider serviceProvider) return true; } } -} \ No newline at end of file +} diff --git a/TUnit.Engine/TestDiscoveryService.cs b/TUnit.Engine/TestDiscoveryService.cs index ad230e3453..9d8c022f33 100644 --- a/TUnit.Engine/TestDiscoveryService.cs +++ b/TUnit.Engine/TestDiscoveryService.cs @@ -119,7 +119,7 @@ public async Task DiscoverTests(string testSessionId, ITest filteredTests = testsToInclude.ToList(); } - contextProvider.TestDiscoveryContext.AddTests(allTests.Select(t => t.Context)); + contextProvider.TestDiscoveryContext.AddTests(allTests.Select(static t => t.Context)); await _testExecutor.ExecuteAfterTestDiscoveryHooksAsync(cancellationToken).ConfigureAwait(false); @@ -217,7 +217,7 @@ public async IAsyncEnumerable DiscoverTestsFullyStreamin } // Process dependent tests in dependency order - var yieldedTests = new HashSet(independentTests.Select(t => t.TestId)); + var yieldedTests = new HashSet(independentTests.Select(static t => t.TestId)); var remainingTests = new List(dependentTests); while (remainingTests.Count > 0) @@ -278,6 +278,6 @@ private bool AreAllDependenciesSatisfied(AbstractExecutableTest test, Concurrent public IEnumerable GetCachedTestContexts() { - return _cachedTests.Select(t => t.Context); + return _cachedTests.Select(static t => t.Context); } } diff --git a/TUnit.Engine/TestSessionCoordinator.cs b/TUnit.Engine/TestSessionCoordinator.cs index 0f7602236f..db303b1cf1 100644 --- a/TUnit.Engine/TestSessionCoordinator.cs +++ b/TUnit.Engine/TestSessionCoordinator.cs @@ -68,7 +68,7 @@ public async Task ExecuteTests( private void InitializeEventReceivers(List testList, CancellationToken cancellationToken) { - var testContexts = testList.Select(t => t.Context); + var testContexts = testList.Select(static t => t.Context); _eventReceiverOrchestrator.InitializeTestCounts(testContexts); } diff --git a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj index 2102e65e7b..ba0160bad2 100644 --- a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj +++ b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj @@ -10,8 +10,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj index 9d4abd9a31..dbe8ebcf39 100644 --- a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj @@ -9,7 +9,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj index a2518968f4..046439d130 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj @@ -11,7 +11,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj index ae34fbd78f..c7b954fb0d 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj @@ -10,7 +10,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj b/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj index b3934bcddb..74ac797d8c 100644 --- a/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj +++ b/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj @@ -10,8 +10,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj index 72c014a253..7d526949a9 100644 --- a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj @@ -8,7 +8,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.VB/TestProject.vbproj b/TUnit.Templates/content/TUnit.VB/TestProject.vbproj index 565670e38f..3bbabe2344 100644 --- a/TUnit.Templates/content/TUnit.VB/TestProject.vbproj +++ b/TUnit.Templates/content/TUnit.VB/TestProject.vbproj @@ -8,6 +8,6 @@ - + diff --git a/TUnit.Templates/content/TUnit/TestProject.csproj b/TUnit.Templates/content/TUnit/TestProject.csproj index 32c0d36462..64d257c1af 100644 --- a/TUnit.Templates/content/TUnit/TestProject.csproj +++ b/TUnit.Templates/content/TUnit/TestProject.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/docs/docs/comparison/attributes.md b/docs/docs/comparison/attributes.md index ad7cd45e2e..3553f36bcf 100644 --- a/docs/docs/comparison/attributes.md +++ b/docs/docs/comparison/attributes.md @@ -59,3 +59,10 @@ Here are TUnit's equivalent attributes to other test frameworks. | [Category] | [Trait("Category","")] | [Category] | [TestCategory] | | [Property] | [Trait] | [Property] | [TestProperty] | +## Culture-sensitive Attributes + +| TUnit | xUnit | NUnit | MSTest | +|--------------------|-------|------------------------|--------| +| [Culture("en-US")] | - | [SetCulture("en-US")] | - | +| - | - | [Culture("en-US")] | - | +| - | - | [SetUICulture("en-US") | - | diff --git a/docs/docs/test-authoring/culture.md b/docs/docs/test-authoring/culture.md new file mode 100644 index 0000000000..2b3bd78662 --- /dev/null +++ b/docs/docs/test-authoring/culture.md @@ -0,0 +1,28 @@ +# Culture + +The `[Culture]` attribute is used to set the [current Culture](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.currentculture) for the duration of a test. It may be specified at the level of a test, fixture or assembly. +The culture remains set until the test or fixture completes and is then reset to its original value. + +Specifying the culture is useful for comparing against expected output +that depends on the culture, e.g. decimal separators, etc. + +Only one culture may be specified. If you wish to run the same test under multiple cultures, +you can achieve the same result by factoring out your test code into a private method +that is called by each individual test method. + +## Examples + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + [Test, Culture("de-AT")] + public async Task Test3() + { + await Assert.That(double.Parse("3,5")).IsEqualTo(3.5); + } +} +``` diff --git a/docs/sidebars.js b/docs/sidebars.js deleted file mode 100644 index 80a648875f..0000000000 --- a/docs/sidebars.js +++ /dev/null @@ -1,133 +0,0 @@ -module.exports = { - docs: [ - { - type: 'category', - label: 'Introduction', - items: [ - 'introduction/intro', - 'introduction/faq', - ], - }, - { - type: 'category', - label: 'Getting Started', - items: [ - 'getting-started/installation', - 'getting-started/libraries', - 'getting-started/writing-your-first-test', - 'getting-started/running-your-tests', - 'getting-started/congratulations', - 'getting-started/migration-from-xunit', - ], - }, - { - type: 'category', - label: 'Test Authoring', - items: [ - 'test-authoring/things-to-know', - { - type: 'category', - label: 'Data Driven Testing', - items: [ - 'test-authoring/arguments', - 'test-authoring/method-data-source', - 'test-authoring/class-data-source', - 'test-authoring/matrix-tests', - { - type: 'link', - label: 'Data Source Generators', - href: '/docs/customization-extensibility/data-source-generators', - }, - ], - }, - 'test-authoring/skip', - 'test-authoring/explicit', - 'test-authoring/depends-on', - 'test-authoring/order', - ], - }, - { - type: 'category', - label: 'Assertions', - items: [ - - ], - }, - { - type: 'category', - label: 'Test Lifecycle', - items: [ - 'test-lifecycle/setup', - 'test-lifecycle/cleanup', - 'test-lifecycle/test-context', - 'test-lifecycle/properties', - 'test-lifecycle/class-constructors', - 'test-lifecycle/dependency-injection', - 'test-lifecycle/property-injection', - 'test-lifecycle/event-subscribing', - ], - }, - { - type: 'category', - label: 'Execution Control', - items: [ - 'execution/retrying', - 'execution/repeating', - 'execution/timeouts', - 'execution/test-filters', - 'execution/executors', - ], - }, - { - type: 'category', - label: 'Parallelism Control', - items: [ - 'parallelism/not-in-parallel', - 'parallelism/parallel-groups', - 'parallelism/parallel-limiter', - ], - }, - { - type: 'category', - label: 'Customization & Extensibility', - items: [ - 'customization-extensibility/extensions', - 'customization-extensibility/data-source-generators', - 'customization-extensibility/argument-formatters', - 'customization-extensibility/logging', - 'customization-extensibility/display-names', - ], - }, - { - type: 'category', - label: 'Examples & Use Cases', - items: [ - 'examples/intro', - 'examples/aspnet', - 'examples/playwright', - 'examples/complex-test-infrastructure', - 'examples/instrumenting-global-test-ids', - 'examples/tunit-ci-pipeline', - 'examples/fsharp-interactive', - ], - }, - { - type: 'category', - label: 'Reference', - items: [ - 'reference/attributes', - 'reference/framework-differences', - 'reference/command-line-flags', - 'reference/engine-modes', - 'reference/test-configuration', - ], - }, - { - type: 'category', - label: 'Experimental Features', - items: [ - 'experimental/dynamic-tests', - ], - }, - ], -}; diff --git a/docs/sidebars.ts b/docs/sidebars.ts index fe2e79a684..cd0975c5c1 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -50,6 +50,7 @@ const sidebars: SidebarsConfig = { 'test-authoring/depends-on', 'test-authoring/order', 'test-authoring/mocking', + 'test-authoring/culture' ], }, {