Skip to content

fix: prevent substring matching in UID filter causing incorrect test inclusion#4659

Merged
thomhurst merged 2 commits intomainfrom
fix/4656-overlapping-class-name-filter
Feb 4, 2026
Merged

fix: prevent substring matching in UID filter causing incorrect test inclusion#4659
thomhurst merged 2 commits intomainfrom
fix/4656-overlapping-class-name-filter

Conversation

@thomhurst
Copy link
Owner

Summary

Fixes #4656

When filtering tests by UID (e.g., from Visual Studio), class names like ABCV would incorrectly match ABCVC because the filter used string.Contains() for matching. This caused TestDiscoveryContext.AllTests to include tests that shouldn't be there.

Root Cause

In MetadataFilterMatcher.CouldMatchUidFilter(), the code used substring matching:

if (uidValue.Contains(className) && uidValue.Contains(methodName))

This meant that when checking if test ABCV.B2 matches a filter containing UID for ABCVC.B2, it would incorrectly return true because "ABCVC" contains "ABCV" as a substring.

The Fix

Changed to use word-boundary matching:

  • Class names must be bounded by . (namespace/indices), < (generics), or + (nested types)
  • Method names must be preceded by . and followed by ., <, or (

This ensures ABCV only matches the exact class name, not substrings like ABCVC.

Test plan

  • Added reproduction test case in TUnit.TestProject/Bugs/4656/
  • Added integration tests in TUnit.Engine.Tests/OverlappingClassNameFilterTests.cs
  • Tests verify that filtering for ABCVC/B2 runs exactly 2 tests (not 4 if bug exists)
  • Tests verify that filtering for ABCV/* runs exactly 3 tests (not 6 if bug exists)
  • All tests pass in both SourceGenerated and Reflection modes

…inclusion (#4656)

When filtering tests by UID (e.g., from Visual Studio), class names like
"ABCV" would incorrectly match "ABCVC" because the filter used
string.Contains() for matching.

This fix changes CouldMatchUidFilter to use word-boundary matching:
- Class names must be bounded by '.', '<', or '+'
- Method names must be preceded by '.' and followed by '.', '<', or '('

This ensures "ABCV" only matches the exact class name, not substrings
like "ABCVC".

Fixes #4656
Instead of using word-boundary matching for class names, use exact
prefix matching with namespace.classname to ensure we don't match
classes with the same name in different namespaces.
@claude
Copy link
Contributor

claude bot commented Feb 3, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

The changes correctly fix the substring matching issue while maintaining proper word-boundary semantics for test UIDs. The implementation properly handles the test ID format and adds comprehensive test coverage.

@claude
Copy link
Contributor

claude bot commented Feb 3, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.


Review Summary

This PR effectively fixes issue #4656 where substring matching in UID filters caused incorrect test inclusion. The implementation is sound:

Correctness: The change from substring matching to prefix and word-boundary matching correctly prevents ABCV from matching ABCVC tests.

CLAUDE.md Compliance:

  • Operates in the unified execution path (post-metadata-collection), so dual-mode requirements do not apply
  • No source generator or public API changes, so snapshot tests are not required
  • Uses modern C# features appropriately (collection expressions, StringComparison.Ordinal)
  • Performance impact is minimal (filter evaluation is not a hot path)

Test Coverage: Comprehensive regression tests in both the engine tests and integration tests validate the fix works correctly in both source-generated and reflection modes.

The architectural approach is particularly elegant—by using namespace-aware prefix matching with proper delimiters, the fix ensures exact matches without needing complex regex patterns.

var backtickIndex = className.IndexOf('`');
if (backtickIndex > 0)
{
classNameForMatching = className.Substring(0, backtickIndex);
Copy link
Contributor

@TimothyMakkison TimothyMakkison Feb 4, 2026

Choose a reason for hiding this comment

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

classNameForMatching = className.AsSpan().Slice(0, backtickIndex); and make ReadOnlySpan<char> classNameForMatching

Could prolly use ValueStringBuilder for expectedClassPrefix and expectedGenericClassPrefix but that's pretty pendanitc

@thomhurst thomhurst merged commit a3d2446 into main Feb 4, 2026
14 of 15 checks passed
@thomhurst thomhurst deleted the fix/4656-overlapping-class-name-filter branch February 4, 2026 22:48
thomhurst pushed a commit that referenced this pull request Feb 5, 2026
…ching

The CouldMatchUidFilter method was failing to match tests when using
VS Test Explorer because:

1. For nested types (e.g., Outer+Inner), Type.Name only returns the
   innermost class name, but UIDs contain the full path with '+' separators

2. For classes with constructor parameters, UIDs contain '(' after the
   class name (e.g., MyClass(String).0.0.Method), but the code only
   checked for '.' or '<'

This fix:
- Adds BuildClassNameForMatching() to construct the full nested type
  hierarchy matching TestIdentifierService.WriteTypeNameWithGenerics
- Validates the character after the class name prefix is a valid
  boundary: '.', '<', or '('
- Maintains protection against substring matching (ABCV vs ABCVC)

Fixes #4656 (follow-up issue after PR #4659)

https://claude.ai/code/session_016KqnWq2pDNM7sDLSn6LvRe
thomhurst added a commit that referenced this pull request Feb 5, 2026
…ching (#4669)

* fix: handle nested types and constructor parameters in UID filter matching

The CouldMatchUidFilter method was failing to match tests when using
VS Test Explorer because:

1. For nested types (e.g., Outer+Inner), Type.Name only returns the
   innermost class name, but UIDs contain the full path with '+' separators

2. For classes with constructor parameters, UIDs contain '(' after the
   class name (e.g., MyClass(String).0.0.Method), but the code only
   checked for '.' or '<'

This fix:
- Adds BuildClassNameForMatching() to construct the full nested type
  hierarchy matching TestIdentifierService.WriteTypeNameWithGenerics
- Validates the character after the class name prefix is a valid
  boundary: '.', '<', or '('
- Maintains protection against substring matching (ABCV vs ABCVC)

Fixes #4656 (follow-up issue after PR #4659)

https://claude.ai/code/session_016KqnWq2pDNM7sDLSn6LvRe

* fix: use ValueListBuilder and include generic type args in UID matching

Address code review feedback:

1. Performance: Replace List<string> with ValueListBuilder<string> and
   ValueStringBuilder to avoid allocations in the hot path during test
   discovery.

2. Bug fix: Include generic type arguments for all types in the nested
   hierarchy. For Outer<int>.Inner, the UID contains
   Outer<System.Int32>+Inner, not Outer+Inner.

The implementation now exactly mirrors TestIdentifierService.WriteTypeNameWithGenerics
to ensure consistent UID format matching.

https://claude.ai/code/session_016KqnWq2pDNM7sDLSn6LvRe

* test: add comprehensive tests for UID filter matching

Add test fixtures and integration tests covering:
- Nested classes (Outer+Inner format)
- Deeply nested classes (Outer+Inner+Deep format)
- Overlapping class names (FilterTest vs FilterTestHelper)
- Method name boundary matching (Test vs TestMethod)
- Original issue regression (ABCV vs ABCVC)

Test fixtures in TUnit.TestProject/Bugs/4656/UidFilterMatchingTests.cs
Integration tests in TUnit.Engine.Tests/UidFilterMatchingTests.cs

https://claude.ai/code/session_016KqnWq2pDNM7sDLSn6LvRe

* fix: resolve CI failures in test explorer UID filter matching

- Fix source generator crash (TUNIT0999) for non-generic types nested
  inside generic outer types by adding Arity > 0 guard in
  GloballyQualified() and skipping such types in test metadata generation
- Move [Arguments] from method level to class level for test classes
  with constructor parameters (fixes TUnit0038/TUnit0050 errors)
- Fix engine test tree node filters to use innermost class name instead
  of nested type path format (OuterClass+InnerClass -> InnerClass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve CI failures for generic and deeply nested test classes

Add [GenerateGenericTest] attributes to open generic test classes so they
can be instantiated with concrete type arguments. Remove DeeplyNestedClass
(3-level nesting fails in net8.0 AOT) and InnerNonGenericClass (nested
inside open generic, can't be source-generated).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [After(TestDiscovery)] TestDiscoveryContext.AllTests is incorrect

2 participants