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
8 changes: 7 additions & 1 deletion TUnit.Core/AbstractDynamicTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public class DynamicTest<[DynamicallyAccessedMembers(
/// </summary>
public int? CreatorLineNumber { get; set; }

/// <summary>
/// Custom display name for this dynamic test. If not set, a default name will be generated.
/// </summary>
public string? DisplayName { get; set; }

public override IEnumerable<DiscoveryResult> GetTests()
{
var result = new DynamicDiscoveryResult
Expand All @@ -112,7 +117,8 @@ public override IEnumerable<DiscoveryResult> GetTests()
TestClassType = typeof(T),
CreatorFilePath = CreatorFilePath,
CreatorLineNumber = CreatorLineNumber,
DynamicTestIndex = DynamicTestIndex
DynamicTestIndex = DynamicTestIndex,
DisplayName = DisplayName
};

yield return result;
Expand Down
125 changes: 125 additions & 0 deletions TUnit.Core/DynamicTestMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Diagnostics.CodeAnalysis;

namespace TUnit.Core;

/// <summary>
/// Unified metadata class for dynamic tests.
/// Used by both AOT/source-generated mode and reflection mode for tests created via
/// DynamicTestBuilderAttribute or runtime test variant creation.
/// </summary>
public sealed class DynamicTestMetadata : TestMetadata, IDynamicTestMetadata
{
private readonly DynamicDiscoveryResult _dynamicResult;

public DynamicTestMetadata(DynamicDiscoveryResult dynamicResult)
{
_dynamicResult = dynamicResult;
}

public int DynamicTestIndex => _dynamicResult.DynamicTestIndex;

public string? DisplayName => _dynamicResult.DisplayName;

/// <summary>
/// Parent test ID for test variants created at runtime.
/// </summary>
public string? ParentTestId => _dynamicResult.ParentTestId;

/// <summary>
/// Relationship to parent test for test variants.
/// </summary>
public Enums.TestRelationship? Relationship => _dynamicResult.Relationship;

/// <summary>
/// Custom properties for test variants.
/// </summary>
public Dictionary<string, object?>? Properties => _dynamicResult.Properties;

[field: AllowNull, MaybeNull]
public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecutableTest> CreateExecutableTestFactory
{
get => field ??= CreateExecutableTest;
}

private AbstractExecutableTest CreateExecutableTest(ExecutableTestCreationContext context, TestMetadata metadata)
{
var modifiedContext = new ExecutableTestCreationContext
{
TestId = context.TestId,
DisplayName = _dynamicResult.DisplayName ?? context.DisplayName,
Arguments = _dynamicResult.TestMethodArguments ?? context.Arguments,
ClassArguments = _dynamicResult.TestClassArguments ?? context.ClassArguments,
Context = context.Context,
TestClassInstanceFactory = context.TestClassInstanceFactory
};

// Apply runtime test variant properties
if (_dynamicResult.ParentTestId != null)
{
modifiedContext.Context.ParentTestId = _dynamicResult.ParentTestId;
}

if (_dynamicResult.Relationship.HasValue)
{
modifiedContext.Context.Relationship = _dynamicResult.Relationship.Value;
}

if (_dynamicResult.Properties != null)
{
foreach (var kvp in _dynamicResult.Properties)
{
modifiedContext.Context.StateBag.Items[kvp.Key] = kvp.Value;
}
}

// Create instance factory
var createInstance = async (TestContext testContext) =>
{
// If we have a factory from discovery, use it
if (modifiedContext.TestClassInstanceFactory != null)
{
return await modifiedContext.TestClassInstanceFactory();
}

// Check if there's a ClassConstructor to use
if (testContext.ClassConstructor != null)
{
var testBuilderContext = TestBuilderContext.FromTestContext(testContext, null);
var classConstructorMetadata = new ClassConstructorMetadata
{
TestSessionId = metadata.TestSessionId,
TestBuilderContext = testBuilderContext
};

return await testContext.ClassConstructor.Create(metadata.TestClassType, classConstructorMetadata);
}

// Fall back to default instance factory
var instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments);

// Handle property injections
foreach (var propertyInjection in metadata.PropertyInjections)
{
var value = propertyInjection.ValueFactory();
propertyInjection.Setter(instance, value);
}

return instance;
};

var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null");

return new ExecutableTest(createInstance,
async (instance, args, ctx, ct) =>
{
await invokeTest(instance, args);
})
{
TestId = modifiedContext.TestId,
Metadata = metadata,
Arguments = modifiedContext.Arguments,
ClassArguments = modifiedContext.ClassArguments,
Context = modifiedContext.Context
};
}
}
5 changes: 5 additions & 0 deletions TUnit.Core/IDynamicTestMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ public interface IDynamicTestMetadata
/// Used to generate unique test IDs when multiple dynamic tests target the same method.
/// </summary>
int DynamicTestIndex { get; }

/// <summary>
/// Custom display name for this dynamic test. If null, a default name will be generated.
/// </summary>
string? DisplayName { get; }
}
70 changes: 1 addition & 69 deletions TUnit.Engine/Building/Collectors/AotTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco

var testName = methodInfo.Name;

return Task.FromResult<TestMetadata>(new AotDynamicTestMetadata(result)
return Task.FromResult<TestMetadata>(new DynamicTestMetadata(result)
{
TestName = testName,
TestClassType = result.TestClassType,
Expand Down Expand Up @@ -296,74 +296,6 @@ private static MethodMetadata CreateDummyMethodMetadata(Type type, string method
};
}

private sealed class AotDynamicTestMetadata(DynamicDiscoveryResult dynamicResult) : TestMetadata, IDynamicTestMetadata
{
public int DynamicTestIndex => dynamicResult.DynamicTestIndex;

public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecutableTest> CreateExecutableTestFactory
{
get => (context, metadata) =>
{
// For dynamic tests, we need to use the specific arguments from the dynamic result
var modifiedContext = new ExecutableTestCreationContext
{
TestId = context.TestId,
DisplayName = context.DisplayName,
Arguments = dynamicResult.TestMethodArguments ?? context.Arguments,
ClassArguments = dynamicResult.TestClassArguments ?? context.ClassArguments,
Context = context.Context
};

// Create instance and test invoker for the dynamic test
var createInstance = async (TestContext testContext) =>
{
object instance;

// Check if there's a ClassConstructor to use
if (testContext.ClassConstructor != null)
{
var testBuilderContext = TestBuilderContext.FromTestContext(testContext, null);
var classConstructorMetadata = new ClassConstructorMetadata
{
TestSessionId = "", // Dynamic tests don't have session IDs
TestBuilderContext = testBuilderContext
};

instance = await testContext.ClassConstructor.Create(metadata.TestClassType, classConstructorMetadata);
}
else
{
instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments);
}

// Handle property injections
foreach (var propertyInjection in metadata.PropertyInjections)
{
var value = propertyInjection.ValueFactory();
propertyInjection.Setter(instance, value);
}

return instance;
};

var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null");

return new ExecutableTest(createInstance,
async (instance, args, context, ct) =>
{
await invokeTest(instance, args);
})
{
TestId = modifiedContext.TestId,
Metadata = metadata,
Arguments = modifiedContext.Arguments,
ClassArguments = modifiedContext.ClassArguments,
Context = modifiedContext.Context
};
};
}
}

private sealed class FailedDynamicTestMetadata(Exception exception) : TestMetadata
{
public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecutableTest> CreateExecutableTestFactory
Expand Down
12 changes: 10 additions & 2 deletions TUnit.Engine/Building/TestBuilderPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,11 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
};

var testId = TestIdentifierService.GenerateTestId(resolvedMetadata, testData);
var dynamicMetadata = (IDynamicTestMetadata)resolvedMetadata;
var baseDisplayName = dynamicMetadata.DisplayName ?? resolvedMetadata.TestName;
var displayName = repeatCount > 0
? $"{resolvedMetadata.TestName} (Repeat {repeatIndex + 1}/{repeatCount + 1})"
: resolvedMetadata.TestName;
? $"{baseDisplayName} (Repeat {repeatIndex + 1}/{repeatCount + 1})"
: baseDisplayName;

// Create TestDetails for dynamic tests
var testDetails = new TestDetails
Expand Down Expand Up @@ -326,6 +328,12 @@ private async IAsyncEnumerable<AbstractExecutableTest> BuildTestsFromSingleMetad
// Set the TestDetails on the context
context.Metadata.TestDetails = testDetails;

// Set custom display name for dynamic tests if specified
if (dynamicMetadata.DisplayName != null)
{
context.Metadata.DisplayName = dynamicMetadata.DisplayName;
}
Comment on lines +332 to +335
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block sets the DisplayName on the context.Metadata, but this seems redundant because the same DisplayName value is already being passed to CreateExecutableTestFactory through the ExecutableTestCreationContext on line 343. The DynamicTestMetadata.CreateExecutableTest method already handles setting the display name via the DisplayName property passed in the context parameter (line 49 in DynamicTestMetadata.cs).

Consider removing this redundant assignment since the DisplayName is already properly propagated through the ExecutableTestCreationContext. This would simplify the code and avoid setting the same value twice through different paths.

Suggested change
if (dynamicMetadata.DisplayName != null)
{
context.Metadata.DisplayName = dynamicMetadata.DisplayName;
}

Copilot uses AI. Check for mistakes.

// Invoke discovery event receivers to properly handle all attribute behaviors
await InvokeDiscoveryEventReceiversAsync(context).ConfigureAwait(false);

Expand Down
83 changes: 1 addition & 82 deletions TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1922,7 +1922,7 @@ private Task<TestMetadata> CreateMetadataFromDynamicDiscoveryResult(DynamicDisco

var testName = GenerateTestName(result.TestClassType, methodInfo);

var metadata = new DynamicReflectionTestMetadata(result.TestClassType, methodInfo, result)
var metadata = new DynamicTestMetadata(result)
{
TestName = testName,
TestClassType = result.TestClassType,
Expand Down Expand Up @@ -2091,85 +2091,4 @@ private static TestMetadata CreateFailedTestMetadataForDynamicTest(DynamicDiscov
};
}

private sealed class DynamicReflectionTestMetadata : TestMetadata, IDynamicTestMetadata
{
private readonly DynamicDiscoveryResult _dynamicResult;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
private readonly Type _testClass;
private readonly MethodInfo _testMethod;

public DynamicReflectionTestMetadata(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type testClass,
MethodInfo testMethod,
DynamicDiscoveryResult dynamicResult)
{
_testClass = testClass;
_testMethod = testMethod;
_dynamicResult = dynamicResult;
}

public int DynamicTestIndex => _dynamicResult.DynamicTestIndex;

public override Func<ExecutableTestCreationContext, TestMetadata, AbstractExecutableTest> CreateExecutableTestFactory
{
get => (context, metadata) =>
{
// For dynamic tests, we need to use the specific arguments from the dynamic result
var modifiedContext = new ExecutableTestCreationContext
{
TestId = context.TestId,
DisplayName = context.DisplayName,
Arguments = _dynamicResult.TestMethodArguments ?? context.Arguments,
ClassArguments = _dynamicResult.TestClassArguments ?? context.ClassArguments,
Context = context.Context
};

// Create a regular ExecutableTest with the modified context
// Create instance and test invoker for the dynamic test
Func<TestContext, Task<object>> createInstance = async (TestContext testContext) =>
{
// Try to create instance with ClassConstructor attribute
var attributes = metadata.AttributeFactory();
var classConstructorInstance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor(
attributes,
_testClass,
metadata.TestSessionId,
testContext).ConfigureAwait(false);

if (classConstructorInstance != null)
{
return classConstructorInstance;
}

// Fall back to default instance factory
var instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments);

// Handle property injections
foreach (var propertyInjection in metadata.PropertyInjections)
{
var value = propertyInjection.ValueFactory();
propertyInjection.Setter(instance, value);
}

return instance;
};

var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null");

return new ExecutableTest(createInstance,
async (instance, args, context, ct) =>
{
await invokeTest(instance, args).ConfigureAwait(false);
})
{
TestId = modifiedContext.TestId,
Metadata = metadata,
Arguments = modifiedContext.Arguments,
ClassArguments = modifiedContext.ClassArguments,
Context = modifiedContext.Context
};
};
}
}

}
Loading
Loading