21
21
logger = logging .getLogger (__name__ )
22
22
23
23
24
+ def get_group_identifier_from_request (request , pre_hash : str ) -> str :
25
+ """
26
+ Determine the appropriate group identifier for client tracking.
27
+
28
+ For xdist execution: Uses xdist group name (includes subgroup suffix if split)
29
+ For sequential execution: Uses pre_hash directly
30
+
31
+ Args:
32
+ request: The pytest request object containing test metadata
33
+ pre_hash: The pre-allocation group hash
34
+
35
+ Returns:
36
+ Group identifier string to use for client tracking
37
+
38
+ """
39
+ # Check if this test has an xdist_group marker (indicates xdist execution)
40
+ xdist_group_marker = None
41
+ iter_markers = getattr (request .node , "iter_markers" , None )
42
+ if iter_markers is None :
43
+ return pre_hash
44
+
45
+ for marker in iter_markers ("xdist_group" ):
46
+ xdist_group_marker = marker
47
+ break
48
+
49
+ if (
50
+ xdist_group_marker
51
+ and hasattr (xdist_group_marker , "kwargs" )
52
+ and "name" in xdist_group_marker .kwargs
53
+ ):
54
+ group_identifier = xdist_group_marker .kwargs ["name" ]
55
+ logger .debug (f"Using xdist group identifier: { group_identifier } " )
56
+ return group_identifier
57
+
58
+ # Fallback to pre_hash for sequential execution or when no xdist marker is found
59
+ logger .debug (f"Using pre_hash as group identifier: { pre_hash } " )
60
+ return pre_hash
61
+
62
+
63
+ def extract_pre_hash_from_group_identifier (group_identifier : str ) -> str :
64
+ """
65
+ Extract the pre_hash from a group identifier.
66
+
67
+ For xdist subgroups: Removes the subgroup suffix (e.g., "0x123:0" -> "0x123")
68
+ For sequential: Returns as-is (group_identifier == pre_hash)
69
+
70
+ Args:
71
+ group_identifier: The group identifier string
72
+
73
+ Returns:
74
+ The pre_hash without any subgroup suffix
75
+
76
+ """
77
+ if ":" in group_identifier :
78
+ # Split subgroup format: "pre_hash:subgroup_index"
79
+ pre_hash = group_identifier .split (":" , 1 )[0 ]
80
+ logger .debug (f"Extracted pre_hash { pre_hash } from group identifier { group_identifier } " )
81
+ return pre_hash
82
+
83
+ # No subgroup suffix, return as-is
84
+ return group_identifier
85
+
86
+
24
87
class ClientWrapper (ABC ):
25
88
"""
26
89
Abstract base class for managing client instances in engine simulators.
@@ -275,8 +338,9 @@ class MultiTestClientManager:
275
338
"""
276
339
Singleton manager for coordinating multi-test clients across test execution.
277
340
278
- This class tracks all multi-test clients by their preHash and ensures proper
279
- lifecycle management including cleanup at session end.
341
+ This class tracks all multi-test clients by their group identifier and ensures proper
342
+ lifecycle management including cleanup at session end. Group identifiers can be
343
+ either pre_hash (for sequential execution) or xdist group names (for parallel execution).
280
344
"""
281
345
282
346
_instance : Optional ["MultiTestClientManager" ] = None
@@ -294,7 +358,7 @@ def __init__(self) -> None:
294
358
if hasattr (self , "_initialized" ) and self ._initialized :
295
359
return
296
360
297
- self .multi_test_clients : Dict [str , MultiTestClient ] = {}
361
+ self .multi_test_clients : Dict [str , MultiTestClient ] = {} # group_identifier -> client
298
362
self .pre_alloc_path : Optional [Path ] = None
299
363
self .test_tracker : Optional ["PreAllocGroupTestTracker" ] = None
300
364
self ._initialized = True
@@ -322,12 +386,12 @@ def set_test_tracker(self, test_tracker: "PreAllocGroupTestTracker") -> None:
322
386
self .test_tracker = test_tracker
323
387
logger .debug ("Test tracker set for automatic client cleanup" )
324
388
325
- def load_pre_alloc_group (self , pre_hash : str ) -> PreAllocGroup :
389
+ def load_pre_alloc_group (self , group_identifier : str ) -> PreAllocGroup :
326
390
"""
327
- Load the pre-allocation group for a given preHash .
391
+ Load the pre-allocation group for a given group identifier .
328
392
329
393
Args:
330
- pre_hash : The hash identifying the pre-allocation group
394
+ group_identifier : The group identifier (pre_hash or xdist group name)
331
395
332
396
Returns:
333
397
The loaded PreAllocGroup
@@ -340,6 +404,8 @@ def load_pre_alloc_group(self, pre_hash: str) -> PreAllocGroup:
340
404
if self .pre_alloc_path is None :
341
405
raise RuntimeError ("Pre-alloc path not set in MultiTestClientManager" )
342
406
407
+ # Extract pre_hash from group identifier (handles subgroups)
408
+ pre_hash = extract_pre_hash_from_group_identifier (group_identifier )
343
409
pre_alloc_file = self .pre_alloc_path / f"{ pre_hash } .json"
344
410
if not pre_alloc_file .exists ():
345
411
raise FileNotFoundError (f"Pre-allocation file not found: { pre_alloc_file } " )
@@ -348,38 +414,41 @@ def load_pre_alloc_group(self, pre_hash: str) -> PreAllocGroup:
348
414
349
415
def get_or_create_multi_test_client (
350
416
self ,
351
- pre_hash : str ,
417
+ group_identifier : str ,
352
418
client_type : ClientType ,
353
419
) -> MultiTestClient :
354
420
"""
355
- Get an existing MultiTestClient or create a new one for the given preHash .
421
+ Get an existing MultiTestClient or create a new one for the given group identifier .
356
422
357
423
This method doesn't start the actual client - that's done by HiveTestSuite.
358
424
It just manages the MultiTestClient wrapper objects.
359
425
360
426
Args:
361
- pre_hash : The hash identifying the pre-allocation group
427
+ group_identifier : The group identifier (pre_hash or xdist group name)
362
428
client_type: The type of client that will be started
363
429
364
430
Returns:
365
431
The MultiTestClient wrapper instance
366
432
367
433
"""
368
- # Check if we already have a MultiTestClient for this preHash
369
- if pre_hash in self .multi_test_clients :
370
- multi_test_client = self .multi_test_clients [pre_hash ]
434
+ # Check if we already have a MultiTestClient for this group identifier
435
+ if group_identifier in self .multi_test_clients :
436
+ multi_test_client = self .multi_test_clients [group_identifier ]
371
437
if multi_test_client .is_running :
372
- logger .debug (f"Found existing MultiTestClient for pre-allocation group { pre_hash } " )
438
+ logger .debug (f"Found existing MultiTestClient for group { group_identifier } " )
373
439
return multi_test_client
374
440
else :
375
441
# MultiTestClient exists but isn't running, remove it
376
442
logger .warning (
377
- f"Found stopped MultiTestClient for pre-allocation group { pre_hash } , removing"
443
+ f"Found stopped MultiTestClient for group { group_identifier } , removing"
378
444
)
379
- del self .multi_test_clients [pre_hash ]
445
+ del self .multi_test_clients [group_identifier ]
380
446
381
447
# Load the pre-allocation group for this group
382
- pre_alloc_group = self .load_pre_alloc_group (pre_hash )
448
+ pre_alloc_group = self .load_pre_alloc_group (group_identifier )
449
+
450
+ # Extract pre_hash for the MultiTestClient constructor
451
+ pre_hash = extract_pre_hash_from_group_identifier (group_identifier )
383
452
384
453
# Create new MultiTestClient wrapper
385
454
multi_test_client = MultiTestClient (
@@ -388,43 +457,43 @@ def get_or_create_multi_test_client(
388
457
pre_alloc_group = pre_alloc_group ,
389
458
)
390
459
391
- # Track the MultiTestClient
392
- self .multi_test_clients [pre_hash ] = multi_test_client
460
+ # Track the MultiTestClient by group identifier
461
+ self .multi_test_clients [group_identifier ] = multi_test_client
393
462
394
463
logger .info (
395
- f"Created new MultiTestClient wrapper for pre-allocation group { pre_hash } "
396
- f"(total tracked clients: { len (self .multi_test_clients )} )"
464
+ f"Created new MultiTestClient wrapper for group { group_identifier } "
465
+ f"(pre_hash: { pre_hash } , total tracked clients: { len (self .multi_test_clients )} )"
397
466
)
398
467
399
468
return multi_test_client
400
469
401
470
def get_client_for_test (
402
- self , pre_hash : str , test_id : Optional [str ] = None
471
+ self , group_identifier : str , test_id : Optional [str ] = None
403
472
) -> Optional [Client ]:
404
473
"""
405
- Get the actual client instance for a test with the given preHash .
474
+ Get the actual client instance for a test with the given group identifier .
406
475
407
476
Args:
408
- pre_hash : The hash identifying the pre-allocation group
477
+ group_identifier : The group identifier (pre_hash or xdist group name)
409
478
test_id: Optional test ID for completion tracking
410
479
411
480
Returns:
412
481
The client instance if available, None otherwise
413
482
414
483
"""
415
- if pre_hash in self .multi_test_clients :
416
- multi_test_client = self .multi_test_clients [pre_hash ]
484
+ if group_identifier in self .multi_test_clients :
485
+ multi_test_client = self .multi_test_clients [group_identifier ]
417
486
if multi_test_client .is_running :
418
487
multi_test_client .increment_test_count ()
419
488
return multi_test_client .client
420
489
return None
421
490
422
- def mark_test_completed (self , pre_hash : str , test_id : str ) -> None :
491
+ def mark_test_completed (self , group_identifier : str , test_id : str ) -> None :
423
492
"""
424
493
Mark a test as completed and trigger automatic client cleanup if appropriate.
425
494
426
495
Args:
427
- pre_hash : The hash identifying the pre-allocation group
496
+ group_identifier : The group identifier (pre_hash or xdist group name)
428
497
test_id: The unique test identifier
429
498
430
499
"""
@@ -433,57 +502,55 @@ def mark_test_completed(self, pre_hash: str, test_id: str) -> None:
433
502
return
434
503
435
504
# Mark test as completed in tracker
436
- is_group_complete = self .test_tracker .mark_test_completed (pre_hash , test_id )
505
+ is_group_complete = self .test_tracker .mark_test_completed (group_identifier , test_id )
437
506
438
507
if is_group_complete :
439
- # All tests in this pre-allocation group are complete
440
- self ._auto_stop_client_if_complete (pre_hash )
508
+ # All tests in this group are complete
509
+ self ._auto_stop_client_if_complete (group_identifier )
441
510
442
- def _auto_stop_client_if_complete (self , pre_hash : str ) -> None :
511
+ def _auto_stop_client_if_complete (self , group_identifier : str ) -> None :
443
512
"""
444
- Automatically stop the client for a pre-allocation group if all tests are complete.
513
+ Automatically stop the client for a group if all tests are complete.
445
514
446
515
Args:
447
- pre_hash : The hash identifying the pre-allocation group
516
+ group_identifier : The group identifier (pre_hash or xdist group name)
448
517
449
518
"""
450
- if pre_hash not in self .multi_test_clients :
451
- logger .debug (f"No client found for pre-allocation group { pre_hash } " )
519
+ if group_identifier not in self .multi_test_clients :
520
+ logger .debug (f"No client found for group { group_identifier } " )
452
521
return
453
522
454
- multi_test_client = self .multi_test_clients [pre_hash ]
523
+ multi_test_client = self .multi_test_clients [group_identifier ]
455
524
if not multi_test_client .is_running :
456
- logger .debug (f"Client for pre-allocation group { pre_hash } is already stopped" )
525
+ logger .debug (f"Client for group { group_identifier } is already stopped" )
457
526
return
458
527
459
528
# Stop the client and remove from tracking
460
529
logger .info (
461
- f"Auto-stopping client for pre-allocation group { pre_hash } - "
530
+ f"Auto-stopping client for group { group_identifier } - "
462
531
f"all tests completed ({ multi_test_client .test_count } tests executed)"
463
532
)
464
533
465
534
try :
466
535
multi_test_client .stop ()
467
536
except Exception as e :
468
- logger .error (f"Error auto-stopping client for pre-allocation group { pre_hash } : { e } " )
537
+ logger .error (f"Error auto-stopping client for group { group_identifier } : { e } " )
469
538
finally :
470
539
# Remove from tracking to free memory
471
- del self .multi_test_clients [pre_hash ]
472
- logger .debug (f"Removed completed client from tracking: { pre_hash } " )
540
+ del self .multi_test_clients [group_identifier ]
541
+ logger .debug (f"Removed completed client from tracking: { group_identifier } " )
473
542
474
543
def stop_all_clients (self ) -> None :
475
544
"""Mark all multi-test clients as stopped."""
476
545
logger .info (f"Marking all { len (self .multi_test_clients )} multi-test clients as stopped" )
477
546
478
- for pre_hash , multi_test_client in list (self .multi_test_clients .items ()):
547
+ for group_identifier , multi_test_client in list (self .multi_test_clients .items ()):
479
548
try :
480
549
multi_test_client .stop ()
481
550
except Exception as e :
482
- logger .error (
483
- f"Error stopping MultiTestClient for pre-allocation group { pre_hash } : { e } "
484
- )
551
+ logger .error (f"Error stopping MultiTestClient for group { group_identifier } : { e } " )
485
552
finally :
486
- del self .multi_test_clients [pre_hash ]
553
+ del self .multi_test_clients [group_identifier ]
487
554
488
555
logger .info ("All MultiTestClient wrappers cleared" )
489
556
@@ -494,7 +561,8 @@ def get_client_count(self) -> int:
494
561
def get_test_counts (self ) -> Dict [str , int ]:
495
562
"""Get test counts for each multi-test client."""
496
563
return {
497
- pre_hash : client .test_count for pre_hash , client in self .multi_test_clients .items ()
564
+ group_identifier : client .test_count
565
+ for group_identifier , client in self .multi_test_clients .items ()
498
566
}
499
567
500
568
def reset (self ) -> None :
0 commit comments