diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/SourceInformationWriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/SourceInformationWriter.cs
index 460410e5ea..d5d1a1870e 100644
--- a/TUnit.Core.SourceGenerator/CodeGenerators/Writers/SourceInformationWriter.cs
+++ b/TUnit.Core.SourceGenerator/CodeGenerators/Writers/SourceInformationWriter.cs
@@ -9,8 +9,7 @@ public static class SourceInformationWriter
{
public static void GenerateClassInformation(ICodeWriter sourceCodeWriter, Compilation compilation, INamedTypeSymbol namedTypeSymbol)
{
- var parent = namedTypeSymbol.ContainingType;
- var parentExpression = parent != null ? MetadataGenerationHelper.GenerateClassMetadataGetOrAdd(parent, null, sourceCodeWriter.IndentLevel) : null;
+ var parentExpression = GenerateParentClassMetadataExpression(namedTypeSymbol, sourceCodeWriter.IndentLevel);
var classMetadata = MetadataGenerationHelper.GenerateClassMetadataGetOrAdd(namedTypeSymbol, parentExpression, sourceCodeWriter.IndentLevel);
// Handle multi-line class metadata similar to method metadata
@@ -98,4 +97,20 @@ public static void GenerateParameterInformation(ICodeWriter sourceCodeWriter,
MetadataGenerationHelper.WriteParameterMetadataGeneric(sourceCodeWriter, parameter);
sourceCodeWriter.Append(",");
}
+
+ ///
+ /// Recursively generates parent ClassMetadata expression for nested types.
+ /// Returns null if the type has no containing type.
+ ///
+ private static string? GenerateParentClassMetadataExpression(INamedTypeSymbol typeSymbol, int indentLevel)
+ {
+ var parent = typeSymbol.ContainingType;
+ if (parent == null)
+ {
+ return null;
+ }
+
+ var grandparentExpression = GenerateParentClassMetadataExpression(parent, indentLevel);
+ return MetadataGenerationHelper.GenerateClassMetadataGetOrAdd(parent, grandparentExpression, indentLevel);
+ }
}
diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
index 9422f2fd25..354c645cd4 100644
--- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
+++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs
@@ -1934,7 +1934,7 @@ private static void GenerateEnumerateTestDescriptors(CodeWriter writer, TestMeth
{
var methodName = testMethod.MethodSymbol.Name;
var namespaceName = testMethod.TypeSymbol.ContainingNamespace?.ToDisplayString() ?? "";
- var simpleClassName = testMethod.TypeSymbol.Name;
+ var simpleClassName = testMethod.TypeSymbol.GetNestedClassName();
var fullyQualifiedName = string.IsNullOrEmpty(namespaceName)
? $"{simpleClassName}.{methodName}"
: $"{namespaceName}.{simpleClassName}.{methodName}";
diff --git a/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs b/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
index db6b93fda3..2da6f06b2b 100644
--- a/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
+++ b/TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
@@ -34,7 +34,7 @@ public static void WriteMethodMetadata(ICodeWriter writer, IMethodSymbol methodS
WriteParameterMetadataArrayForMethod(writer, methodSymbol);
writer.AppendLine(",");
writer.Append("Class = ");
- WriteClassMetadataGetOrAdd(writer, namedTypeSymbol);
+ WriteClassMetadataGetOrAddWithParent(writer, namedTypeSymbol);
// Manually restore indent level
writer.SetIndentLevel(currentIndent);
@@ -147,6 +147,24 @@ private static void WriteClassMetadataGetOrAdd(ICodeWriter writer, INamedTypeSym
writer.Append("})");
}
+ ///
+ /// Writes ClassMetadata with recursive parent generation for nested types
+ ///
+ private static void WriteClassMetadataGetOrAddWithParent(ICodeWriter writer, INamedTypeSymbol typeSymbol)
+ {
+ if (typeSymbol.ContainingType != null)
+ {
+ // Generate the parent expression as a string, then pass it
+ var parentWriter = new CodeWriter("", includeHeader: false).SetIndentLevel(writer.IndentLevel);
+ WriteClassMetadataGetOrAddWithParent(parentWriter, typeSymbol.ContainingType);
+ WriteClassMetadataGetOrAdd(writer, typeSymbol, parentWriter.ToString());
+ }
+ else
+ {
+ WriteClassMetadataGetOrAdd(writer, typeSymbol);
+ }
+ }
+
///
/// Generates code for creating a ClassMetadata instance with GetOrAdd pattern
///
diff --git a/TUnit.Core/Extensions/TestContextExtensions.cs b/TUnit.Core/Extensions/TestContextExtensions.cs
index 0a3381d515..af39629ed0 100644
--- a/TUnit.Core/Extensions/TestContextExtensions.cs
+++ b/TUnit.Core/Extensions/TestContextExtensions.cs
@@ -8,18 +8,29 @@ public static class TestContextExtensions
{
public static string GetClassTypeName(this TestContext context)
{
+ var type = context.Metadata.TestDetails.ClassType;
var parameters = context.Metadata.TestDetails.MethodMetadata.Class.Parameters;
+ var nestedPrefix = GetNestedTypePrefix(type);
+
if (parameters.Length == 0)
{
- return context.Metadata.TestDetails.ClassType.Name;
+ return nestedPrefix != null
+ ? $"{nestedPrefix}+{type.Name}"
+ : type.Name;
}
var args = context.Metadata.TestDetails.TestClassArguments;
var sb = StringBuilderPool.Get();
try
{
- sb.Append(context.Metadata.TestDetails.ClassType.Name);
+ if (nestedPrefix != null)
+ {
+ sb.Append(nestedPrefix);
+ sb.Append('+');
+ }
+
+ sb.Append(type.Name);
sb.Append('(');
for (var i = 0; i < args.Length; i++)
@@ -40,6 +51,40 @@ public static string GetClassTypeName(this TestContext context)
}
}
+ ///
+ /// Gets the nested type prefix (outer class names joined by '+') for a type, or null if not nested.
+ /// For example, for OuterClass+MiddleClass+InnerClass, returns "OuterClass+MiddleClass".
+ ///
+ internal static string? GetNestedTypePrefix(Type type)
+ {
+ if (type.DeclaringType == null)
+ {
+ return null;
+ }
+
+ // Walk the declaring type chain and build the hierarchy
+ var hierarchy = new List();
+ var current = type.DeclaringType;
+ while (current != null)
+ {
+ hierarchy.Add(current.Name);
+ current = current.DeclaringType;
+ }
+
+ hierarchy.Reverse();
+ return string.Join("+", hierarchy);
+ }
+
+ ///
+ /// Gets the full nested type name with '+' separator (matching .NET Type.FullName convention for nested types).
+ /// For example: OuterClass+InnerClass
+ ///
+ internal static string GetNestedTypeName(Type type)
+ {
+ var prefix = GetNestedTypePrefix(type);
+ return prefix != null ? $"{prefix}+{type.Name}" : type.Name;
+ }
+
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Dynamic test metadata creation uses reflection")]
#endif
diff --git a/TUnit.Engine.Tests/NestedClassFilteringTests.cs b/TUnit.Engine.Tests/NestedClassFilteringTests.cs
new file mode 100644
index 0000000000..78ec7b6335
--- /dev/null
+++ b/TUnit.Engine.Tests/NestedClassFilteringTests.cs
@@ -0,0 +1,46 @@
+using Shouldly;
+using TUnit.Engine.Tests.Enums;
+
+namespace TUnit.Engine.Tests;
+
+public class NestedClassFilteringTests(TestMode testMode) : InvokableTestBase(testMode)
+{
+ [Test]
+ public async Task Filter_NestedClass_ByFullNestedName()
+ {
+ await RunTestsWithFilter(
+ "/*/*/NestedTestClassTests+NestedClass/*",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0)
+ ]);
+ }
+
+ [Test]
+ public async Task Filter_NestedClass_SpecificMethod()
+ {
+ await RunTestsWithFilter(
+ "/*/*/NestedTestClassTests+NestedClass/Inner",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0)
+ ]);
+ }
+
+ [Test]
+ public async Task Filter_OuterClass_StillWorks()
+ {
+ await RunTestsWithFilter(
+ "/*/*/NestedTestClassTests/Outer",
+ [
+ result => result.ResultSummary.Outcome.ShouldBe("Completed"),
+ result => result.ResultSummary.Counters.Total.ShouldBe(1),
+ result => result.ResultSummary.Counters.Passed.ShouldBe(1),
+ result => result.ResultSummary.Counters.Failed.ShouldBe(0)
+ ]);
+ }
+}
diff --git a/TUnit.Engine.Tests/UidFilterMatchingTests.cs b/TUnit.Engine.Tests/UidFilterMatchingTests.cs
index 67e107873a..a38ac24b8e 100644
--- a/TUnit.Engine.Tests/UidFilterMatchingTests.cs
+++ b/TUnit.Engine.Tests/UidFilterMatchingTests.cs
@@ -15,11 +15,11 @@ public class UidFilterMatchingTests(TestMode testMode) : InvokableTestBase(testM
[Test]
public async Task Filter_NestedClass_ShouldMatchOnlyNestedClass()
{
- // Filter for the nested class InnerClass
- // Tree node paths use just the innermost class name (Type.Name)
+ // Filter for the nested class InnerClass using full nested path
+ // Tree node paths now use OuterClass+InnerClass format for nested types
// Should only run tests from InnerClass, not OuterClass
await RunTestsWithFilter(
- "/*/TUnit.TestProject.Bugs._4656/InnerClass/InnerMethod",
+ "/*/TUnit.TestProject.Bugs._4656/OuterClass+InnerClass/InnerMethod",
[
result => result.ResultSummary.Outcome.ShouldBe("Completed"),
result => result.ResultSummary.Counters.Total.ShouldBe(1,
diff --git a/TUnit.Engine/Building/ReflectionMetadataBuilder.cs b/TUnit.Engine/Building/ReflectionMetadataBuilder.cs
index bf869eba2c..f6f06e38b2 100644
--- a/TUnit.Engine/Building/ReflectionMetadataBuilder.cs
+++ b/TUnit.Engine/Building/ReflectionMetadataBuilder.cs
@@ -90,7 +90,7 @@ private static ClassMetadata CreateClassMetadata([DynamicallyAccessedMembers(Dyn
}),
Parameters = constructorParameters,
Properties = [],
- Parent = null
+ Parent = type.DeclaringType != null ? CreateClassMetadata(type.DeclaringType) : null
};
});
}
diff --git a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs
index cc3954b121..00e5f742d0 100644
--- a/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs
+++ b/TUnit.Engine/Discovery/ReflectionHookDiscoveryService.cs
@@ -837,20 +837,7 @@ private static MethodMetadata CreateMethodMetadata(
{
Name = method.Name,
Type = type,
- Class = new ClassMetadata
- {
- Name = type.Name,
- Type = type,
- TypeInfo = new ConcreteType(type),
- Namespace = type.Namespace ?? string.Empty,
- Assembly = new AssemblyMetadata
- {
- Name = type.Assembly.GetName().Name ?? "Unknown"
- },
- Parameters = [],
- Properties = [],
- Parent = null
- },
+ Class = CreateClassMetadata(type),
Parameters = method.GetParameters().Select(p => new ParameterMetadata(p.ParameterType)
{
Name = p.Name ?? string.Empty,
@@ -865,6 +852,24 @@ private static MethodMetadata CreateMethodMetadata(
};
}
+ private static ClassMetadata CreateClassMetadata(Type type)
+ {
+ return ClassMetadata.GetOrAdd(type.FullName ?? type.Name, () => new ClassMetadata
+ {
+ Name = type.Name,
+ Type = type,
+ TypeInfo = new ConcreteType(type),
+ Namespace = type.Namespace ?? string.Empty,
+ Assembly = AssemblyMetadata.GetOrAdd(type.Assembly.GetName().Name ?? "Unknown", () => new AssemblyMetadata
+ {
+ Name = type.Assembly.GetName().Name ?? "Unknown"
+ }),
+ Parameters = [],
+ Properties = [],
+ Parent = type.DeclaringType != null ? CreateClassMetadata(type.DeclaringType) : null
+ });
+ }
+
private static Func