Skip to content

Commit fb05cc5

Browse files
cvanelterenCopilot
andcommitted
Racing condition xdist fix (#273)
* minor fixes * isolate tmp files in workers * Update ultraplot/tests/conftest.py Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent c0931be commit fb05cc5

File tree

2 files changed

+55
-2
lines changed

2 files changed

+55
-2
lines changed

ultraplot/tests/conftest.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,50 @@ def test_something(rng):
5454
return np.random.default_rng(seed=SEED)
5555

5656

57+
@pytest.fixture(autouse=True)
58+
def isolate_mpl_testing():
59+
"""
60+
Isolate matplotlib testing for parallel execution.
61+
62+
This prevents race conditions in parallel testing (pytest-xdist) where
63+
multiple processes can interfere with each other's image comparison tests.
64+
The main issue is that pytest-mpl uses shared temporary directories that
65+
can conflict between processes.
66+
"""
67+
import matplotlib as mpl
68+
import matplotlib.pyplot as plt
69+
import tempfile
70+
import os
71+
72+
# Store original backend and ensure consistent state
73+
original_backend = mpl.get_backend()
74+
if original_backend != "Agg":
75+
mpl.use("Agg", force=True)
76+
77+
# Clear any existing figures
78+
plt.close("all")
79+
80+
# Create process-specific temporary directory for mpl results
81+
# This prevents file conflicts between parallel processes
82+
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
83+
with tempfile.TemporaryDirectory(prefix=f"mpl_test_{worker_id}_") as temp_dir:
84+
os.environ["MPL_TEST_TEMP_DIR"] = temp_dir
85+
86+
yield
87+
88+
# Clean up after test
89+
plt.close("all")
90+
uplt.close("all")
91+
92+
# Remove environment variable
93+
if "MPL_TEST_TEMP_DIR" in os.environ:
94+
del os.environ["MPL_TEST_TEMP_DIR"]
95+
96+
# Restore original backend
97+
if original_backend != "Agg":
98+
mpl.use(original_backend, force=True)
99+
100+
57101
@pytest.fixture(autouse=True)
58102
def close_figures_after_test():
59103
"""Automatically close all figures after each test."""
@@ -139,11 +183,20 @@ def pytest_configure(config):
139183
- Suppresses verbose matplotlib logging
140184
- Registers the StoreFailedMplPlugin for enhanced functionality
141185
- Sets up the plugin regardless of cleanup options (HTML reports always available)
186+
- Configures process-specific temporary directories for parallel testing
142187
"""
143188
# Suppress ultraplot config loading which mpl does not recognize
144189
logging.getLogger("matplotlib").setLevel(logging.ERROR)
145190
logging.getLogger("ultraplot").setLevel(logging.WARNING)
146191

192+
# Configure process-specific results directory for parallel testing
193+
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
194+
if (
195+
not hasattr(config.option, "mpl_results_path")
196+
or not config.option.mpl_results_path
197+
):
198+
config.option.mpl_results_path = f"./mpl-results-{worker_id}"
199+
147200
try:
148201
# Always register the plugin - it provides enhanced functionality beyond just cleanup
149202
config.pluginmanager.register(StoreFailedMplPlugin(config))

ultraplot/tests/test_1dplots.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def test_heatmap_labels(rng):
525525
return fig
526526

527527

528-
@pytest.mark.mpl_image_compare()
528+
@pytest.mark.mpl_image_compare
529529
def test_networks(rng):
530530
"""
531531
Create a baseline network graph that tests
@@ -575,7 +575,7 @@ def test_networks(rng):
575575
inax = ax.inset_axes([*pos, 0.2, 0.2], zoom=0)
576576
layout_kw = {}
577577
if layout in ("random", "spring", "arf"):
578-
layout_kw = dict(seed=SEED)
578+
layout_kw = dict(seed=np.random.default_rng(SEED))
579579

580580
inax.graph(
581581
g,

0 commit comments

Comments
 (0)