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
49 changes: 17 additions & 32 deletions TUnit.Core/Helpers/DataSourceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public static class DataSourceHelpers
return null;
}

if(value.GetType().IsGenericType && value.GetType().GetGenericTypeDefinition() == typeof(Func<>))
var type = value.GetType();
if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Func<>))
{
return ((Delegate)value).DynamicInvoke();
}
Expand Down Expand Up @@ -74,6 +75,18 @@ public static T InvokeIfFunc<T>(object? value)
return [() => Task.FromResult<object?>(value)];
}

#if NET5_0_OR_GREATER || NETCOREAPP3_0_OR_GREATER
private static object?[] CopyTupleElements(ITuple tuple, int count)
{
var result = new object?[count];
for (var i = 0; i < count; i++)
{
result[i] = tuple[i];
}
return result;
}
#endif

/// <summary>
/// AOT-compatible tuple unwrapping that handles common tuple types without reflection
/// </summary>
Expand All @@ -88,13 +101,7 @@ public static T InvokeIfFunc<T>(object? value)
// Try to use ITuple interface first for any ValueTuple type (available in .NET Core 3.0+)
if (value is ITuple tuple)
{
var length = tuple.Length;
var result = new object?[length];
for (var i = 0; i < length; i++)
{
result[i] = tuple[i];
}
return result;
return CopyTupleElements(tuple, tuple.Length);
}
#endif

Expand Down Expand Up @@ -426,30 +433,8 @@ public static T InvokeIfFunc<T>(object? value)
// Try to use ITuple interface first for any ValueTuple type
if (value is ITuple tuple)
{
var result = new List<object?>();
var typeIndex = 0;

for (var i = 0; i < tuple.Length && typeIndex < expectedTypes.Length; i++)
{
var element = tuple[i];
var expectedType = expectedTypes[typeIndex];

// Check if the expected type is a tuple type
if (TupleHelper.IsTupleType(expectedType) && IsTuple(element))
{
// Keep nested tuple as-is
result.Add(element);
typeIndex++;
}
else
{
// Add element normally
result.Add(element);
typeIndex++;
}
}

return result.ToArray();
// Elements are copied 1:1, capped at the expected parameter count
return CopyTupleElements(tuple, Math.Min(tuple.Length, expectedTypes.Length));
}
#endif

Expand Down
15 changes: 2 additions & 13 deletions TUnit.Engine/Building/TestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
TestBuilderContext.Current = testBuilderContext;

// Check for ClassConstructor attribute and set it early if present (reuse already created attributes)
ClassConstructorAttribute? classConstructorAttribute = null;
foreach (var attr in attributes)
{
if (attr is ClassConstructorAttribute cca)
{
classConstructorAttribute = cca;
break;
}
}
var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();
if (classConstructorAttribute != null)
{
testBuilderContext.ClassConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!;
Expand Down Expand Up @@ -1605,10 +1597,7 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
// Check for ClassConstructor attribute and set it early if present
// Look for any attribute that inherits from ClassConstructorAttribute
// This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
var classConstructorAttribute = attributes
.Where(a => a is ClassConstructorAttribute)
.Cast<ClassConstructorAttribute>()
.FirstOrDefault();
var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();

if (classConstructorAttribute != null)
{
Expand Down
31 changes: 15 additions & 16 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using TUnit.Core.Interfaces;
using TUnit.Core.Services;
using TUnit.Engine.Building.Interfaces;
using TUnit.Engine.Extensions;
using TUnit.Engine.Services;
using TUnit.Engine.Utilities;

Expand Down Expand Up @@ -56,15 +57,7 @@ private TestBuilderContext CreateTestBuilderContext(TestMetadata metadata)

// Look for any attribute that inherits from ClassConstructorAttribute
// This handles both ClassConstructorAttribute and ClassConstructorAttribute<T>
ClassConstructorAttribute? classConstructorAttribute = null;
foreach (var attr in attributes)
{
if (attr is ClassConstructorAttribute cca)
{
classConstructorAttribute = cca;
break;
}
}
var classConstructorAttribute = attributes.FirstOfType<ClassConstructorAttribute>();

if (classConstructorAttribute != null)
{
Expand Down Expand Up @@ -210,6 +203,11 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
// Get dynamic test metadata for DisplayName support
var dynamicTestMetadata = metadata as IDynamicTestMetadata;

// Hoist loop-invariant state so repeat iterations share one attribute dictionary and factory delegate
var attributes = metadata.GetOrCreateAttributes();
var attributesByType = attributes.ToAttributeDictionary();
Func<Task<object>> instanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, []));

return await Enumerable.Range(0, repeatCount + 1)
.SelectAsync(async repeatIndex =>
{
Expand All @@ -218,7 +216,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
var dynamicTestIndex = dynamicTestMetadata?.DynamicTestIndex ?? 0;
var testData = new TestBuilder.TestData
{
TestClassInstanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, [])),
TestClassInstanceFactory = instanceFactory,
ClassDataSourceAttributeIndex = 0,
ClassDataLoopIndex = 0,
ClassData = [],
Expand All @@ -239,9 +237,6 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
? $"{baseDisplayName} (Repeat {repeatIndex + 1}/{repeatCount + 1})"
: baseDisplayName;

// Get attributes first
var attributes = metadata.GetOrCreateAttributes();

// Create TestDetails for dynamic tests
var testDetails = new TestDetails(attributes)
{
Expand All @@ -259,7 +254,7 @@ private async Task<AbstractExecutableTest[]> GenerateDynamicTests(TestMetadata m
TestEndColumnNumber = metadata.EndColumnNumber,
ReturnType = typeof(Task),
MethodMetadata = metadata.MethodMetadata,
AttributesByType = attributes.ToAttributeDictionary()
AttributesByType = attributesByType
};

var testBuilderContext = CreateTestBuilderContext(metadata);
Expand Down Expand Up @@ -339,6 +334,10 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
// Get attributes for test details
var attributes = resolvedMetadata.GetOrCreateAttributes();

// Hoist loop-invariant state so repeat iterations share one attribute dictionary and factory delegate
var attributesByType = attributes.ToAttributeDictionary();
Func<Task<object>> instanceFactory = () => Task.FromResult(resolvedMetadata.InstanceFactory(Type.EmptyTypes, []));

// Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc.
// We'll create multiple test instances based on RepeatCount
// Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests
Expand All @@ -348,7 +347,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
// Create a simple TestData for ID generation
var testData = new TestBuilder.TestData
{
TestClassInstanceFactory = () => Task.FromResult(resolvedMetadata.InstanceFactory(Type.EmptyTypes, [])),
TestClassInstanceFactory = instanceFactory,
ClassDataSourceAttributeIndex = 0,
ClassDataLoopIndex = 0,
ClassData = [],
Expand Down Expand Up @@ -385,7 +384,7 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
TestEndColumnNumber = resolvedMetadata.EndColumnNumber,
ReturnType = typeof(Task),
MethodMetadata = resolvedMetadata.MethodMetadata,
AttributesByType = attributes.ToAttributeDictionary()
AttributesByType = attributesByType
};

var context = _contextProvider.CreateTestContext(
Expand Down
21 changes: 21 additions & 0 deletions TUnit.Engine/Extensions/AttributeArrayExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace TUnit.Engine.Extensions;

internal static class AttributeArrayExtensions
{
/// <summary>
/// Returns the first attribute assignable to <typeparamref name="T"/>, or null.
/// Allocation-free alternative to OfType&lt;T&gt;().FirstOrDefault() for hot paths.
/// </summary>
public static T? FirstOfType<T>(this Attribute[] attributes) where T : Attribute
{
foreach (var attribute in attributes)
{
if (attribute is T typed)
{
return typed;
}
}

return null;
}
}
Loading