@@ -166,6 +166,96 @@ export function searchOutputForFailures(patterns: FailurePatternsConfig, output:
166166 return messages ;
167167}
168168
169+ /**
170+ * Finds the minimal regex fragments to match targets from the superset.
171+ * If a target is a prefix of a forbidden string, it returns an exact match.
172+ * Otherwise, it returns a minimal prefix.
173+ *
174+ * @param superset : the list of all individual test names (individual and or group names)
175+ * @param targets : the list of test names (individual and or group names) we want to match.
176+ * @returns : the list of regex fragments to match the targets without matching any other test from the superset
177+ */
178+ export function getMinimalRegexFragments ( superset : string [ ] , targets : string [ ] ) : string [ ] {
179+ interface TrieNode {
180+ children : Map < string , TrieNode > ;
181+ forbiddenCount : number ;
182+ }
183+
184+ const targetSet = new Set ( targets ) ;
185+ const forbidden = superset . filter ( s => ! targetSet . has ( s ) ) ;
186+
187+ if ( targets . length === 0 ) {
188+ return [ ] ;
189+ }
190+ if ( forbidden . length === 0 ) {
191+ return [ "^." ] ;
192+ }
193+
194+ const root : TrieNode = { children : new Map ( ) , forbiddenCount : 0 } ;
195+
196+ // 1. Build Trie with forbidden strings
197+ for ( const s of forbidden ) {
198+ let current = root ;
199+ for ( const char of s ) {
200+ if ( ! current . children . has ( char ) ) {
201+ current . children . set ( char , { children : new Map ( ) , forbiddenCount : 0 } ) ;
202+ }
203+ current = current . children . get ( char ) ! ;
204+ current . forbiddenCount ++ ;
205+ }
206+ }
207+
208+ const fragmentSet = new Set < string > ( ) ;
209+
210+ // 2. Identify the optimal regex fragment for each target
211+ for ( const target of targets ) {
212+ let current = root ;
213+ let prefix = "" ;
214+ let foundUnique = false ;
215+
216+ for ( const char of target ) {
217+ prefix += char ;
218+ const next = current . children . get ( char ) ;
219+
220+ if ( ! next || next . forbiddenCount === 0 ) {
221+ // Safe unique prefix found
222+ fragmentSet . add ( `^${ util . escapeStringForRegex ( prefix ) } ` ) ;
223+ foundUnique = true ;
224+ break ;
225+ }
226+ current = next ;
227+ }
228+
229+ // 3. The "Swallowed" Target Case
230+ // Target is a prefix of a forbidden string (e.g., "Test" vs "Test.1")
231+ if ( ! foundUnique ) {
232+ fragmentSet . add ( `^${ util . escapeStringForRegex ( target ) } $` ) ;
233+ }
234+ }
235+
236+ // 4. Remove redundant fragments
237+ // Sort by length: "Test\." (length 6) comes before "Test\.x\.1" (length 10)
238+ const sorted = Array . from ( fragmentSet ) . sort ( ( a , b ) => a . length - b . length ) ;
239+ const minimalFragments : string [ ] = [ ] ;
240+
241+ for ( const fragment of sorted ) {
242+ // If we already have a prefix that covers this fragment, skip it.
243+ // Note: 'existing' only matches 'fragment' if it's a true prefix (no $ anchor)
244+ const isRedundant = minimalFragments . some ( existing => {
245+ if ( existing . endsWith ( '$' ) ) {
246+ return false ; // Exact matches can't cover other things
247+ }
248+ return fragment . startsWith ( existing ) ;
249+ } ) ;
250+
251+ if ( ! isRedundant ) {
252+ minimalFragments . push ( fragment ) ;
253+ }
254+ }
255+
256+ return minimalFragments ;
257+ }
258+
169259function matchToTestMessage ( pat : FailurePattern , match : RegExpMatchArray ) : vscode . TestMessage {
170260 const file = match [ pat . file as number ] ;
171261 const line = pat . line ? parseLineMatch ( match [ pat . line ] ) : 0 ;
@@ -442,7 +532,8 @@ export class CTestDriver implements vscode.Disposable {
442532 const ctestArgs = await this . getCTestArgs ( driver , customizedTask , testPreset ) || [ ] ;
443533 if ( testsToRun && testsToRun . length > 0 ) {
444534 ctestArgs . push ( "-R" ) ;
445- const testsNamesRegex = testsToRun . map ( t => `^${ util . escapeStringForRegex ( t ) } \$` ) . join ( '|' ) ;
535+ const superset = this . getTestNames ( ) || [ ] ;
536+ const testsNamesRegex = getMinimalRegexFragments ( superset , testsToRun ) . join ( '|' ) ;
446537 ctestArgs . push ( testsNamesRegex ) ;
447538 }
448539
@@ -613,7 +704,8 @@ export class CTestDriver implements vscode.Disposable {
613704
614705 run . started ( test ) ;
615706
616- const _ctestArgs = driver . ctestArgs . concat ( '-R' , `^${ util . escapeStringForRegex ( test . id ) } \$` ) ;
707+ const superset = this . getTestNames ( ) || [ ] ;
708+ const _ctestArgs = driver . ctestArgs . concat ( '-R' , getMinimalRegexFragments ( superset , [ test . id ] ) . join ( '|' ) ) ;
617709
618710 const testResults = await this . runCTestImpl ( driver . driver , driver . ctestPath , _ctestArgs , cancellation , customizedTask , consumer ) ;
619711
@@ -651,21 +743,21 @@ export class CTestDriver implements vscode.Disposable {
651743 // then there may be a scenario when the user requested only a subset of tests to be ran.
652744 // In this case, we should specifically use the -R flag to select the exact tests.
653745 // Otherwise, we can leave it to the -T flag to run all tests.
746+ let targetTests : vscode . TestItem [ ] | undefined ;
654747 if ( entryPoint === RunCTestHelperEntryPoint . TestExplorer && testExplorer && this . _tests && this . _tests . tests . length !== driver . tests . length ) {
655- uniqueCtestArgs . push ( "-R" ) ;
656- const testsNamesRegex = driver . tests . map ( t => {
657- run . started ( t ) ;
658- return `^${ util . escapeStringForRegex ( t . id ) } \$` ;
659- } ) . join ( '|' ) ;
660- uniqueCtestArgs . push ( testsNamesRegex ) ;
748+ targetTests = driver . tests ;
661749 } else if ( testsToRun && testsToRun . length > 0 ) {
750+ targetTests = driver . tests . filter ( t => testsToRun . includes ( t . id ) ) ;
751+ }
752+
753+ if ( targetTests ) {
662754 uniqueCtestArgs . push ( "-R" ) ;
663- const tests = driver . tests . filter ( t => testsToRun . includes ( t . id ) ) ;
664- const testsNamesRegex = tests . map ( t => {
755+ const superset = this . getTestNames ( ) || [ ] ;
756+ const targets = targetTests . map ( t => {
665757 run . started ( t ) ;
666- return `^ ${ util . escapeStringForRegex ( t . id ) } \$` ;
667- } ) . join ( '|' ) ;
668- uniqueCtestArgs . push ( testsNamesRegex ) ;
758+ return t . id ;
759+ } ) ;
760+ uniqueCtestArgs . push ( getMinimalRegexFragments ( superset , targets ) . join ( '|' ) ) ;
669761 }
670762
671763 const testResults = await this . runCTestImpl ( uniqueDriver , uniqueCtestPath , uniqueCtestArgs , cancellation , customizedTask , consumer ) ;
0 commit comments