import importlib import importlib.util import os import signal import subprocess import sys import time import urllib.request import pytest import matplotlib as mpl # Minimal smoke-testing of the backends for which the dependencies are # PyPI-installable on Travis. They are not available for all tested Python # versions so we don't fail on missing backends. def _get_testable_interactive_backends(): backends = [] for deps, backend in [ (["cairo", "gi"], "gtk3agg"), (["cairo", "gi"], "gtk3cairo"), (["PyQt5"], "qt5agg"), (["PyQt5", "cairocffi"], "qt5cairo"), (["tkinter"], "tkagg"), (["wx"], "wx"), (["wx"], "wxagg"), ]: reason = None if not os.environ.get("DISPLAY"): reason = "No $DISPLAY" elif any(importlib.util.find_spec(dep) is None for dep in deps): reason = "Missing dependency" if reason: backend = pytest.param( backend, marks=pytest.mark.skip(reason=reason)) backends.append(backend) return backends # Using a timer not only allows testing of timers (on other backends), but is # also necessary on gtk3 and wx, where a direct call to key_press_event("q") # from draw_event causes breakage due to the canvas widget being deleted too # early. Also, gtk3 redefines key_press_event with a different signature, so # we directly invoke it from the superclass instead. _test_script = """\ import importlib import importlib.util import sys from unittest import TestCase import matplotlib as mpl from matplotlib import pyplot as plt, rcParams from matplotlib.backend_bases import FigureCanvasBase rcParams.update({ "webagg.open_in_browser": False, "webagg.port_retries": 1, }) backend = plt.rcParams["backend"].lower() assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises if backend.endswith("agg") and not backend.startswith(("gtk3", "web")): # Force interactive framework setup. plt.figure() # 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 assert_raises(ImportError): mpl.use("tkagg", force=True) def check_alt_backend(alt_backend): mpl.use(alt_backend, force=True) fig = plt.figure() assert_equal( type(fig.canvas).__module__, "matplotlib.backends.backend_{}".format(alt_backend)) 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_equal( type(fig.canvas).__module__, "matplotlib.backends.backend_{}".format(backend)) ax.plot([0, 1], [2, 3]) timer = fig.canvas.new_timer(1) timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q") # Trigger quitting upon draw. fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) plt.show() """ _test_timeout = 10 # Empirically, 1s is not enough on Travis. @pytest.mark.parametrize("backend", _get_testable_interactive_backends()) @pytest.mark.flaky(reruns=3) def test_interactive_backend(backend): proc = subprocess.run([sys.executable, "-c", _test_script], env={**os.environ, "MPLBACKEND": backend}, timeout=_test_timeout) if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.") @pytest.mark.skipif('SYSTEM_TEAMFOUNDATIONCOLLECTIONURI' in os.environ, reason="this test fails an azure for unknown reasons") @pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.") def test_webagg(): pytest.importorskip("tornado") proc = subprocess.Popen([sys.executable, "-c", _test_script], env={**os.environ, "MPLBACKEND": "webagg"}) url = "http://{}:{}".format( mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"]) timeout = time.perf_counter() + _test_timeout 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