test: force headless Matplotlib backend for local + CI test runs#197
Open
petercorke wants to merge 1 commit into
Open
test: force headless Matplotlib backend for local + CI test runs#197petercorke wants to merge 1 commit into
petercorke wants to merge 1 commit into
Conversation
Local `pytest` runs were popping up real windows and, for a few tests,
hanging outright. Two separate problems, one root cause:
1. CI sets MPLBACKEND=Agg via the workflow env, but nothing did that
for local runs. Several modules (geom2d, geom3d, spline, animate)
import matplotlib.pyplot at module load time, so whatever backend
is active when they're first imported sticks for the whole
session. Locally that's the platform's interactive backend, hence
the popups.
Fix: tests/conftest.py sets os.environ.setdefault("MPLBACKEND",
"Agg") before anything else runs. conftest.py is guaranteed to
load before pytest imports any test module, so this closes the gap
for local runs without touching the CI workflow (now redundant
there, not conflicting). setdefault(), not a hard override, keeps
an escape hatch: `MPLBACKEND=MacOSX pytest ...` still shows you a
real plot when you actually want one.
2. Forcing Agg everywhere surfaced a latent bug: Twist/pose animation
tests use tranimate(..., wait=True), which busy-loops on
`plt.pause()` waiting for FuncAnimation's timer callback to
deregister itself. That deregistration is driven by the GUI event
loop, which Agg doesn't have — so under Agg the loop never exits.
Confirmed with a hard-alarm timeout: the affected test consumed a
full 12s budget with zero output, versus completing normally under
a real backend.
These tests were already skipped in CI (via an `os.environ.get(
"CI") == "true"` check) for a related but distinct reason — no
display in the CI runner. That check happened to also mask this
hang, but only in CI; locally, before this change, it ran fine
because a real backend really does pump the event loop. Once Agg
is forced locally too, the same hang would trigger there.
Fix: broadened the three affected skip conditions (test_animate in
test_transforms3d_plot.py; the two animate=True visualize tests
and the inline guard in test_spline.py) to additionally skip
whenever plt.get_backend() == "agg", which is the actual root
cause rather than the CI-env-var proxy for it. Purely additive —
no existing skip condition was removed, so CI behaviour is
unchanged. Everything else (plain, non-animated plotting tests)
now safely runs headless both locally and in CI, which is strictly
more coverage than before.
Verified: full suite passes locally with CI unset (339 passed, 3
skipped, 1.6s, no windows, no hangs) and with CI=true set (338 passed,
4 skipped, matching prior CI behaviour).
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Running
pytestlocally pops up real Matplotlib windows, and a couple ofanimation tests hang outright. CI already sets
MPLBACKEND=Aggin theworkflow env, but that never applied to local runs — several modules
(
geom2d,geom3d,spline,animate) importmatplotlib.pyplotatmodule load time, so whichever backend is active at that point sticks for
the whole session, and locally that's the platform's interactive backend.
Fix
tests/conftest.py(new):os.environ.setdefault("MPLBACKEND", "Agg")before anything else runs.
conftest.pyis guaranteed to load beforepytest imports any test module, so this closes the gap for local runs
without touching the CI workflow (redundant there now, not conflicting).
setdefault(), not a hard override, soMPLBACKEND=MacOSX pytest ...still shows a real plot when you actually want to eyeball one.
Forcing Agg everywhere surfaced a real, previously-masked hang:
tranimate(..., wait=True)busy-loops onplt.pause()waiting forFuncAnimation's timer callback to deregister itself — driven by theGUI event loop, which Agg doesn't have. Confirmed with a hard-alarm
timeout: the affected test ate a full 12s budget with zero output vs.
completing normally under a real backend. These tests were already
skipped in CI via
os.environ.get("CI") == "true", which happened toalso mask this locally-only-when-Agg-is-forced hang — but only in CI;
locally (pre-Agg) they ran fine because a real backend genuinely pumps
the event loop.
Broadened the three affected skip conditions (
test_animateintest_transforms3d_plot.py; the twoanimate=Truetest_visualizetests and the inline guard in
test_spline.py) to additionally skipwhen
plt.get_backend() == "agg"— the actual root cause, rather thanthe CI-env-var proxy for it. Purely additive, no existing condition
removed, so CI behaviour is unchanged. Everything else (plain,
non-animated plotting tests) now safely runs headless both locally and
in CI — strictly more coverage than before.
Testing
CIunset (simulating a local run): 339 passed, 3 skipped,1.6s, no windows, no hangs.
CI=true(simulating CI): 338 passed, 4 skipped — matchesprior CI behaviour.
black --checkclean at the pinned 23.10.0.