Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit efc7f81

Browse files
tacaswellQuLogic
andcommitted
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. Also get coverage information out of isolated tk tests. Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent fdfbf7c commit efc7f81

File tree

3 files changed

+137
-136
lines changed

3 files changed

+137
-136
lines changed

doc/api/backend_qt_api.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ change without warning [#]_.
3737
Previously Matplotlib's Qt backends had the Qt version number in the name, both
3838
in the module and the :rc:`backend` value
3939
(e.g. ``matplotlib.backends.backend_qt4agg`` and
40-
``matplotlib.backends.backend_qt5agg``), however as part of adding support for
40+
``matplotlib.backends.backend_qt5agg``). However as part of adding support for
4141
Qt6 we were able to support both Qt5 and Qt6 with a single implementation with
4242
all of the Qt version and binding support handled in
4343
`~matplotlib.backends.qt_compat`. A majority of the renderer agnostic Qt code

lib/matplotlib/testing/__init__.py

Lines changed: 44 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,46 @@ def setup():
4950
set_reproducibility_for_testing()
5051

5152

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

lib/matplotlib/tests/test_backends_interactive.py

Lines changed: 92 additions & 132 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

2720
# Minimal smoke-testing of the backends for which the dependencies are
@@ -95,8 +88,8 @@ def _test_interactive_impl():
9588
"webagg.open_in_browser": False,
9689
"webagg.port_retries": 1,
9790
})
98-
if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
99-
rcParams.update(json.loads(sys.argv[1]))
91+
92+
rcParams.update(json.loads(sys.argv[1]))
10093
backend = plt.rcParams["backend"].lower()
10194
assert_equal = TestCase().assertEqual
10295
assert_raises = TestCase().assertRaises
@@ -171,36 +164,23 @@ def test_interactive_backend(env, toolbar):
171164
if env["MPLBACKEND"] == "macosx":
172165
if toolbar == "toolmanager":
173166
pytest.skip("toolmanager is not implemented for macosx.")
167+
proc = _run_helper(_test_interactive_impl,
168+
json.dumps({"toolbar": toolbar}),
169+
timeout=_test_timeout,
170+
**env)
174171

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

188174

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

196178
from matplotlib import pyplot as plt, rcParams
197179

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

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

265241

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

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

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

289-
def test_qt5backends_uses_qt5():
290294

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

347311

348312
def _impl_test_cross_Qt_imports():
@@ -378,11 +342,11 @@ def test_cross_Qt_imports():
378342
for qt6 in qt6_bindings:
379343
for pair in ([qt5, qt6], [qt6, qt5]):
380344
try:
381-
_run_helper(__name__, _impl_test_cross_Qt_imports,
345+
_run_helper(_impl_test_cross_Qt_imports,
382346
*pair,
383347
timeout=_test_timeout)
384348
except subprocess.CalledProcessError as ex:
385-
# if segfauldt, carry on. We do try to warn the user they
349+
# if segfault, carry on. We do try to warn the user they
386350
# are doing something that we do not expect to work
387351
if ex.returncode == -11:
388352
continue
@@ -397,7 +361,7 @@ def test_webagg():
397361
proc = subprocess.Popen(
398362
[sys.executable, "-c",
399363
inspect.getsource(_test_interactive_impl)
400-
+ "\n_test_interactive_impl()"],
364+
+ "\n_test_interactive_impl()", "{}"],
401365
env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
402366
url = "http://{}:{}".format(
403367
mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"])
@@ -419,37 +383,33 @@ def test_webagg():
419383
assert proc.wait(timeout=_test_timeout) == 0
420384

421385

386+
def _lazy_headless():
387+
import os
388+
import sys
389+
390+
# make it look headless
391+
os.environ.pop('DISPLAY', None)
392+
os.environ.pop('WAYLAND_DISPLAY', None)
393+
394+
# we should fast-track to Agg
395+
import matplotlib.pyplot as plt
396+
plt.get_backend() == 'agg'
397+
assert 'PyQt5' not in sys.modules
398+
399+
# make sure we really have pyqt installed
400+
import PyQt5 # noqa
401+
assert 'PyQt5' in sys.modules
402+
403+
# try to switch and make sure we fail with ImportError
404+
try:
405+
plt.switch_backend('qt5agg')
406+
except ImportError:
407+
...
408+
else:
409+
sys.exit(1)
410+
411+
422412
@pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
423413
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
424414
def test_lazy_linux_headless():
425-
test_script = """
426-
import os
427-
import sys
428-
429-
# make it look headless
430-
os.environ.pop('DISPLAY', None)
431-
os.environ.pop('WAYLAND_DISPLAY', None)
432-
433-
# we should fast-track to Agg
434-
import matplotlib.pyplot as plt
435-
plt.get_backend() == 'agg'
436-
assert 'PyQt5' not in sys.modules
437-
438-
# make sure we really have pyqt installed
439-
import PyQt5
440-
assert 'PyQt5' in sys.modules
441-
442-
# try to switch and make sure we fail with ImportError
443-
try:
444-
plt.switch_backend('qt5agg')
445-
except ImportError:
446-
...
447-
else:
448-
sys.exit(1)
449-
450-
"""
451-
proc = subprocess.run([sys.executable, "-c", test_script],
452-
env={**os.environ, "MPLBACKEND": ""})
453-
if proc.returncode:
454-
pytest.fail("The subprocess returned with non-zero exit status "
455-
f"{proc.returncode}.")
415+
proc = _run_helper(_lazy_headless, timeout=_test_timeout, MPLBACKEND="")

0 commit comments

Comments
 (0)