Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
{
Expand All @@ -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<object?[]?>(item.ToObjectArrayWithTypes(paramTypes));
};
}
Expand All @@ -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<object?[]?>(item.ToObjectArrayWithTypes(paramTypes));
};
}
Expand All @@ -234,7 +235,6 @@ public MethodDataSourceAttribute(
{
yield return async () =>
{
var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray();
return await Task.FromResult<object?[]?>(taskResult.ToObjectArrayWithTypes(paramTypes));
};
}
Expand All @@ -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<object?[]?>(item.ToObjectArrayWithTypes(paramTypes));
}

Expand All @@ -260,14 +259,28 @@ public MethodDataSourceAttribute(
}
else
{
var paramTypes = dataGeneratorMetadata.TestInformation?.Parameters.Select(static p => p.Type).ToArray();
yield return async () =>
{
return await Task.FromResult<object?[]?>(methodResult.ToObjectArrayWithTypes(paramTypes));
};
}
}

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()
Expand Down
22 changes: 15 additions & 7 deletions TUnit.Engine/Discovery/ReflectionAttributeExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ internal static class ReflectionAttributeExtractor
/// </summary>
private static readonly ConcurrentDictionary<AttributeCacheKey, Attribute[]> _attributesCache = new();

/// <summary>
/// Cache for all attributes lookups (method + class + assembly combined)
/// </summary>
private static readonly ConcurrentDictionary<(Type, MethodInfo), Attribute[]> _allAttributesCache = new();

/// <summary>
/// Composite cache key combining type, method, and attribute type information
/// </summary>
Expand Down Expand Up @@ -181,15 +186,18 @@ public static IDataSourceAttribute[] ExtractDataSources(ICustomAttributeProvider

public static Attribute[] GetAllAttributes(Type testClass, MethodInfo testMethod)
{
var attributes = new List<Attribute>();
return _allAttributesCache.GetOrAdd((testClass, testMethod), key =>
{
var attributes = new List<Attribute>();

// 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",
Expand Down
31 changes: 11 additions & 20 deletions TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,18 @@ internal sealed class ReflectionTestDataCollector : ITestDataCollector
private static readonly ConcurrentDictionary<Assembly, Type[]> _assemblyTypesCache = new();
private static readonly ConcurrentDictionary<Type, MethodInfo[]> _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<TestMetadata>.Empty);
_assemblyTypesCache.Clear();
_typeMethodsCache.Clear();
lock (_assemblyCacheLock)
if (assemblies.Length == 0)
{
_cachedAssemblies = null;
return [ Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly() ];
}

return assemblies;
}

private async Task<List<TestMetadata>> ProcessAssemblyAsync(Assembly assembly, SemaphoreSlim semaphore)
Expand Down Expand Up @@ -92,7 +83,7 @@ public async Task<IEnumerable<TestMetadata>> CollectTestsAsync(string testSessio
}
#endif

var allAssemblies = GetCachedAssemblies();
var allAssemblies = Assemblies;
var assembliesList = new List<Assembly>(allAssemblies.Length);
foreach (var assembly in allAssemblies)
{
Expand Down Expand Up @@ -146,7 +137,7 @@ public async IAsyncEnumerable<TestMetadata> CollectTestsStreamingAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Get assemblies to scan
var allAssemblies = GetCachedAssemblies();
var allAssemblies = Assemblies;
var assemblies = new List<Assembly>(allAssemblies.Length);
foreach (var assembly in allAssemblies)
{
Expand Down Expand Up @@ -1638,7 +1629,7 @@ private async Task<List<TestMetadata>> DiscoverDynamicTests(string testSessionId
}
}

var allAssemblies = GetCachedAssemblies();
var allAssemblies = Assemblies;
var assembliesList = new List<Assembly>(allAssemblies.Length);
foreach (var assembly in allAssemblies)
{
Expand Down Expand Up @@ -1704,7 +1695,7 @@ private async IAsyncEnumerable<TestMetadata> DiscoverDynamicTestsStreamingAsync(
string testSessionId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var allAssemblies = GetCachedAssemblies();
var allAssemblies = Assemblies;
var assemblies = new List<Assembly>(allAssemblies.Length);
foreach (var assembly in allAssemblies)
{
Expand Down
33 changes: 25 additions & 8 deletions TUnit.Engine/Extensions/TestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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
)
Expand Down Expand Up @@ -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<ITestFrameworkCapabilities>() is not {} capabilities)
{
_cachedIsTrxEnabled = false;
return false;
}

if(capabilities.GetCapability<ITrxReportCapability>() is not TrxReportCapability trxCapability)
{
_cachedIsTrxEnabled = false;
return false;
}

return trxCapability.IsTrxEnabled;
_cachedIsTrxEnabled = trxCapability.IsTrxEnabled;
return _cachedIsTrxEnabled.Value;
}

private static IEnumerable<TestMetadataProperty> ExtractProperties(TestDetails testDetails)
Expand All @@ -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<TrxMessage> GetTrxMessages(TestContext testContext, string? standardOutput, string? standardError)
Expand All @@ -229,17 +246,17 @@ private static IEnumerable<TrxMessage> GetTrxMessages(TestContext testContext, s
/// <summary>
/// Efficiently create parameter type array without LINQ materialization
/// </summary>
private static string[] CreateParameterTypeArray(IReadOnlyList<Type>? 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;
}
Expand Down
7 changes: 5 additions & 2 deletions TUnit.Engine/Helpers/DataUnwrapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using TUnit.Core;
using TUnit.Core.Helpers;

Expand Down Expand Up @@ -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);
}

Expand Down
Loading