| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791 |
- import functools
- import importlib
- import importlib.util
- import inspect
- import json
- import os
- import platform
- import signal
- import subprocess
- import sys
- import tempfile
- import time
- import urllib.request
- from PIL import Image
- import pytest
- import matplotlib as mpl
- from matplotlib import _c_internal_utils
- from matplotlib.backend_tools import ToolToggleBase
- from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment
- class _WaitForStringPopen(subprocess.Popen):
- """
- A Popen that passes flags that allow triggering KeyboardInterrupt.
- """
- def __init__(self, *args, **kwargs):
- if sys.platform == 'win32':
- kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE
- super().__init__(
- *args, **kwargs,
- # Force Agg so that each test can switch to its desired backend.
- env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"},
- stdout=subprocess.PIPE, universal_newlines=True)
- def wait_for(self, terminator):
- """Read until the terminator is reached."""
- buf = ''
- while True:
- c = self.stdout.read(1)
- if not c:
- raise RuntimeError(
- f'Subprocess died before emitting expected {terminator!r}')
- buf += c
- if buf.endswith(terminator):
- return
- # Minimal smoke-testing of the backends for which the dependencies are
- # PyPI-installable on CI. They are not available for all tested Python
- # versions so we don't fail on missing backends.
- @functools.lru_cache
- def _get_available_interactive_backends():
- _is_linux_and_display_invalid = (sys.platform == "linux" and
- not _c_internal_utils.display_is_valid())
- _is_linux_and_xdisplay_invalid = (sys.platform == "linux" and
- not _c_internal_utils.xdisplay_is_valid())
- envs = []
- for deps, env in [
- *[([qt_api],
- {"MPLBACKEND": "qtagg", "QT_API": qt_api})
- for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
- *[([qt_api, "cairocffi"],
- {"MPLBACKEND": "qtcairo", "QT_API": qt_api})
- for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]],
- *[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"})
- for version in [3, 4] for renderer in ["agg", "cairo"]],
- (["tkinter"], {"MPLBACKEND": "tkagg"}),
- (["wx"], {"MPLBACKEND": "wx"}),
- (["wx"], {"MPLBACKEND": "wxagg"}),
- (["matplotlib.backends._macosx"], {"MPLBACKEND": "macosx"}),
- ]:
- reason = None
- missing = [dep for dep in deps if not importlib.util.find_spec(dep)]
- if missing:
- reason = "{} cannot be imported".format(", ".join(missing))
- elif _is_linux_and_xdisplay_invalid and (
- env["MPLBACKEND"] == "tkagg"
- # Remove when https://github.com/wxWidgets/Phoenix/pull/2638 is out.
- or env["MPLBACKEND"].startswith("wx")):
- reason = "$DISPLAY is unset"
- elif _is_linux_and_display_invalid:
- reason = "$DISPLAY and $WAYLAND_DISPLAY are unset"
- elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'):
- reason = "macosx backend fails on Azure"
- elif env["MPLBACKEND"].startswith('gtk'):
- try:
- import gi # type: ignore[import]
- except ImportError:
- # Though we check that `gi` exists above, it is possible that its
- # C-level dependencies are not available, and then it still raises an
- # `ImportError`, so guard against that.
- available_gtk_versions = []
- else:
- gi_repo = gi.Repository.get_default()
- available_gtk_versions = gi_repo.enumerate_versions('Gtk')
- version = env["MPLBACKEND"][3]
- if f'{version}.0' not in available_gtk_versions:
- reason = "no usable GTK bindings"
- marks = []
- if reason:
- marks.append(pytest.mark.skip(reason=f"Skipping {env} because {reason}"))
- elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin':
- # ignore on macosx because that's currently broken (github #16849)
- marks.append(pytest.mark.xfail(reason='github #16849'))
- elif (env['MPLBACKEND'] == 'tkagg' and
- ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
- sys.platform == 'darwin' and
- sys.version_info[:2] < (3, 11)
- ):
- marks.append( # https://github.com/actions/setup-python/issues/649
- pytest.mark.xfail(reason='Tk version mismatch on Azure macOS CI'))
- envs.append(({**env, 'BACKEND_DEPS': ','.join(deps)}, marks))
- return envs
- def _get_testable_interactive_backends():
- # We re-create this because some of the callers below might modify the markers.
- return [pytest.param({**env}, marks=[*marks],
- id='-'.join(f'{k}={v}' for k, v in env.items()))
- for env, marks in _get_available_interactive_backends()]
- # Reasonable safe values for slower CI/Remote and local architectures.
- _test_timeout = 120 if is_ci_environment() else 20
- def _test_toolbar_button_la_mode_icon(fig):
- # test a toolbar button icon using an image in LA mode (GH issue 25174)
- # create an icon in LA mode
- with tempfile.TemporaryDirectory() as tempdir:
- img = Image.new("LA", (26, 26))
- tmp_img_path = os.path.join(tempdir, "test_la_icon.png")
- img.save(tmp_img_path)
- class CustomTool(ToolToggleBase):
- image = tmp_img_path
- description = "" # gtk3 backend does not allow None
- toolmanager = fig.canvas.manager.toolmanager
- toolbar = fig.canvas.manager.toolbar
- toolmanager.add_tool("test", CustomTool)
- toolbar.add_tool("test", "group")
- # The source of this function gets extracted and run in another process, so it
- # must be fully self-contained.
- # Using a timer not only allows testing of timers (on other backends), but is
- # also necessary on gtk3 and wx, where directly processing a KeyEvent() for "q"
- # from draw_event causes breakage as the canvas widget gets deleted too early.
- def _test_interactive_impl():
- import importlib.util
- import io
- import json
- import sys
- import pytest
- import matplotlib as mpl
- from matplotlib import pyplot as plt
- from matplotlib.backend_bases import KeyEvent
- mpl.rcParams.update({
- "webagg.open_in_browser": False,
- "webagg.port_retries": 1,
- })
- mpl.rcParams.update(json.loads(sys.argv[1]))
- backend = plt.rcParams["backend"].lower()
- if backend.endswith("agg") and not backend.startswith(("gtk", "web")):
- # Force interactive framework setup.
- fig = plt.figure()
- plt.close(fig)
- # Check that we cannot switch to a backend using another interactive
- # framework, but can switch to a backend using cairo instead of agg,
- # or a non-interactive backend. In the first case, we use tkagg as
- # the "other" interactive backend as it is (essentially) guaranteed
- # to be present. Moreover, don't test switching away from gtk3 (as
- # Gtk.main_level() is not set up at this point yet) and webagg (which
- # uses no interactive framework).
- if backend != "tkagg":
- with pytest.raises(ImportError):
- mpl.use("tkagg", force=True)
- def check_alt_backend(alt_backend):
- mpl.use(alt_backend, force=True)
- fig = plt.figure()
- assert (type(fig.canvas).__module__ ==
- f"matplotlib.backends.backend_{alt_backend}")
- plt.close("all")
- if importlib.util.find_spec("cairocffi"):
- check_alt_backend(backend[:-3] + "cairo")
- check_alt_backend("svg")
- mpl.use(backend, force=True)
- fig, ax = plt.subplots()
- assert type(fig.canvas).__module__ == f"matplotlib.backends.backend_{backend}"
- assert fig.canvas.manager.get_window_title() == "Figure 1"
- if mpl.rcParams["toolbar"] == "toolmanager":
- # test toolbar button icon LA mode see GH issue 25174
- _test_toolbar_button_la_mode_icon(fig)
- ax.plot([0, 1], [2, 3])
- if fig.canvas.toolbar: # i.e toolbar2.
- fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2)
- timer = fig.canvas.new_timer(1.) # Test that floats are cast to int.
- timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process)
- # Trigger quitting upon draw.
- fig.canvas.mpl_connect("draw_event", lambda event: timer.start())
- fig.canvas.mpl_connect("close_event", print)
- result = io.BytesIO()
- fig.savefig(result, format='png')
- plt.show()
- # Ensure that the window is really closed.
- plt.pause(0.5)
- # Test that saving works after interactive window is closed, but the figure
- # is not deleted.
- result_after = io.BytesIO()
- fig.savefig(result_after, format='png')
- assert result.getvalue() == result_after.getvalue()
- @pytest.mark.parametrize("env", _get_testable_interactive_backends())
- @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"])
- @pytest.mark.flaky(reruns=3)
- def test_interactive_backend(env, toolbar):
- if env["MPLBACKEND"] == "macosx":
- if toolbar == "toolmanager":
- pytest.skip("toolmanager is not implemented for macosx.")
- if env["MPLBACKEND"] == "wx":
- pytest.skip("wx backend is deprecated; tests failed on appveyor")
- if env["MPLBACKEND"] == "wxagg" and toolbar == "toolmanager":
- pytest.skip("Temporarily deactivated: show() changes figure height "
- "and thus fails the test")
- try:
- proc = _run_helper(
- _test_interactive_impl,
- json.dumps({"toolbar": toolbar}),
- timeout=_test_timeout,
- extra_env=env,
- )
- except subprocess.CalledProcessError as err:
- pytest.fail(
- "Subprocess failed to test intended behavior\n"
- + str(err.stderr))
- assert proc.stdout.count("CloseEvent") == 1
- def _test_thread_impl():
- from concurrent.futures import ThreadPoolExecutor
- import matplotlib as mpl
- from matplotlib import pyplot as plt
- mpl.rcParams.update({
- "webagg.open_in_browser": False,
- "webagg.port_retries": 1,
- })
- # Test artist creation and drawing does not crash from thread
- # No other guarantees!
- fig, ax = plt.subplots()
- # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg
- plt.pause(0.5)
- future = ThreadPoolExecutor().submit(ax.plot, [1, 3, 6])
- future.result() # Joins the thread; rethrows any exception.
- fig.canvas.mpl_connect("close_event", print)
- future = ThreadPoolExecutor().submit(fig.canvas.draw)
- plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176)
- future.result() # Joins the thread; rethrows any exception.
- plt.close() # backend is responsible for flushing any events here
- if plt.rcParams["backend"].lower().startswith("wx"):
- # TODO: debug why WX needs this only on py >= 3.8
- fig.canvas.flush_events()
- _thread_safe_backends = _get_testable_interactive_backends()
- # Known unsafe backends. Remove the xfails if they start to pass!
- for param in _thread_safe_backends:
- backend = param.values[0]["MPLBACKEND"]
- if "cairo" in backend:
- # Cairo backends save a cairo_t on the graphics context, and sharing
- # these is not threadsafe.
- param.marks.append(
- pytest.mark.xfail(raises=subprocess.CalledProcessError))
- elif backend == "wx":
- param.marks.append(
- pytest.mark.xfail(raises=subprocess.CalledProcessError))
- elif backend == "macosx":
- from packaging.version import parse
- mac_ver = platform.mac_ver()[0]
- # Note, macOS Big Sur is both 11 and 10.16, depending on SDK that
- # Python was compiled against.
- if mac_ver and parse(mac_ver) < parse('10.16'):
- param.marks.append(
- pytest.mark.xfail(raises=subprocess.TimeoutExpired,
- strict=True))
- elif param.values[0].get("QT_API") == "PySide2":
- param.marks.append(
- pytest.mark.xfail(raises=subprocess.CalledProcessError))
- elif backend == "tkagg" and platform.python_implementation() != 'CPython':
- param.marks.append(
- pytest.mark.xfail(
- reason='PyPy does not support Tkinter threading: '
- 'https://foss.heptapod.net/pypy/pypy/-/issues/1929',
- strict=True))
- elif (backend == 'tkagg' and
- ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
- sys.platform == 'darwin' and sys.version_info[:2] < (3, 11)):
- param.marks.append( # https://github.com/actions/setup-python/issues/649
- pytest.mark.xfail('Tk version mismatch on Azure macOS CI'))
- @pytest.mark.parametrize("env", _thread_safe_backends)
- @pytest.mark.flaky(reruns=3)
- def test_interactive_thread_safety(env):
- proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env)
- assert proc.stdout.count("CloseEvent") == 1
- def _impl_test_lazy_auto_backend_selection():
- import matplotlib
- import matplotlib.pyplot as plt
- # just importing pyplot should not be enough to trigger resolution
- bk = matplotlib.rcParams._get('backend')
- assert not isinstance(bk, str)
- assert plt._backend_mod is None
- # but actually plotting should
- plt.plot(5)
- assert plt._backend_mod is not None
- bk = matplotlib.rcParams._get('backend')
- assert isinstance(bk, str)
- def test_lazy_auto_backend_selection():
- _run_helper(_impl_test_lazy_auto_backend_selection,
- timeout=_test_timeout)
- def _implqt5agg():
- import matplotlib.backends.backend_qt5agg # noqa
- import sys
- assert 'PyQt6' not in sys.modules
- assert 'pyside6' not in sys.modules
- assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
- def _implcairo():
- import matplotlib.backends.backend_qt5cairo # noqa
- import sys
- assert 'PyQt6' not in sys.modules
- assert 'pyside6' not in sys.modules
- assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
- def _implcore():
- import matplotlib.backends.backend_qt5 # noqa
- import sys
- assert 'PyQt6' not in sys.modules
- assert 'pyside6' not in sys.modules
- assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules
- def test_qt5backends_uses_qt5():
- qt5_bindings = [
- dep for dep in ['PyQt5', 'pyside2']
- if importlib.util.find_spec(dep) is not None
- ]
- qt6_bindings = [
- dep for dep in ['PyQt6', 'pyside6']
- if importlib.util.find_spec(dep) is not None
- ]
- if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
- pytest.skip('need both QT6 and QT5 bindings')
- _run_helper(_implqt5agg, timeout=_test_timeout)
- if importlib.util.find_spec('pycairo') is not None:
- _run_helper(_implcairo, timeout=_test_timeout)
- _run_helper(_implcore, timeout=_test_timeout)
- def _impl_missing():
- import sys
- # Simulate uninstalled
- sys.modules["PyQt6"] = None
- sys.modules["PyQt5"] = None
- sys.modules["PySide2"] = None
- sys.modules["PySide6"] = None
- import matplotlib.pyplot as plt
- with pytest.raises(ImportError, match="Failed to import any of the following Qt"):
- plt.switch_backend("qtagg")
- # Specifically ensure that Pyside6/Pyqt6 are not in the error message for qt5agg
- with pytest.raises(ImportError, match="^(?:(?!(PySide6|PyQt6)).)*$"):
- plt.switch_backend("qt5agg")
- def test_qt_missing():
- _run_helper(_impl_missing, timeout=_test_timeout)
- def _impl_test_cross_Qt_imports():
- import importlib
- import sys
- import warnings
- _, host_binding, mpl_binding = sys.argv
- # import the mpl binding. This will force us to use that binding
- importlib.import_module(f'{mpl_binding}.QtCore')
- mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets')
- import matplotlib.backends.backend_qt
- host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets')
- host_app = host_qwidgets.QApplication(["mpl testing"])
- warnings.filterwarnings("error", message=r".*Mixing Qt major.*",
- category=UserWarning)
- matplotlib.backends.backend_qt._create_qApp()
- def qt5_and_qt6_pairs():
- qt5_bindings = [
- dep for dep in ['PyQt5', 'PySide2']
- if importlib.util.find_spec(dep) is not None
- ]
- qt6_bindings = [
- dep for dep in ['PyQt6', 'PySide6']
- if importlib.util.find_spec(dep) is not None
- ]
- if len(qt5_bindings) == 0 or len(qt6_bindings) == 0:
- yield pytest.param(None, None,
- marks=[pytest.mark.skip('need both QT6 and QT5 bindings')])
- return
- for qt5 in qt5_bindings:
- for qt6 in qt6_bindings:
- yield from ([qt5, qt6], [qt6, qt5])
- @pytest.mark.skipif(
- sys.platform == "linux" and not _c_internal_utils.display_is_valid(),
- reason="$DISPLAY and $WAYLAND_DISPLAY are unset")
- @pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()])
- def test_cross_Qt_imports(host, mpl):
- try:
- proc = _run_helper(_impl_test_cross_Qt_imports, host, mpl,
- timeout=_test_timeout)
- except subprocess.CalledProcessError as ex:
- # We do try to warn the user they are doing something that we do not
- # expect to work, so we're going to ignore if the subprocess crashes or
- # is killed, and just check that the warning is printed.
- stderr = ex.stderr
- else:
- stderr = proc.stderr
- assert "Mixing Qt major versions may not work as expected." in stderr
- @pytest.mark.skipif('TF_BUILD' in os.environ,
- reason="this test fails an azure for unknown reasons")
- @pytest.mark.skipif(sys.platform == "win32", reason="Cannot send SIGINT on Windows.")
- def test_webagg():
- pytest.importorskip("tornado")
- proc = subprocess.Popen(
- [sys.executable, "-c",
- inspect.getsource(_test_interactive_impl)
- + "\n_test_interactive_impl()", "{}"],
- env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"})
- url = f'http://{mpl.rcParams["webagg.address"]}:{mpl.rcParams["webagg.port"]}'
- timeout = time.perf_counter() + _test_timeout
- try:
- while True:
- try:
- retcode = proc.poll()
- # check that the subprocess for the server is not dead
- assert retcode is None
- conn = urllib.request.urlopen(url)
- break
- except urllib.error.URLError:
- if time.perf_counter() > timeout:
- pytest.fail("Failed to connect to the webagg server.")
- else:
- continue
- conn.close()
- proc.send_signal(signal.SIGINT)
- assert proc.wait(timeout=_test_timeout) == 0
- finally:
- if proc.poll() is None:
- proc.kill()
- def _lazy_headless():
- import os
- import sys
- backend, deps = sys.argv[1:]
- deps = deps.split(',')
- # make it look headless
- os.environ.pop('DISPLAY', None)
- os.environ.pop('WAYLAND_DISPLAY', None)
- for dep in deps:
- assert dep not in sys.modules
- # we should fast-track to Agg
- import matplotlib.pyplot as plt
- assert plt.get_backend() == 'agg'
- for dep in deps:
- assert dep not in sys.modules
- # make sure we really have dependencies installed
- for dep in deps:
- importlib.import_module(dep)
- assert dep in sys.modules
- # try to switch and make sure we fail with ImportError
- try:
- plt.switch_backend(backend)
- except ImportError:
- pass
- else:
- sys.exit(1)
- @pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test")
- @pytest.mark.parametrize("env", _get_testable_interactive_backends())
- def test_lazy_linux_headless(env):
- proc = _run_helper(
- _lazy_headless,
- env.pop('MPLBACKEND'), env.pop("BACKEND_DEPS"),
- timeout=_test_timeout,
- extra_env={**env, 'DISPLAY': '', 'WAYLAND_DISPLAY': ''}
- )
- def _test_number_of_draws_script():
- import matplotlib.pyplot as plt
- fig, ax = plt.subplots()
- # animated=True tells matplotlib to only draw the artist when we
- # explicitly request it
- ln, = ax.plot([0, 1], [1, 2], animated=True)
- # make sure the window is raised, but the script keeps going
- plt.show(block=False)
- plt.pause(0.3)
- # Connect to draw_event to count the occurrences
- fig.canvas.mpl_connect('draw_event', print)
- # get copy of entire figure (everything inside fig.bbox)
- # sans animated artist
- bg = fig.canvas.copy_from_bbox(fig.bbox)
- # draw the animated artist, this uses a cached renderer
- ax.draw_artist(ln)
- # show the result to the screen
- fig.canvas.blit(fig.bbox)
- for j in range(10):
- # reset the background back in the canvas state, screen unchanged
- fig.canvas.restore_region(bg)
- # Create a **new** artist here, this is poor usage of blitting
- # but good for testing to make sure that this doesn't create
- # excessive draws
- ln, = ax.plot([0, 1], [1, 2])
- # render the artist, updating the canvas state, but not the screen
- ax.draw_artist(ln)
- # copy the image to the GUI state, but screen might not changed yet
- fig.canvas.blit(fig.bbox)
- # flush any pending GUI events, re-painting the screen if needed
- fig.canvas.flush_events()
- # Let the event loop process everything before leaving
- plt.pause(0.1)
- _blit_backends = _get_testable_interactive_backends()
- for param in _blit_backends:
- backend = param.values[0]["MPLBACKEND"]
- if backend == "gtk3cairo":
- # copy_from_bbox only works when rendering to an ImageSurface
- param.marks.append(
- pytest.mark.skip("gtk3cairo does not support blitting"))
- elif backend == "gtk4cairo":
- # copy_from_bbox only works when rendering to an ImageSurface
- param.marks.append(
- pytest.mark.skip("gtk4cairo does not support blitting"))
- elif backend == "wx":
- param.marks.append(
- pytest.mark.skip("wx does not support blitting"))
- elif (backend == 'tkagg' and
- ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and
- sys.platform == 'darwin' and
- sys.version_info[:2] < (3, 11)
- ):
- param.marks.append( # https://github.com/actions/setup-python/issues/649
- pytest.mark.xfail('Tk version mismatch on Azure macOS CI')
- )
- @pytest.mark.parametrize("env", _blit_backends)
- # subprocesses can struggle to get the display, so rerun a few times
- @pytest.mark.flaky(reruns=4)
- def test_blitting_events(env):
- proc = _run_helper(
- _test_number_of_draws_script, timeout=_test_timeout, extra_env=env)
- # Count the number of draw_events we got. We could count some initial
- # canvas draws (which vary in number by backend), but the critical
- # check here is that it isn't 10 draws, which would be called if
- # blitting is not properly implemented
- ndraws = proc.stdout.count("DrawEvent")
- assert 0 < ndraws < 5
- def _impl_test_interactive_timers():
- # A timer with <1 millisecond gets converted to int and therefore 0
- # milliseconds, which the mac framework interprets as singleshot.
- # We only want singleshot if we specify that ourselves, otherwise we want
- # a repeating timer
- from unittest.mock import Mock
- import matplotlib.pyplot as plt
- pause_time = 0.5
- fig = plt.figure()
- plt.pause(pause_time)
- timer = fig.canvas.new_timer(0.1)
- mock = Mock()
- timer.add_callback(mock)
- timer.start()
- plt.pause(pause_time)
- timer.stop()
- assert mock.call_count > 1
- # Now turn it into a single shot timer and verify only one gets triggered
- mock.call_count = 0
- timer.single_shot = True
- timer.start()
- plt.pause(pause_time)
- assert mock.call_count == 1
- # Make sure we can start the timer a second time
- timer.start()
- plt.pause(pause_time)
- assert mock.call_count == 2
- plt.close("all")
- @pytest.mark.parametrize("env", _get_testable_interactive_backends())
- def test_interactive_timers(env):
- if env["MPLBACKEND"] == "gtk3cairo" and os.getenv("CI"):
- pytest.skip("gtk3cairo timers do not work in remote CI")
- if env["MPLBACKEND"] == "wx":
- pytest.skip("wx backend is deprecated; tests failed on appveyor")
- _run_helper(_impl_test_interactive_timers,
- timeout=_test_timeout, extra_env=env)
- def _test_sigint_impl(backend, target_name, kwargs):
- import sys
- import matplotlib.pyplot as plt
- import os
- import threading
- plt.switch_backend(backend)
- def interrupter():
- if sys.platform == 'win32':
- import win32api
- win32api.GenerateConsoleCtrlEvent(0, 0)
- else:
- import signal
- os.kill(os.getpid(), signal.SIGINT)
- target = getattr(plt, target_name)
- timer = threading.Timer(1, interrupter)
- fig = plt.figure()
- fig.canvas.mpl_connect(
- 'draw_event',
- lambda *args: print('DRAW', flush=True)
- )
- fig.canvas.mpl_connect(
- 'draw_event',
- lambda *args: timer.start()
- )
- try:
- target(**kwargs)
- except KeyboardInterrupt:
- print('SUCCESS', flush=True)
- @pytest.mark.parametrize("env", _get_testable_interactive_backends())
- @pytest.mark.parametrize("target, kwargs", [
- ('show', {'block': True}),
- ('pause', {'interval': 10})
- ])
- def test_sigint(env, target, kwargs):
- backend = env.get("MPLBACKEND")
- if not backend.startswith(("qt", "macosx")):
- pytest.skip("SIGINT currently only tested on qt and macosx")
- proc = _WaitForStringPopen(
- [sys.executable, "-c",
- inspect.getsource(_test_sigint_impl) +
- f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"])
- try:
- proc.wait_for('DRAW')
- stdout, _ = proc.communicate(timeout=_test_timeout)
- except Exception:
- proc.kill()
- stdout, _ = proc.communicate()
- raise
- assert 'SUCCESS' in stdout
- def _test_other_signal_before_sigint_impl(backend, target_name, kwargs):
- import signal
- import matplotlib.pyplot as plt
- plt.switch_backend(backend)
- target = getattr(plt, target_name)
- fig = plt.figure()
- fig.canvas.mpl_connect('draw_event', lambda *args: print('DRAW', flush=True))
- timer = fig.canvas.new_timer(interval=1)
- timer.single_shot = True
- timer.add_callback(print, 'SIGUSR1', flush=True)
- def custom_signal_handler(signum, frame):
- timer.start()
- signal.signal(signal.SIGUSR1, custom_signal_handler)
- try:
- target(**kwargs)
- except KeyboardInterrupt:
- print('SUCCESS', flush=True)
- @pytest.mark.skipif(sys.platform == 'win32',
- reason='No other signal available to send on Windows')
- @pytest.mark.parametrize("env", _get_testable_interactive_backends())
- @pytest.mark.parametrize("target, kwargs", [
- ('show', {'block': True}),
- ('pause', {'interval': 10})
- ])
- def test_other_signal_before_sigint(env, target, kwargs, request):
- backend = env.get("MPLBACKEND")
- if not backend.startswith(("qt", "macosx")):
- pytest.skip("SIGINT currently only tested on qt and macosx")
- if backend == "macosx":
- request.node.add_marker(pytest.mark.xfail(reason="macosx backend is buggy"))
- if sys.platform == "darwin" and target == "show":
- # We've not previously had these toolkits installed on CI, and so were never
- # aware that this was crashing. However, we've had little luck reproducing it
- # locally, so mark it xfail for now. For more information, see
- # https://github.com/matplotlib/matplotlib/issues/27984
- request.node.add_marker(
- pytest.mark.xfail(reason="Qt backend is buggy on macOS"))
- proc = _WaitForStringPopen(
- [sys.executable, "-c",
- inspect.getsource(_test_other_signal_before_sigint_impl) +
- "\n_test_other_signal_before_sigint_impl("
- f"{backend!r}, {target!r}, {kwargs!r})"])
- try:
- proc.wait_for('DRAW')
- os.kill(proc.pid, signal.SIGUSR1)
- proc.wait_for('SIGUSR1')
- os.kill(proc.pid, signal.SIGINT)
- stdout, _ = proc.communicate(timeout=_test_timeout)
- except Exception:
- proc.kill()
- stdout, _ = proc.communicate()
- raise
- print(stdout)
- assert 'SUCCESS' in stdout
|