diff --git a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs index 1597914248..4b9ce67e1e 100644 --- a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs @@ -183,6 +183,9 @@ public MethodDataSourceAttribute( yield break; } + // Compute paramTypes once to avoid repeated LINQ allocations + var paramTypes = GetParameterTypes(dataGeneratorMetadata.TestInformation?.Parameters); + // If it's IAsyncEnumerable, handle it specially if (IsAsyncEnumerable(methodResult.GetType())) { @@ -192,7 +195,6 @@ public MethodDataSourceAttribute( hasAnyItems = true; yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); }; } @@ -218,7 +220,6 @@ public MethodDataSourceAttribute( hasAnyItems = true; yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); }; } @@ -234,7 +235,6 @@ public MethodDataSourceAttribute( { yield return async () => { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); return await Task.FromResult(taskResult.ToObjectArrayWithTypes(paramTypes)); }; } @@ -247,7 +247,6 @@ public MethodDataSourceAttribute( foreach (var item in enumerable) { hasAnyItems = true; - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); yield return () => Task.FromResult(item.ToObjectArrayWithTypes(paramTypes)); } @@ -260,7 +259,6 @@ public MethodDataSourceAttribute( } else { - var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray(); yield return async () => { return await Task.FromResult(methodResult.ToObjectArrayWithTypes(paramTypes)); @@ -268,6 +266,21 @@ public MethodDataSourceAttribute( } } + private static Type[]? GetParameterTypes(ParameterMetadata[]? parameters) + { + if (parameters == null || parameters.Length == 0) + { + return null; + } + + var types = new Type[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) + { + types[i] = parameters[i].Type; + } + return types; + } + private static bool IsAsyncEnumerable([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type) { return type.GetInterfaces() diff --git a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs index 2a9802612c..f2faf32e0c 100644 --- a/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs +++ b/TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs @@ -21,6 +21,11 @@ internal static class ReflectionAttributeExtractor /// private static readonly ConcurrentDictionary _attributesCache = new(); + /// + /// Cache for all attributes lookups (method + class + assembly combined) + /// + private static readonly ConcurrentDictionary<(Type, MethodInfo), Attribute[]> _allAttributesCache = new(); + /// /// Composite cache key combining type, method, and attribute type information /// @@ -181,15 +186,18 @@ public static IDataSourceAttribute[] ExtractDataSources(ICustomAttributeProvider public static Attribute[] GetAllAttributes(Type testClass, MethodInfo testMethod) { - var attributes = new List(); + return _allAttributesCache.GetOrAdd((testClass, testMethod), key => + { + var attributes = new List(); - // Add in reverse order of precedence so method attributes come first - // This ensures ScopedAttributeFilter will keep method-level attributes over class/assembly - attributes.AddRange(testMethod.GetCustomAttributes()); - attributes.AddRange(testClass.GetCustomAttributes()); - attributes.AddRange(testClass.Assembly.GetCustomAttributes()); + // Add in reverse order of precedence so method attributes come first + // This ensures ScopedAttributeFilter will keep method-level attributes over class/assembly + attributes.AddRange(key.Item2.GetCustomAttributes()); + attributes.AddRange(key.Item1.GetCustomAttributes()); + attributes.AddRange(key.Item1.Assembly.GetCustomAttributes()); - return attributes.ToArray(); + return attributes.ToArray(); + }); } [UnconditionalSuppressMessage("Trimming", diff --git a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index ab4e305060..6d2622ba62 100644 --- a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -33,27 +33,18 @@ internal sealed class ReflectionTestDataCollector : ITestDataCollector private static readonly ConcurrentDictionary _assemblyTypesCache = new(); private static readonly ConcurrentDictionary _typeMethodsCache = new(); - private static Assembly[]? _cachedAssemblies; - private static readonly Lock _assemblyCacheLock = new(); + private static Assembly[] Assemblies => field ??= FindAssemblies(); - private static Assembly[] GetCachedAssemblies() + private static Assembly[] FindAssemblies() { - lock (_assemblyCacheLock) - { - return _cachedAssemblies ??= AppDomain.CurrentDomain.GetAssemblies(); - } - } + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - public static void ClearCaches() - { - _scannedAssemblies.Clear(); - Interlocked.Exchange(ref _discoveredTests, ImmutableList.Empty); - _assemblyTypesCache.Clear(); - _typeMethodsCache.Clear(); - lock (_assemblyCacheLock) + if (assemblies.Length == 0) { - _cachedAssemblies = null; + return [ Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly() ]; } + + return assemblies; } private async Task> ProcessAssemblyAsync(Assembly assembly, SemaphoreSlim semaphore) @@ -92,7 +83,7 @@ public async Task> CollectTestsAsync(string testSessio } #endif - var allAssemblies = GetCachedAssemblies(); + var allAssemblies = Assemblies; var assembliesList = new List(allAssemblies.Length); foreach (var assembly in allAssemblies) { @@ -146,7 +137,7 @@ public async IAsyncEnumerable CollectTestsStreamingAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Get assemblies to scan - var allAssemblies = GetCachedAssemblies(); + var allAssemblies = Assemblies; var assemblies = new List(allAssemblies.Length); foreach (var assembly in allAssemblies) { @@ -1638,7 +1629,7 @@ private async Task> DiscoverDynamicTests(string testSessionId } } - var allAssemblies = GetCachedAssemblies(); + var allAssemblies = Assemblies; var assembliesList = new List(allAssemblies.Length); foreach (var assembly in allAssemblies) { @@ -1704,7 +1695,7 @@ private async IAsyncEnumerable DiscoverDynamicTestsStreamingAsync( string testSessionId, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var allAssemblies = GetCachedAssemblies(); + var allAssemblies = Assemblies; var assemblies = new List(allAssemblies.Length); foreach (var assembly in allAssemblies) { diff --git a/TUnit.Engine/Extensions/TestExtensions.cs b/TUnit.Engine/Extensions/TestExtensions.cs index cf10efe7de..4a4a34345f 100644 --- a/TUnit.Engine/Extensions/TestExtensions.cs +++ b/TUnit.Engine/Extensions/TestExtensions.cs @@ -10,6 +10,8 @@ namespace TUnit.Engine.Extensions; internal static class TestExtensions { + private static bool? _cachedIsTrxEnabled; + internal static TestNode ToTestNode(this TestContext testContext, TestNodeStateProperty stateProperty) { var testDetails = testContext.Metadata.TestDetails ?? throw new ArgumentNullException(nameof(testContext.Metadata.TestDetails)); @@ -34,7 +36,7 @@ internal static TestNode ToTestNode(this TestContext testContext, TestNodeStateP assemblyFullName: testDetails.MethodMetadata.Class.Type.Assembly.GetName().FullName, typeName: testContext.GetClassTypeName(), methodName: testDetails.MethodName, - parameterTypeFullNames: CreateParameterTypeArray(testDetails.MethodMetadata.Parameters.Select(static p => p.Type).ToArray()), + parameterTypeFullNames: CreateParameterTypeArray(testDetails.MethodMetadata.Parameters), returnTypeFullName: testDetails.ReturnType.FullName ?? typeof(void).FullName!, methodArity: testDetails.MethodMetadata.GenericTypeCount ) @@ -172,17 +174,25 @@ private static int EstimateCount(TestContext testContext, TestNodeStateProperty private static bool IsTrxEnabled(TestContext testContext) { + if (_cachedIsTrxEnabled.HasValue) + { + return _cachedIsTrxEnabled.Value; + } + if(testContext.Services.GetService() is not {} capabilities) { + _cachedIsTrxEnabled = false; return false; } if(capabilities.GetCapability() is not TrxReportCapability trxCapability) { + _cachedIsTrxEnabled = false; return false; } - return trxCapability.IsTrxEnabled; + _cachedIsTrxEnabled = trxCapability.IsTrxEnabled; + return _cachedIsTrxEnabled.Value; } private static IEnumerable ExtractProperties(TestDetails testDetails) @@ -204,8 +214,15 @@ private static TimingProperty GetTimingProperty(TestContext testContext, DateTim } var end = testContext.Execution.TestEnd ?? DateTimeOffset.Now; + var timings = testContext.Timings; + var stepTimings = new StepTimingInfo[timings.Count]; + var i = 0; + foreach (var timing in timings) + { + stepTimings[i++] = new StepTimingInfo(timing.StepName, timing.StepName, new TimingInfo(timing.Start, timing.End, timing.Duration)); + } - return new TimingProperty(new TimingInfo(overallStart, end, end - overallStart), testContext.Timings.Select(x => new StepTimingInfo(x.StepName, x.StepName, new TimingInfo(x.Start, x.End, x.Duration))).ToArray()); + return new TimingProperty(new TimingInfo(overallStart, end, end - overallStart), stepTimings); } private static IEnumerable GetTrxMessages(TestContext testContext, string? standardOutput, string? standardError) @@ -229,17 +246,17 @@ private static IEnumerable GetTrxMessages(TestContext testContext, s /// /// Efficiently create parameter type array without LINQ materialization /// - private static string[] CreateParameterTypeArray(IReadOnlyList? parameterTypes) + private static string[] CreateParameterTypeArray(ParameterMetadata[] parameters) { - if (parameterTypes == null || parameterTypes.Count == 0) + if (parameters.Length == 0) { return []; } - var array = new string[parameterTypes.Count]; - for (var i = 0; i < parameterTypes.Count; i++) + var array = new string[parameters.Length]; + for (var i = 0; i < parameters.Length; i++) { - array[i] = parameterTypes[i].FullName!; + array[i] = parameters[i].Type.FullName!; } return array; } diff --git a/TUnit.Engine/Helpers/DataUnwrapper.cs b/TUnit.Engine/Helpers/DataUnwrapper.cs index 9f985203b7..df89c4fc97 100644 --- a/TUnit.Engine/Helpers/DataUnwrapper.cs +++ b/TUnit.Engine/Helpers/DataUnwrapper.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using TUnit.Core; using TUnit.Core.Helpers; @@ -38,7 +37,11 @@ internal class DataUnwrapper // Otherwise use the default unwrapping if(values.Length == 1 && DataSourceHelpers.IsTuple(values[0])) { - var paramTypes = expectedParameters.Select(p => p.Type).ToArray(); + var paramTypes = new Type[expectedParameters.Length]; + for (var i = 0; i < expectedParameters.Length; i++) + { + paramTypes[i] = expectedParameters[i].Type; + } return values[0].ToObjectArrayWithTypes(paramTypes); }