8000 TST: re-arrange sub-process tests to be able to get coverage on them · matplotlib/matplotlib@28bce7c · GitHub
[go: up one dir, main page]

Skip to content

Commit 28bce7c

Browse files
committed
TST: re-arrange sub-process tests to be able to get coverage on them
By putting the implementation in top-level functions and then importing the test module in the sub-process we are able to get accurate coverage on these tests. pytest-cov takes care of all of the coverage related magic implicitly.
1 parent 2eec561 commit 28bce7c

File tree

2 files changed

+138
-133
lines changed

2 files changed

+138
-133
lines changed

lib/matplotlib/testing/__init__.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""
22
Helper functions for testing.
33
"""
4-
4+
from pathlib import Path
5+
from tempfile import TemporaryDirectory
56
import locale
67
import logging
8+
import os
79
import subprocess
8-
from pathlib import Path
9-
from tempfile import TemporaryDirectory
10+
import sys
1011

1112
import matplotlib as mpl
1213
from matplotlib import _api
@@ -49,6 +50,49 @@ def setup():
4950
set_reproducibility_for_testing()
5051

5152

53+
def subprocess_run_helper(module, func, *args, timeout, **extra_env):
54+
"""
55+
Run a function in a sub-process
56+
57+
Parameters
58+
----------
59+
module : str
60+
The name of the module to import from
61+
62+
func : function
63+
The function in this module to run
64+
65+
*args : str
66+
Any additional command line arguments to be passed to
67+
in the first argument to subprocess.run
68+
69+
**extra_env : Dict[str, str]
70+
Any additional envromental variables to be set for
71+
the subprocess.
72+
73+
"""
74+
target = func.__name__
75+
76+
proc = subprocess.run(
77+
[sys.executable,
78+
"-c",
79+
f"""
80+
from {module} import {target}
81+
{target}()
82+
""",
83+
*args],
84+
env={
85+
**os.environ,
86+
"SOURCE_DATE_EPOCH": "0",
87+
**extra_env
88+
},
89+
timeout=timeout, check=True,
90+
stdout=subprocess.PIPE,
91+
stderr=subprocess.PIPE,
92+
universal_newlines=True)
93+
return proc
94+
95+
5296
def _check_for_pgf(texsystem):
5397
"""
5498
Check if a given TeX system + pgf is available

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 91 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,14 @@
77
import signal
88
import subprocess
99
import sys
10-
import textwrap
1110
import time
1211
import urllib.request
1312

1413
import pytest
1514

1615
import matplotlib as mpl
1716
from matplotlib import _c_internal_utils
18-
19-
20-
def _run_function_in_subprocess(func):
21-
func_source = textwrap.dedent(inspect.getsource(func))
22-
func_source = func_source[func_source.index('\n')+1:] # Remove decorator
23-
return f"{func_source}\n{func.__name__}()"
24-
17+
from matplotlib.testing import subprocess_run_helper as _run_helper
2518

2619
# Minimal smoke-testing of the backends for which the dependencies are
2720
# PyPI-installable on CI. They are not available for all tested Python
@@ -94,8 +87,8 @@ def _test_interactive_impl():
9487
"webagg.open_in_browser": False,
9588
"webagg.port_retries": 1,
9689
})
97-
if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
98-
rcParams.update(json.loads(sys.argv[1]))
90+
91+
rcParams.update(json.loads(sys.argv[1]))
9992
backend = plt.rcParams["backend"].lower()
10093
assert_equal = TestCase().assertEqual
10194
assert_raises = TestCase().assertRaises
@@ -170,36 +163,23 @@ def test_interactive_backend(env, toolbar):
170163
if env["MPLBACKEND"] == "macosx":
171164
if toolbar == "toolmanager":
172165
pytest.skip("toolmanager is not implemented for macosx.")
166+
proc = _run_helper(__name__, _test_interactive_impl,
167+
json.dumps({"toolbar": toolbar}),
168+
timeout=_test_timeout,
169+
**env)
173170

174-
proc = subprocess.run(
175-
[sys.executable, "-c",
176-
inspect.getsource(_test_interactive_impl)
177-
+ "\n_test_interactive_impl()",
178-
json.dumps({"toolbar": toolbar})],
179-
env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env},
180-
timeout=_test_timeout,
181-
stdout=subprocess.PIPE, universal_newlines=True)
182-
if proc.returncode:
183-
pytest.fail("The subprocess returned with non-zero exit status "
184-
f"{proc.returncode}.")
185171
assert proc.stdout.count("CloseEvent") == 1
186172

187173

188-
# The source of this function gets extracted and run in another process, so it
189-
# must be fully self-contained.
190174
def _test_thread_impl():
191175
from concurrent.futures import ThreadPoolExecutor
192-
import json
193-
import sys
194176

195177 from matplotlib import pyplot as plt, rcParams
196178

197179
rcParams.update({
198180
"webagg.open_in_browser": False,
199181
"webagg.port_retries": 1,
200182
})
201-
if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
202-
rcParams.update(json.loads(sys.argv[1]))
203183

204184
# Test artist creation and drawing does not crash from thread
205185
# No other guarantees!
@@ -253,40 +233,65 @@ def _test_thread_impl():
253233
@pytest.mark.parametrize("env", _thread_safe_backends)
254234
@pytest.mark.flaky(reruns=3)
255235
def test_interactive_thread_safety(env):
256-
proc = subprocess.run(
257-
[sys.executable, "-c",
258-
inspect.getsource(_test_thread_impl) + "\n_test_thread_impl()"],
259-
env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env},
260-
timeout=_test_timeout, check=True,
261-
stdout=subprocess.PIPE, universal_newlines=True)
236+
proc = _run_helper(__name__, _test_thread_impl,
237+
timeout=_test_timeout, **env)
262238
assert proc.stdout.count("CloseEvent") == 1
263239

264240

241+
def _impl_test_lazy_auto_backend_selection():
242+
import matplotlib
243+
import matplotlib.pyplot as plt
244+
# just importing pyplot should not be enough to trigger resolution
245+
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
246+
assert not isinstance(bk, str)
247+
assert plt._backend_mod is None
248+
# but actually plotting should
249+
plt.plot(5)
250+
assert plt._backend_mod is not None
251+
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
252+
assert isinstance(bk, str)
253+
254+
265255
def test_lazy_auto_backend_selection():
256+
_run_helper(__name__, _impl_test_lazy_auto_backend_selection,
257+
timeout=_test_timeout)
266258

267-
@_run_function_in_subprocess
268-
def _impl():
269-
import matplotlib
270-
import matplotlib.pyplot as plt
271-
# just importing pyplot should not be enough to trigger resolution
272-
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
273-
assert not isinstance(bk, str)
274-
assert plt._backend_mod is None
275-
# but actually plotting should
276-
plt.plot(5)
277-
assert plt._backend_mod is not None
278-
bk = dict.__getitem__(matplotlib.rcParams, 'backend')
279-
assert isinstance(bk, str)
280-
281-
proc = subprocess.run(
282-
[sys.executable, "-c", _impl],
283-
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
284-
timeout=_test_timeout, check=True,
285-
stdout=subprocess.PIPE, universal_newlines=True)
286259

260+
def _implqt5agg():
261+
import matplotlib.backends.backend_qt5agg # noqa
262+
import sys
263+
264+
assert 'PyQt6' not in sys.modules
265+
assert 'pyside6' not in sys.modules
266+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
267+
268+
import matplotlib.backends.backend_qt5
269+
matplotlib.backends.backend_qt5.qApp
270+
271+
272+
def _implcairo():
273+
import matplotlib.backends.backend_qt5cairo # noqa
274+
import sys
275+
276+
assert 'PyQt6' not in sys.modules
277+
assert 'pyside6' not in sys.modules
278+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
279+
280+
import matplotlib.backends.backend_qt5
281+
matplotlib.backends.backend_qt5.qApp
282+
283+
284+
def _implcore():
285+
import matplotlib.backends.backend_qt5
286+
import sys
287+
288+
assert 'PyQt6' not in sys.modules
289+
assert 'pyside6' not in sys.modules
290+
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
291+
matplotlib.backends.backend_qt5.qApp
287292

288-
def test_qt5backends_uses_qt5():
289293

294+
def test_qt5backends_uses_qt5():
290295
qt5_bindings = [
291296
dep for dep in ['PyQt5', 'pyside2']
292297
if importlib.util.find_spec(dep) is not None
@@ -297,51 +302,9 @@ def test_qt5backends_uses_qt5():
297302
]
298303
if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
299304
pytest.skip('need both QT6 and QT5 bindings')
300-
301-
@_run_function_in_subprocess
302-
def _implagg():
303-
import matplotlib.backends.backend_qt5agg # noqa
304-
import sys
305-
306-
assert 'PyQt6' not in sys.modules
307-
assert 'pyside6' not in sys.modules
308-
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
309-
310-
@_run_function_in_subprocess
311-
def _implcairo():
312-
import matplotlib.backends.backend_qt5cairo # noqa
313-
import sys
314-
315-
assert 'PyQt6' not in sys.modules
316-
assert 'pyside6' not in sys.modules
317-
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
318-
319-
@_run_function_in_subprocess
320-
def _implcore():
321-
import matplotlib.backends.backend_qt5 # noqa
322-
import sys
323-
324-
assert 'PyQt6' not in sys.modules
325-
assert 'pyside6' not in sys.modules
326-
assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
327-
328-
subprocess.run(
329-
[sys.executable, "-c", _implagg],
330-
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
331-
timeout=_test_timeout, check=True,
332-
stdout=subprocess.PIPE, universal_newlines=True)
333-
334-
subprocess.run(
335-
[sys.executable, "-c", _implcairo],
336-
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
337-
timeout=_test_timeout, check=True,
338-
stdout=subprocess.PIPE, universal_newlines=True)
339-
340-
subprocess.run(
341-
[sys.executable, "-c", _implcore],
342-
env={**os.environ, "SOURCE_DATE_EPOCH": "0"},
343-
timeout=_test_timeout, check=True,
344-
stdout=subprocess.PIPE, universal_newlines=True)
305+
_run_helper(__name__, _implqt5agg, timeout=_test_timeout)
306+
_run_helper(__name__, _implcairo, timeout=_test_timeout)
307+
_run_helper(__name__, _implcore, timeout=_test_timeout)
345308

346309

347310
@pytest.mark.skipif('TF_BUILD' in os.environ,
@@ -352,7 +315,7 @@ def test_webagg():
352315
proc = subprocess.Popen(
353316
[sys.executable, "-c",
354317
inspect.getsource(_test_interactive_impl)
355-
+ "\n_test_interactive_impl()"],
318+
+ "\n_test_interactive_impl()", "{}"],
356319
env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
357320
url = "http://{}:{}".format(
358321
mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"])
@@ -374,37 +337,35 @@ def test_webagg():
374337
assert proc.wait(timeout=_test_timeout) == 0
375338

376339

340+
def _lazy_headless():
341+
import os
342+
import sys
343+
344+
# make it look headless
345+
os.environ.pop('DISPLAY', None)
346+
os.environ.pop('WAYLAND_DISPLAY', None)
347+
348+
# we should fast-track to Agg
349+
import matplotlib.pyplot as plt
350+
plt.get_backend() == 'agg'
351+
assert 'PyQt5' not in sys.modules
352+
353+
# make sure we really have pyqt installed
354+
import PyQt5 # noqa
355+
assert 'PyQt5' in sys.modules
356+
357+
# try to switch and make sure we fail with ImportError
358+
try:
359+
plt.switch_backend('qt5agg')
360+
except ImportError:
361+
...
362+
else:
363+
sys.exit(1)
364+
365+
377366
@pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
378367
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
379368
def test_lazy_linux_headless():
380-
test_script = """
381-
import os
382-
import sys
383-
384-
# make it look headless
385-
os.environ.pop('DISPLAY', None)
386-
os.environ.pop('WAYLAND_DISPLAY', None)
387-
388-
# we should fast-track to Agg
389-
import matplotlib.pyplot as plt
390-
plt.get_backend() == 'agg'
391-
assert 'PyQt5' not in sys.modules
392-
393-
# make sure we really have pyqt installed
394-
import PyQt5
395-
assert 'PyQt5' in sys.modules
396-
397-
# try to switch and make sure we fail with ImportError
398-
try:
399-
plt.switch_backend('qt5agg')
400-
except ImportError:
401-
...
402-
else:
403-
sys.exit(1)
404-
405-
"""
406-
proc = subprocess.run([sys.executable, "-c", test_script],
407-
env={**os.environ, "MPLBACKEND": ""})
408-
if proc.returncode:
409-
pytest.fail("The subprocess returned with non-zero exit status "
410-
f"{proc.returncode}.")
369+
proc = _run_helper(__name__, _lazy_headless,
370+
timeout=_test_timeout,
371+
MPLBACKEND="")

0 commit comments

Comments
 (0)
0