11using System . Diagnostics . CodeAnalysis ;
2+ using Microsoft . Testing . Platform . Extensions . Messages ;
3+ using Microsoft . Testing . Platform . Requests ;
24using TUnit . Core ;
35using TUnit . Core . Enums ;
46using TUnit . Core . Exceptions ;
@@ -114,8 +116,18 @@ private async Task<object> CreateInstance(TestMetadata metadata, Type[] resolved
114116#if NET6_0_OR_GREATER
115117 [ RequiresUnreferencedCode ( "Test building in reflection mode uses generic type resolution which requires unreferenced code" ) ]
116118#endif
117- public async Task < IEnumerable < AbstractExecutableTest > > BuildTestsFromMetadataAsync ( TestMetadata metadata )
119+ public async Task < IEnumerable < AbstractExecutableTest > > BuildTestsFromMetadataAsync ( TestMetadata metadata , TestBuildingContext buildingContext )
118120 {
121+ // OPTIMIZATION: Pre-filter in execution mode to skip building tests that cannot match the filter
122+ if ( buildingContext . IsForExecution && buildingContext . Filter != null )
123+ {
124+ if ( ! CouldTestMatchFilter ( buildingContext . Filter , metadata ) )
125+ {
126+ // This test class cannot match the filter - skip all expensive work!
127+ return Array . Empty < AbstractExecutableTest > ( ) ;
128+ }
129+ }
130+
119131 var tests = new List < AbstractExecutableTest > ( ) ;
120132
121133 try
@@ -126,7 +138,7 @@ public async Task<IEnumerable<AbstractExecutableTest>> BuildTestsFromMetadataAsy
126138 // Build tests from each concrete instantiation
127139 foreach ( var concreteMetadata in genericMetadata . ConcreteInstantiations . Values )
128140 {
129- var concreteTests = await BuildTestsFromMetadataAsync ( concreteMetadata ) ;
141+ var concreteTests = await BuildTestsFromMetadataAsync ( concreteMetadata , buildingContext ) ;
130142 tests . AddRange ( concreteTests ) ;
131143 }
132144 return tests ;
@@ -1563,4 +1575,111 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
15631575 return await CreateFailedTestForDataGenerationError ( metadata , ex ) ;
15641576 }
15651577 }
1578+
1579+ /// <summary>
1580+ /// Determines if a test could potentially match the filter without building the full test object.
1581+ /// This is a conservative check - returns true unless we can definitively rule out the test.
1582+ /// </summary>
1583+ private bool CouldTestMatchFilter ( ITestExecutionFilter filter , TestMetadata metadata )
1584+ {
1585+ #pragma warning disable TPEXP
1586+ return filter switch
1587+ {
1588+ null => true ,
1589+ NopFilter => true ,
1590+ TreeNodeFilter treeFilter => CouldMatchTreeNodeFilter ( treeFilter , metadata ) ,
1591+ TestNodeUidListFilter uidFilter => CouldMatchUidFilter ( uidFilter , metadata ) ,
1592+ _ => true // Unknown filter type - be conservative
1593+ } ;
1594+ #pragma warning restore TPEXP
1595+ }
1596+
1597+ /// <summary>
1598+ /// Checks if a test could match a TestNodeUidListFilter by checking if any UID contains
1599+ /// the namespace, class name, and method name.
1600+ /// </summary>
1601+ private static bool CouldMatchUidFilter ( TestNodeUidListFilter filter , TestMetadata metadata )
1602+ {
1603+ var classMetadata = metadata . MethodMetadata . Class ;
1604+ var namespaceName = classMetadata . Namespace ?? "" ;
1605+ var className = metadata . TestClassType . Name ;
1606+ var methodName = metadata . TestMethodName ;
1607+
1608+ // Check if any UID in the filter contains all three components
1609+ foreach ( var uid in filter . TestNodeUids )
1610+ {
1611+ var uidValue = uid . Value ;
1612+ if ( uidValue . Contains ( namespaceName ) &&
1613+ uidValue . Contains ( className ) &&
1614+ uidValue . Contains ( methodName ) )
1615+ {
1616+ return true ;
1617+ }
1618+ }
1619+
1620+ return false ;
1621+ }
1622+
1623+ /// <summary>
1624+ /// Checks if a test could match a TreeNodeFilter by building the test path and checking the filter.
1625+ /// </summary>
1626+ #pragma warning disable TPEXP
1627+ private bool CouldMatchTreeNodeFilter ( TreeNodeFilter filter , TestMetadata metadata )
1628+ {
1629+ var filterString = filter . Filter ;
1630+
1631+ // No filter means match all
1632+ if ( string . IsNullOrEmpty ( filterString ) )
1633+ {
1634+ return true ;
1635+ }
1636+
1637+ // If the filter contains property conditions, strip them for path-only matching
1638+ // Property conditions will be evaluated in the second pass after tests are fully built
1639+ TreeNodeFilter pathOnlyFilter ;
1640+ if ( filterString . Contains ( '[' ) )
1641+ {
1642+ // Strip all property conditions: [key=value]
1643+ // Use regex to remove all [...] blocks
1644+ var strippedFilterString = System . Text . RegularExpressions . Regex . Replace ( filterString , @"\[([^\]]*)\]" , "" ) ;
1645+
1646+ // Create a new TreeNodeFilter with the stripped filter string using reflection
1647+ pathOnlyFilter = CreateTreeNodeFilterViaReflection ( strippedFilterString ) ;
1648+ }
1649+ else
1650+ {
1651+ pathOnlyFilter = filter ;
1652+ }
1653+
1654+ var path = BuildPathFromMetadata ( metadata ) ;
1655+ var emptyPropertyBag = new PropertyBag ( ) ;
1656+ return pathOnlyFilter . MatchesFilter ( path , emptyPropertyBag ) ;
1657+ }
1658+
1659+ /// <summary>
1660+ /// Creates a TreeNodeFilter instance via reflection since it doesn't have a public constructor.
1661+ /// </summary>
1662+ private static TreeNodeFilter CreateTreeNodeFilterViaReflection ( string filterString )
1663+ {
1664+ var constructor = typeof ( TreeNodeFilter ) . GetConstructors (
1665+ System . Reflection . BindingFlags . NonPublic | System . Reflection . BindingFlags . Instance ) [ 0 ] ;
1666+
1667+ return ( TreeNodeFilter ) constructor . Invoke ( new object [ ] { filterString } ) ;
1668+ }
1669+ #pragma warning restore TPEXP
1670+
1671+ /// <summary>
1672+ /// Builds the test path from metadata, matching the format used by TestFilterService.
1673+ /// Path format: /AssemblyName/Namespace/ClassName/MethodName
1674+ /// </summary>
1675+ private static string BuildPathFromMetadata ( TestMetadata metadata )
1676+ {
1677+ var classMetadata = metadata . MethodMetadata . Class ;
1678+ var assemblyName = classMetadata . Assembly . Name ?? metadata . TestClassType . Assembly . GetName ( ) . Name ?? "*" ;
1679+ var namespaceName = classMetadata . Namespace ?? "*" ;
1680+ var className = classMetadata . Name ;
1681+ var methodName = metadata . TestMethodName ;
1682+
1683+ return $ "/{ assemblyName } /{ namespaceName } /{ className } /{ methodName } ";
1684+ }
15661685}
0 commit comments