@@ -385,19 +385,34 @@ func applyPythonFormulaTestScope(req *runtimev0.TestRequest, spec *pythonhelpers
385385 // owns pytest's private distinction between positional collection targets
386386 // and -k name expressions.
387387 if spec .Output == pythonhelpers .OutputJUnitXML {
388- var exactSelectors , namePatterns []string
388+ target := strings .TrimSpace (req .GetTarget ())
389+ var exactSelectors , relativeNodeSelectors , namePatterns []string
389390 for _ , filter := range req .GetFilters () {
390- if isExactPytestSelector (filter ) {
391+ if isRelativePytestNodeSelector (filter ) {
392+ relativeNodeSelectors = append (relativeNodeSelectors , strings .TrimSpace (filter ))
393+ } else if isExactPytestSelector (filter ) {
391394 exactSelectors = append (exactSelectors , filter )
392395 } else if strings .TrimSpace (filter ) != "" {
393396 namePatterns = append (namePatterns , filter )
394397 }
395398 }
399+ if len (relativeNodeSelectors ) > 0 {
400+ if ! isPytestFileTarget (target ) {
401+ return fmt .Errorf ("Python relative node selectors %q require one .py target, got %q" , relativeNodeSelectors , target )
402+ }
403+ if len (exactSelectors ) > 0 || len (namePatterns ) > 0 {
404+ return fmt .Errorf ("Python relative node selectors cannot be mixed with independent exact selectors or name filters; use a typed TestSelection" )
405+ }
406+ for _ , selector := range relativeNodeSelectors {
407+ exactSelectors = append (exactSelectors , target + "::" + strings .TrimPrefix (selector , "::" ))
408+ }
409+ target = ""
410+ }
396411 nameFilters , err := normalizePytestNameFilters (namePatterns )
397412 if err != nil {
398413 return err
399414 }
400- if target := strings . TrimSpace ( req . GetTarget ()); target != "" {
415+ if target != "" {
401416 exactSelectors = append (exactSelectors , target )
402417 }
403418 if len (exactSelectors ) > 0 {
@@ -441,11 +456,37 @@ func isExactPytestSelector(value string) bool {
441456 return strings .HasSuffix (normalized , ".py" ) || strings .Contains (normalized , ".py/" )
442457}
443458
459+ // isRelativePytestNodeSelector recognizes the pytest identity suffix callers
460+ // naturally pair with TestRequest.target: `Class::method`. It is not a valid
461+ // positional operand on its own. The runtime joins it to the file target so an
462+ // identical logical request cannot accidentally execute two unrelated
463+ // selectors and collect zero cases.
464+ func isRelativePytestNodeSelector (value string ) bool {
465+ value = strings .TrimSpace (value )
466+ if ! strings .Contains (value , "::" ) {
467+ return false
468+ }
469+ return ! isPytestFileTarget (strings .SplitN (value , "::" , 2 )[0 ])
470+ }
471+
472+ func isPytestFileTarget (value string ) bool {
473+ value = strings .TrimSpace (strings .ReplaceAll (value , `\` , "/" ))
474+ return value != "" && ! strings .Contains (value , "::" ) && strings .HasSuffix (value , ".py" )
475+ }
476+
444477func pythonDefaultTestScope (req * runtimev0.TestRequest ) (string , []string , error ) {
445478 if req .GetSelection () == nil {
446479 target := strings .TrimSpace (req .GetTarget ())
447480 namePatterns := make ([]string , 0 , len (req .GetFilters ()))
481+ var relativeNode string
448482 for _ , filter := range req .GetFilters () {
483+ if isRelativePytestNodeSelector (filter ) {
484+ if relativeNode != "" {
485+ return "" , nil , fmt .Errorf ("Python default test request has multiple relative node selectors; use one typed TestSelection" )
486+ }
487+ relativeNode = strings .TrimSpace (filter )
488+ continue
489+ }
449490 if ! isExactPytestSelector (filter ) {
450491 namePatterns = append (namePatterns , filter )
451492 continue
@@ -455,6 +496,12 @@ func pythonDefaultTestScope(req *runtimev0.TestRequest) (string, []string, error
455496 }
456497 target = strings .TrimSpace (filter )
457498 }
499+ if relativeNode != "" {
500+ if ! isPytestFileTarget (target ) || len (namePatterns ) > 0 {
501+ return "" , nil , fmt .Errorf ("Python relative node selector %q requires one .py target and no name filters; use a typed TestSelection" , relativeNode )
502+ }
503+ target += "::" + strings .TrimPrefix (relativeNode , "::" )
504+ }
458505 filters , err := normalizePytestNameFilters (namePatterns )
459506 return target , filters , err
460507 }
0 commit comments