Skip to content
Draft
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
22 changes: 21 additions & 1 deletion TUnit.Core.SourceGenerator/Utilities/MetadataGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,34 @@ public static void WriteMethodMetadata(ICodeWriter writer, IMethodSymbol methodS
WriteParameterMetadataArrayForMethod(writer, methodSymbol);
writer.AppendLine(",");
writer.Append("Class = ");
WriteClassMetadataGetOrAdd(writer, namedTypeSymbol);

// For nested types, generate parent metadata recursively
var parentExpression = GenerateParentClassMetadataExpression(namedTypeSymbol.ContainingType, writer.IndentLevel);
WriteClassMetadataGetOrAdd(writer, namedTypeSymbol, parentExpression);

// Manually restore indent level
writer.SetIndentLevel(currentIndent);
writer.AppendLine();
writer.Append("}");
}

/// <summary>
/// Recursively generates parent class metadata expression for nested types.
/// </summary>
private static string? GenerateParentClassMetadataExpression(INamedTypeSymbol? typeSymbol, int currentIndentLevel)
{
if (typeSymbol == null)
{
return null;
}

// Recursively get the parent's parent expression
var grandParentExpression = GenerateParentClassMetadataExpression(typeSymbol.ContainingType, currentIndentLevel);

// Generate the class metadata for this type with its parent expression
return GenerateClassMetadataGetOrAdd(typeSymbol, grandParentExpression, currentIndentLevel);
}

/// <summary>
/// Generates code for creating a MethodMetadata instance (for backward compat)
/// </summary>
Expand Down
57 changes: 57 additions & 0 deletions TUnit.Engine.Tests/NestedClassFilteringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Shouldly;
using TUnit.Engine.Tests.Enums;

namespace TUnit.Engine.Tests;

/// <summary>
/// Tests for nested class filtering support.
/// This validates that tests in nested classes can be filtered using the OuterClass+NestedClass syntax.
/// </summary>
public class NestedClassFilteringTests(TestMode testMode) : InvokableTestBase(testMode)
{
[Test]
public async Task FilterByNestedClassName_ShouldFindNestedTest()
{
// Filter using the nested class name with '+' separator
// This is what Visual Studio sends when running a test in a nested class
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),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
]);
}

[Test]
public async Task FilterByNestedClassMethodName_ShouldFindNestedTest()
{
// Filter for the specific test method in the nested class
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),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
]);
}

[Test]
public async Task FilterByOuterClassName_ShouldFindOuterTest()
{
// Filter for the outer class test (not nested)
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),
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
]);
}
}
5 changes: 4 additions & 1 deletion TUnit.Engine/Building/ReflectionMetadataBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ private static ClassMetadata CreateClassMetadata([DynamicallyAccessedMembers(Dyn
.Select((p, i) => CreateParameterMetadata(p.ParameterType, p.Name, i, p))
.ToArray() ?? [];

// Create parent metadata for nested types
var parent = type.DeclaringType != null ? CreateClassMetadata(type.DeclaringType) : null;

return new ClassMetadata
{
Name = type.Name,
Expand All @@ -90,7 +93,7 @@ private static ClassMetadata CreateClassMetadata([DynamicallyAccessedMembers(Dyn
}),
Parameters = constructorParameters,
Properties = [],
Parent = null
Parent = parent
};
});
}
Expand Down
31 changes: 30 additions & 1 deletion TUnit.Engine/Services/TestFilterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private string BuildPath(AbstractExecutableTest test)
var classMetadata = test.Context.Metadata.TestDetails.MethodMetadata.Class;
var assemblyName = classMetadata.Assembly.Name ?? metadata.TestClassType.Assembly.GetName().Name ?? "*";
var namespaceName = classMetadata.Namespace ?? "*";
var classTypeName = classMetadata.Name;
var classTypeName = GetNestedClassName(classMetadata);

var path = $"/{assemblyName}/{namespaceName}/{classTypeName}/{metadata.TestMethodName}";

Expand All @@ -147,6 +147,35 @@ private string BuildPath(AbstractExecutableTest test)
return path;
}

/// <summary>
/// Gets the full nested class name with '+' separator (matching .NET Type.FullName convention).
/// For example: OuterClass+InnerClass
/// </summary>
private static string GetNestedClassName(ClassMetadata classMetadata)
{
// If no parent, just return the simple name
if (classMetadata.Parent == null)
{
return classMetadata.Name;
}

// Build the type hierarchy by walking up through parents
var typeHierarchy = new List<string>();
var current = classMetadata;

while (current != null)
{
typeHierarchy.Add(current.Name);
current = current.Parent;
}

// Reverse to get outer-to-inner order
typeHierarchy.Reverse();

// Join with '+' separator (matching .NET Type.FullName convention for nested types)
return string.Join("+", typeHierarchy);
}

private bool CheckTreeNodeFilter(
#pragma warning disable TPEXP
TreeNodeFilter treeNodeFilter,
Expand Down
27 changes: 27 additions & 0 deletions TUnit.TestProject/NestedTestClassTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using TUnit.TestProject.Attributes;

namespace TUnit.TestProject;

/// <summary>
/// Tests for nested class filtering support.
/// This validates that tests in nested classes can be filtered using the OuterClass+NestedClass syntax.
/// See: https://github.com/thomhurst/TUnit/issues/4149
/// </summary>
[EngineTest(ExpectedResult.Pass)]
public class NestedTestClassTests
{
[Test]
public void Outer()
{
// This test should pass when filtered by OuterClass name
}

public class NestedClass
{
[Test]
public void Inner()
{
// This test should pass when filtered by OuterClass+NestedClass name
}
}
}
Loading