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
2 changes: 1 addition & 1 deletion .claude/docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dotnet test --framework net10.0 --treenode-filter "/*/*/ClassName/*"

A single-method filter has the form `/Assembly/Namespace/ClassName/TestMethodName`. Run one filter per command; do not join paths with `|`.

For AOT validation, publish the affected test application in Release with `-p:PublishAot=true --use-current-runtime`, selecting a target framework with `-f` when it multi-targets.
For AOT validation of `tests/TUnit.TestProject`, publish in Release with `-p:Aot=true --use-current-runtime`, selecting a target framework with `-f` when it multi-targets. This project switch scopes `PublishAot` to supported target frameworks; passing `-p:PublishAot=true` globally also applies it to `netstandard2.0` analyzer project references and fails with NETSDK1207.

## Performance

Expand Down
51 changes: 29 additions & 22 deletions src/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3898,27 +3898,26 @@ private async Task<List<TestMetadata>> DiscoverDynamicTests(string testSessionId
{
var dynamicTests = new List<TestMetadata>(50);

// First check if there are any registered dynamic test sources from source generation
if (Sources.DynamicTestSources.Count > 0)
// Generated wrappers call the same builders that the reflection scan invokes below.
// Custom sources have no corresponding builder and must still be collected.
foreach (var source in Sources.DynamicTestSources)
{
foreach (var source in Sources.DynamicTestSources)
if (source.GetType().GetCustomAttribute<System.CodeDom.Compiler.GeneratedCodeAttribute>()?.Tool == "TUnit")
{
try
{
var tests = source.CollectDynamicTests(testSessionId);
foreach (var dynamicTest in tests)
{
var testMetadataList = await ConvertDynamicTestToMetadata(dynamicTest).ConfigureAwait(false);
dynamicTests.AddRange(testMetadataList);
}
}
catch (Exception ex)
continue;
Comment thread
thomhurst marked this conversation as resolved.
Resolved
}

try
{
foreach (var dynamicTest in source.CollectDynamicTests(testSessionId))
{
// Create a failed test metadata for this dynamic test source
var failedTest = CreateFailedTestMetadataForDynamicSource(source, ex);
dynamicTests.Add(failedTest);
dynamicTests.AddRange(await ConvertDynamicTestToMetadata(dynamicTest).ConfigureAwait(false));
}
}
catch (Exception ex)
{
dynamicTests.Add(CreateFailedTestMetadataForDynamicSource(source, ex));
}
}

var allAssemblies = Assemblies;
Expand All @@ -3938,7 +3937,7 @@ private async Task<List<TestMetadata>> DiscoverDynamicTests(string testSessionId
{
try
{
return asm.GetExportedTypes();
return asm.GetTypes();
}
catch
{
Expand Down Expand Up @@ -3988,8 +3987,11 @@ private async Task<List<TestMetadata>> ExecuteDynamicTestBuilder(Type testClass,
var dynamicTests = new List<TestMetadata>(50);

// Extract file path and line number from the DynamicTestBuilderAttribute if possible
var filePath = ExtractFilePath(builderMethod) ?? "Unknown";
var lineNumber = ExtractLineNumber(builderMethod) ?? 0;
#pragma warning disable TUnitWIP0001
var attribute = builderMethod.GetCustomAttribute<DynamicTestBuilderAttribute>(inherit: false);
#pragma warning restore TUnitWIP0001
var filePath = attribute?.File ?? "Unknown";
var lineNumber = attribute?.Line ?? 0;

// Create context
var context = new DynamicTestBuilderContext(filePath, lineNumber);
Expand All @@ -3998,7 +4000,8 @@ private async Task<List<TestMetadata>> ExecuteDynamicTestBuilder(Type testClass,
object? instance = null;
if (!builderMethod.IsStatic)
{
instance = Activator.CreateInstance(testClass);
// Generated wrappers can invoke assembly-accessible constructors.
instance = Activator.CreateInstance(testClass, nonPublic: true);
}

// Execute the builder method
Expand Down Expand Up @@ -4168,13 +4171,17 @@ private static TestMetadata CreateFailedTestMetadataForDynamicBuilder(
var testName = $"[DYNAMIC BUILDER FAILED] {type.FullName}.{method.Name}";
var displayName = $"{testName} - {ex.Message}";

#pragma warning disable TUnitWIP0001
var attribute = method.GetCustomAttribute<DynamicTestBuilderAttribute>(inherit: false);
#pragma warning restore TUnitWIP0001

return new FailedTestMetadata(ex, displayName)
{
TestName = testName,
TestClassType = type,
TestMethodName = method.Name,
FilePath = ExtractFilePath(method) ?? "Unknown",
LineNumber = ExtractLineNumber(method) ?? 0,
FilePath = attribute?.File ?? "Unknown",
LineNumber = attribute?.Line ?? 0,
MethodMetadata = ReflectionMetadataBuilder.CreateMethodMetadata(type, method),
AttributeFactory = () => GetFailedTestAttributes(method),
DataSources = [],
Expand Down
16 changes: 15 additions & 1 deletion tests/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,30 @@ namespace TUnit.TestProject.DynamicTests;
[EngineTest(ExpectedResult.Pass)]
public class DynamicTestIndexTests
{
public void TestMethod(int value)
private static int _builderInvocations;

[Before(TestDiscovery)]
public static void ResetBuilderInvocations()
{
Interlocked.Exchange(ref _builderInvocations, 0);
}

public async Task TestMethod(int value)
{
Console.WriteLine($"TestMethod called with value: {value}");
await Assert.That(_builderInvocations).IsEqualTo(1);
Comment thread
thomhurst marked this conversation as resolved.
Resolved
var details = TestContext.Current!.Metadata.TestDetails;
await Assert.That(Path.GetFileName(details.TestFilePath)).IsEqualTo("DynamicTestIndexTests.cs");
await Assert.That(details.TestLineNumber).IsGreaterThan(0);
}

#pragma warning disable TUnitWIP0001
[DynamicTestBuilder]
#pragma warning restore TUnitWIP0001
public void BuildTests(DynamicTestBuilderContext context)
{
Interlocked.Increment(ref _builderInvocations);

// Add 5 dynamic tests all targeting the SAME method with different arguments.
// Before the DynamicTestIndex fix, these would generate duplicate test IDs
// and only one would execute.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.DynamicTests;

// The generated wrapper can construct this builder from within the same assembly.
internal class InternalConstructorDynamicBuilder
{
internal InternalConstructorDynamicBuilder()
{
}

#pragma warning disable TUnitWIP0001
[DynamicTestBuilder]
#pragma warning restore TUnitWIP0001
public void BuildTests(DynamicTestBuilderContext context)
{
context.AddTest(new DynamicTest<InternalConstructorDynamicBuilderTests>
{
TestMethod = test => test.TestFromInternalBuilder(),
TestMethodArguments = [],
Attributes = []
});
}
}

[EngineTest(ExpectedResult.Pass)]
public class InternalConstructorDynamicBuilderTests
{
// Discovery must construct the builder, whose constructor is internal,
// independently of this publicly constructible test class.
public async Task TestFromInternalBuilder()
{
await Assert.That(TestContext.Current!.Metadata.TestDetails.TestLineNumber).IsGreaterThan(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Runtime.CompilerServices;
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject.DynamicTests;

[EngineTest(ExpectedResult.Pass)]
public class RegisteredDynamicTestSourceTests
{
[ModuleInitializer]
public static void RegisterSource()
{
SourceRegistrar.RegisterDynamic(new CustomSource());
}

// Deliberately has no [Test] or [DynamicTestBuilder]: only registration discovers it.
public async Task RegisteredTest(string sessionId)
{
await Assert.That(sessionId).IsNotNullOrEmpty();
}

private sealed class CustomSource : IDynamicTestSource
{
public IReadOnlyList<AbstractDynamicTest> CollectDynamicTests(string sessionId)
{
var context = new DynamicTestBuilderContext("RegisteredDynamicTestSourceTests.cs", 1);
context.AddTest(new DynamicTest<RegisteredDynamicTestSourceTests>
{
TestMethod = test => test.RegisteredTest(sessionId),
TestMethodArguments = [sessionId],
Attributes = []
});
return context.Tests;
}
}
}
Loading