Skip to content
Merged
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
85 changes: 53 additions & 32 deletions TUnit.Engine/Services/MetadataFilterMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,53 +153,71 @@ public static FilterHints ExtractFilterHints(ITestExecutionFilter? filter)
}

// Parse path: /{assembly}/{namespace}/{className}/{methodName}
var parts = filterString.Split('/');

// Expected format: "", assembly, namespace, className, methodName
// parts[0] is empty (before first /), parts[1] is assembly, etc.
if (parts.Length < 2)
{
return default;
}
// segment[0] is empty (before first /), segment[1] is assembly, etc.
// FilterHints only retains segments 1-4, so we only materialize those.
#if NET8_0_OR_GREATER
var filterSpan = filterString.AsSpan();

string? assemblyName = null;
string? namespaceName = null;
string? className = null;
string? methodName = null;
// Up to 5 useful segments (empty, assembly, namespace, className, methodName);
// a 6th range absorbs any trailing path so the earlier segments stay intact.
Span<Range> ranges = stackalloc Range[6];
var count = filterSpan.Split(ranges, '/');

// Extract assembly name (parts[1])
if (parts.Length > 1 && !IsNonLiteralSegment(parts[1]))
// Need at least the leading empty segment plus the assembly segment.
if (count < 2)
{
assemblyName = parts[1];
return default;
}

// Extract namespace (parts[2])
if (parts.Length > 2 && !IsNonLiteralSegment(parts[2]))
return new FilterHints
{
namespaceName = parts[2];
}
AssemblyName = ExtractSegment(filterSpan, ranges, count, 1),
Namespace = ExtractSegment(filterSpan, ranges, count, 2),
ClassName = ExtractSegment(filterSpan, ranges, count, 3),
MethodName = ExtractSegment(filterSpan, ranges, count, 4)
};
}

// Extract class name (parts[3])
if (parts.Length > 3 && !IsNonLiteralSegment(parts[3]))
private static string? ExtractSegment(ReadOnlySpan<char> filterSpan, ReadOnlySpan<Range> ranges, int count, int index)
{
if (index >= count)
{
className = parts[3];
return null;
}

// Extract method name (parts[4])
if (parts.Length > 4 && !IsNonLiteralSegment(parts[4]))
var segment = filterSpan[ranges[index]];
return IsNonLiteralSegment(segment) ? null : segment.ToString();
}
#else
var parts = filterString.Split('/');

if (parts.Length < 2)
{
methodName = parts[4];
return default;
}

return new FilterHints
{
AssemblyName = assemblyName,
Namespace = namespaceName,
ClassName = className,
MethodName = methodName
AssemblyName = ExtractSegment(parts, 1),
Namespace = ExtractSegment(parts, 2),
ClassName = ExtractSegment(parts, 3),
MethodName = ExtractSegment(parts, 4)
};
}

private static string? ExtractSegment(string[] parts, int index)
{
if (index >= parts.Length)
{
return null;
}

var segment = parts[index];
return IsNonLiteralSegment(segment) ? null : segment;
}
#endif

// Characters that make a TreeNodeFilter path segment non-literal:
// * ? wildcards
// ( ) | & ! grouping / logical operators
Expand All @@ -217,14 +235,17 @@ public static FilterHints ExtractFilterHints(ITestExecutionFilter? filter)
private static readonly char[] _filterOperatorChars = { '*', '?', '(', ')', '|', '&', '!', '\\' };
#endif

private static bool IsNonLiteralSegment(string value)
{
#if NET8_0_OR_GREATER
return string.IsNullOrEmpty(value) || value.AsSpan().IndexOfAny(_filterOperatorChars) >= 0;
private static bool IsNonLiteralSegment(ReadOnlySpan<char> value)
{
return value.IsEmpty || value.IndexOfAny(_filterOperatorChars) >= 0;
}
#else
private static bool IsNonLiteralSegment(string value)
{
return string.IsNullOrEmpty(value) || value.IndexOfAny(_filterOperatorChars) >= 0;
#endif
}
#endif
#pragma warning restore TPEXP

public bool CouldMatchFilter(TestMetadata metadata, ITestExecutionFilter? filter)
Expand Down
Loading