Skip to content

Commit b5a12fb

Browse files
committed
fix(consume): fix test group/client clean-up tracking after introduction of sub-groups
1 parent 1ac603b commit b5a12fb

File tree

6 files changed

+611
-190
lines changed

6 files changed

+611
-190
lines changed

src/pytest_plugins/consume/simulators/enginex/conftest.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ def test_suite_description() -> str:
7979

8080
def pytest_collection_modifyitems(session, config, items):
8181
"""
82-
Build pre-allocation group test counts during collection phase.
82+
Build group test counts during collection phase.
8383
8484
This hook analyzes all collected test items to determine how many tests
85-
belong to each pre-allocation group, enabling automatic client cleanup
86-
when all tests in a group are complete.
85+
belong to each group (pre-allocation groups or xdist subgroups), enabling
86+
automatic client cleanup when all tests in a group are complete.
8787
"""
8888
# Only process items for enginex simulator
8989
if not hasattr(config, "_supported_fixture_formats"):
@@ -101,28 +101,46 @@ def pytest_collection_modifyitems(session, config, items):
101101
if hasattr(item, "callspec") and "test_case" in item.callspec.params:
102102
test_case = item.callspec.params["test_case"]
103103
if hasattr(test_case, "pre_hash"):
104-
pre_hash = test_case.pre_hash
105-
group_counts[pre_hash] = group_counts.get(pre_hash, 0) + 1
104+
# Get group identifier from xdist marker if available
105+
group_identifier = None
106+
for marker in item.iter_markers("xdist_group"):
107+
if hasattr(marker, "kwargs") and "name" in marker.kwargs:
108+
group_identifier = marker.kwargs["name"]
109+
break
110+
111+
# Fallback to pre_hash if no xdist marker (sequential execution)
112+
if group_identifier is None:
113+
group_identifier = test_case.pre_hash
114+
115+
group_counts[group_identifier] = group_counts.get(group_identifier, 0) + 1
106116

107117
# Store on session for later retrieval by test_tracker fixture
108118
session._pre_alloc_group_counts = group_counts
109-
logger.info(
110-
f"Collected {len(group_counts)} pre-allocation groups with tests: {dict(group_counts)}"
111-
)
119+
logger.info(f"Collected {len(group_counts)} groups with tests: {dict(group_counts)}")
112120
else:
113121
# Update tracker directly if it exists
114122
group_counts = {}
115123
for item in items:
116124
if hasattr(item, "callspec") and "test_case" in item.callspec.params:
117125
test_case = item.callspec.params["test_case"]
118126
if hasattr(test_case, "pre_hash"):
119-
pre_hash = test_case.pre_hash
120-
group_counts[pre_hash] = group_counts.get(pre_hash, 0) + 1
127+
# Get group identifier from xdist marker if available
128+
group_identifier = None
129+
for marker in item.iter_markers("xdist_group"):
130+
if hasattr(marker, "kwargs") and "name" in marker.kwargs:
131+
group_identifier = marker.kwargs["name"]
132+
break
133+
134+
# Fallback to pre_hash if no xdist marker (sequential execution)
135+
if group_identifier is None:
136+
group_identifier = test_case.pre_hash
137+
138+
group_counts[group_identifier] = group_counts.get(group_identifier, 0) + 1
121139

122-
for pre_hash, count in group_counts.items():
123-
test_tracker.set_group_test_count(pre_hash, count)
140+
for group_identifier, count in group_counts.items():
141+
test_tracker.set_group_test_count(group_identifier, count)
124142

125-
logger.info(f"Updated test tracker with {len(group_counts)} pre-allocation groups")
143+
logger.info(f"Updated test tracker with {len(group_counts)} groups")
126144

127145

128146
@pytest.fixture(scope="function")

src/pytest_plugins/consume/simulators/helpers/client_wrapper.py

Lines changed: 115 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,69 @@
2121
logger = logging.getLogger(__name__)
2222

2323

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+
2487
class ClientWrapper(ABC):
2588
"""
2689
Abstract base class for managing client instances in engine simulators.
@@ -275,8 +338,9 @@ class MultiTestClientManager:
275338
"""
276339
Singleton manager for coordinating multi-test clients across test execution.
277340
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).
280344
"""
281345

282346
_instance: Optional["MultiTestClientManager"] = None
@@ -294,7 +358,7 @@ def __init__(self) -> None:
294358
if hasattr(self, "_initialized") and self._initialized:
295359
return
296360

297-
self.multi_test_clients: Dict[str, MultiTestClient] = {}
361+
self.multi_test_clients: Dict[str, MultiTestClient] = {} # group_identifier -> client
298362
self.pre_alloc_path: Optional[Path] = None
299363
self.test_tracker: Optional["PreAllocGroupTestTracker"] = None
300364
self._initialized = True
@@ -322,12 +386,12 @@ def set_test_tracker(self, test_tracker: "PreAllocGroupTestTracker") -> None:
322386
self.test_tracker = test_tracker
323387
logger.debug("Test tracker set for automatic client cleanup")
324388

325-
def load_pre_alloc_group(self, pre_hash: str) -> PreAllocGroup:
389+
def load_pre_alloc_group(self, group_identifier: str) -> PreAllocGroup:
326390
"""
327-
Load the pre-allocation group for a given preHash.
391+
Load the pre-allocation group for a given group identifier.
328392
329393
Args:
330-
pre_hash: The hash identifying the pre-allocation group
394+
group_identifier: The group identifier (pre_hash or xdist group name)
331395
332396
Returns:
333397
The loaded PreAllocGroup
@@ -340,6 +404,8 @@ def load_pre_alloc_group(self, pre_hash: str) -> PreAllocGroup:
340404
if self.pre_alloc_path is None:
341405
raise RuntimeError("Pre-alloc path not set in MultiTestClientManager")
342406

407+
# Extract pre_hash from group identifier (handles subgroups)
408+
pre_hash = extract_pre_hash_from_group_identifier(group_identifier)
343409
pre_alloc_file = self.pre_alloc_path / f"{pre_hash}.json"
344410
if not pre_alloc_file.exists():
345411
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:
348414

349415
def get_or_create_multi_test_client(
350416
self,
351-
pre_hash: str,
417+
group_identifier: str,
352418
client_type: ClientType,
353419
) -> MultiTestClient:
354420
"""
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.
356422
357423
This method doesn't start the actual client - that's done by HiveTestSuite.
358424
It just manages the MultiTestClient wrapper objects.
359425
360426
Args:
361-
pre_hash: The hash identifying the pre-allocation group
427+
group_identifier: The group identifier (pre_hash or xdist group name)
362428
client_type: The type of client that will be started
363429
364430
Returns:
365431
The MultiTestClient wrapper instance
366432
367433
"""
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]
371437
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}")
373439
return multi_test_client
374440
else:
375441
# MultiTestClient exists but isn't running, remove it
376442
logger.warning(
377-
f"Found stopped MultiTestClient for pre-allocation group {pre_hash}, removing"
443+
f"Found stopped MultiTestClient for group {group_identifier}, removing"
378444
)
379-
del self.multi_test_clients[pre_hash]
445+
del self.multi_test_clients[group_identifier]
380446

381447
# 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)
383452

384453
# Create new MultiTestClient wrapper
385454
multi_test_client = MultiTestClient(
@@ -388,43 +457,43 @@ def get_or_create_multi_test_client(
388457
pre_alloc_group=pre_alloc_group,
389458
)
390459

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
393462

394463
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)})"
397466
)
398467

399468
return multi_test_client
400469

401470
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
403472
) -> Optional[Client]:
404473
"""
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.
406475
407476
Args:
408-
pre_hash: The hash identifying the pre-allocation group
477+
group_identifier: The group identifier (pre_hash or xdist group name)
409478
test_id: Optional test ID for completion tracking
410479
411480
Returns:
412481
The client instance if available, None otherwise
413482
414483
"""
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]
417486
if multi_test_client.is_running:
418487
multi_test_client.increment_test_count()
419488
return multi_test_client.client
420489
return None
421490

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:
423492
"""
424493
Mark a test as completed and trigger automatic client cleanup if appropriate.
425494
426495
Args:
427-
pre_hash: The hash identifying the pre-allocation group
496+
group_identifier: The group identifier (pre_hash or xdist group name)
428497
test_id: The unique test identifier
429498
430499
"""
@@ -433,57 +502,55 @@ def mark_test_completed(self, pre_hash: str, test_id: str) -> None:
433502
return
434503

435504
# 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)
437506

438507
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)
441510

442-
def _auto_stop_client_if_complete(self, pre_hash: str) -> None:
511+
def _auto_stop_client_if_complete(self, group_identifier: str) -> None:
443512
"""
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.
445514
446515
Args:
447-
pre_hash: The hash identifying the pre-allocation group
516+
group_identifier: The group identifier (pre_hash or xdist group name)
448517
449518
"""
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}")
452521
return
453522

454-
multi_test_client = self.multi_test_clients[pre_hash]
523+
multi_test_client = self.multi_test_clients[group_identifier]
455524
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")
457526
return
458527

459528
# Stop the client and remove from tracking
460529
logger.info(
461-
f"Auto-stopping client for pre-allocation group {pre_hash} - "
530+
f"Auto-stopping client for group {group_identifier} - "
462531
f"all tests completed ({multi_test_client.test_count} tests executed)"
463532
)
464533

465534
try:
466535
multi_test_client.stop()
467536
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}")
469538
finally:
470539
# 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}")
473542

474543
def stop_all_clients(self) -> None:
475544
"""Mark all multi-test clients as stopped."""
476545
logger.info(f"Marking all {len(self.multi_test_clients)} multi-test clients as stopped")
477546

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()):
479548
try:
480549
multi_test_client.stop()
481550
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}")
485552
finally:
486-
del self.multi_test_clients[pre_hash]
553+
del self.multi_test_clients[group_identifier]
487554

488555
logger.info("All MultiTestClient wrappers cleared")
489556

@@ -494,7 +561,8 @@ def get_client_count(self) -> int:
494561
def get_test_counts(self) -> Dict[str, int]:
495562
"""Get test counts for each multi-test client."""
496563
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()
498566
}
499567

500568
def reset(self) -> None:

0 commit comments

Comments
 (0)