Skip to content

Racing condition xdist fix #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions ultraplot/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@ def test_something(rng):
return np.random.default_rng(seed=SEED)


@pytest.fixture(autouse=True)
def isolate_mpl_testing():
"""
Isolate matplotlib testing for parallel execution.

This prevents race conditions in parallel testing (pytest-xdist) where
multiple processes can interfere with each other's image comparison tests.
The main issue is that pytest-mpl uses shared temporary directories that
can conflict between processes.
"""
import matplotlib as mpl
import matplotlib.pyplot as plt
import tempfile
import os

# Store original backend and ensure consistent state
original_backend = mpl.get_backend()
if original_backend != "Agg":
mpl.use("Agg", force=True)

# Clear any existing figures
plt.close("all")

# Create process-specific temporary directory for mpl results
# This prevents file conflicts between parallel processes
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
with tempfile.TemporaryDirectory(prefix=f"mpl_test_{worker_id}_") as temp_dir:
os.environ["MPL_TEST_TEMP_DIR"] = temp_dir

yield

# Clean up after test
plt.close("all")
uplt.close("all")

# Remove environment variable
if "MPL_TEST_TEMP_DIR" in os.environ:
del os.environ["MPL_TEST_TEMP_DIR"]

# Restore original backend
if original_backend != "Agg":
mpl.use(original_backend, force=True)


@pytest.fixture(autouse=True)
def close_figures_after_test():
"""Automatically close all figures after each test."""
Expand Down Expand Up @@ -139,11 +183,20 @@ def pytest_configure(config):
- Suppresses verbose matplotlib logging
- Registers the StoreFailedMplPlugin for enhanced functionality
- Sets up the plugin regardless of cleanup options (HTML reports always available)
- Configures process-specific temporary directories for parallel testing
"""
# Suppress ultraplot config loading which mpl does not recognize
logging.getLogger("matplotlib").setLevel(logging.ERROR)
logging.getLogger("ultraplot").setLevel(logging.WARNING)

# Configure process-specific results directory for parallel testing
worker_id = os.environ.get("PYTEST_XDIST_WORKER", "master")
if (
not hasattr(config.option, "mpl_results_path")
or not config.option.mpl_results_path
):
config.option.mpl_results_path = f"./mpl-results-{worker_id}"

try:
# Always register the plugin - it provides enhanced functionality beyond just cleanup
config.pluginmanager.register(StoreFailedMplPlugin(config))
Expand Down
4 changes: 2 additions & 2 deletions ultraplot/tests/test_1dplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ def test_heatmap_labels(rng):
return fig


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

inax.graph(
g,
Expand Down
Loading