Skip to content

Commit 6f61a68

Browse files
authored
test that matplotlib event loop integration is responsive (#1463)
1 parent 8446e02 commit 6f61a68

File tree

6 files changed

+128
-2
lines changed

6 files changed

+128
-2
lines changed

ipykernel/eventloops.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def loop_gtk3_exit(kernel):
373373
kernel._gtk.stop()
374374

375375

376-
@register_integration("osx")
376+
@register_integration("osx", "macosx")
377377
def loop_cocoa(kernel):
378378
"""Start the kernel, coordinating with the Cocoa CFRunLoop event loop
379379
via the matplotlib MacOSX backend.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ docs = [
5757
test = [
5858
"pytest>=7.0,<9",
5959
"pytest-cov",
60+
# 'pytest-xvfb; platform_system == "Linux"',
6061
"flaky",
6162
"ipyparallel",
6263
"pre-commit",

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import no_type_check
55
from unittest.mock import MagicMock
66

7+
import pytest
78
import pytest_asyncio
89
import zmq
910
from jupyter_client.session import Session
@@ -20,6 +21,7 @@
2021
# Windows
2122
resource = None # type:ignore
2223

24+
from .utils import new_kernel
2325

2426
# Handle resource limit
2527
# Ensure a minimal soft limit of DEFAULT_SOFT if the current hard limit is at least that much.
@@ -158,3 +160,9 @@ def ipkernel():
158160
yield kernel
159161
kernel.destroy()
160162
ZMQInteractiveShell.clear_instance()
163+
164+
165+
@pytest.fixture
166+
def kc():
167+
with new_kernel() as kc:
168+
yield kc

tests/test_eventloop.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def _setup_env():
5656

5757
windows_skip = pytest.mark.skipif(os.name == "nt", reason="causing failures on windows")
5858

59+
# some part of this module seems to hang when run with xvfb
60+
pytestmark = pytest.mark.skipif(
61+
sys.platform == "linux" and bool(os.getenv("CI")), reason="hangs on linux CI"
62+
)
63+
5964

6065
@windows_skip
6166
@pytest.mark.skipif(sys.platform == "darwin", reason="hangs on macos")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import os
2+
import sys
3+
import time
4+
5+
import pytest
6+
from jupyter_client.blocking.client import BlockingKernelClient
7+
8+
from .test_eventloop import qt_guis_avail
9+
from .utils import assemble_output
10+
11+
# these tests don't seem to work with xvfb yet
12+
# these tests seem to be a problem on CI in general
13+
pytestmark = pytest.mark.skipif(
14+
bool(os.getenv("CI")),
15+
reason="tests not working yet reliably on CI",
16+
)
17+
18+
guis = []
19+
if not sys.platform.startswith("tk"):
20+
guis.append("tk")
21+
if qt_guis_avail:
22+
guis.append("qt")
23+
if sys.platform == "darwin":
24+
guis.append("osx")
25+
26+
backends = {
27+
"tk": "tkagg",
28+
"qt": "qtagg",
29+
"osx": "macosx",
30+
}
31+
32+
33+
def execute(
34+
kc: BlockingKernelClient,
35+
code: str,
36+
timeout=120,
37+
):
38+
msg_id = kc.execute(code)
39+
stdout, stderr = assemble_output(kc.get_iopub_msg, timeout=timeout, parent_msg_id=msg_id)
40+
assert not stderr.strip()
41+
return stdout.strip(), stderr.strip()
42+
43+
44+
@pytest.mark.parametrize("gui", guis)
45+
@pytest.mark.timeout(300)
46+
def test_matplotlib_gui(kc, gui):
47+
"""Make sure matplotlib activates and its eventloop runs while the kernel is also responsive"""
48+
pytest.importorskip("matplotlib", reason="this test requires matplotlib")
49+
stdout, stderr = execute(kc, f"%matplotlib {gui}")
50+
assert not stderr
51+
# debug: show output from invoking the matplotlib magic
52+
print(stdout)
53+
execute(
54+
kc,
55+
"""
56+
from concurrent.futures import Future
57+
import matplotlib as mpl
58+
import matplotlib.pyplot as plt
59+
""",
60+
)
61+
stdout, _ = execute(kc, "print(mpl.get_backend())")
62+
assert stdout == backends[gui]
63+
execute(
64+
kc,
65+
"""
66+
fig, ax = plt.subplots()
67+
timer = fig.canvas.new_timer(interval=10)
68+
f = Future()
69+
70+
call_count = 0
71+
def add_call():
72+
global call_count
73+
call_count += 1
74+
if not f.done():
75+
f.set_result(None)
76+
77+
timer.add_callback(add_call)
78+
timer.start()
79+
""",
80+
)
81+
# wait for the first call (up to 60 seconds)
82+
deadline = time.monotonic() + 60
83+
done = False
84+
while time.monotonic() <= deadline:
85+
stdout, _ = execute(kc, "print(f.done())")
86+
if stdout.strip() == "True":
87+
done = True
88+
break
89+
if stdout == "False":
90+
time.sleep(0.1)
91+
else:
92+
pytest.fail(f"Unexpected output {stdout}")
93+
if not done:
94+
pytest.fail("future never finished...")
95+
96+
time.sleep(0.25)
97+
stdout, _ = execute(kc, "print(call_count)")
98+
call_count = int(stdout)
99+
assert call_count > 0
100+
time.sleep(0.25)
101+
stdout, _ = execute(kc, "timer.stop()\nprint(call_count)")
102+
call_count_2 = int(stdout)
103+
assert call_count_2 > call_count
104+
stdout, _ = execute(kc, "print(call_count)")
105+
call_count_3 = int(stdout)
106+
assert call_count_3 <= call_count_2 + 5

tests/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def new_kernel(argv=None):
168168
return manager.run_kernel(**kwargs)
169169

170170

171-
def assemble_output(get_msg, timeout=1, parent_msg_id: str | None = None):
171+
def assemble_output(get_msg, timeout=1, parent_msg_id: str | None = None, raise_error=True):
172172
"""assemble stdout/err from an execution"""
173173
stdout = ""
174174
stderr = ""
@@ -191,6 +191,12 @@ def assemble_output(get_msg, timeout=1, parent_msg_id: str | None = None):
191191
stderr += content["text"]
192192
else:
193193
raise KeyError("bad stream: %r" % content["name"])
194+
elif raise_error and msg["msg_type"] == "error":
195+
tb = "\n".join(msg["content"]["traceback"])
196+
msg = f"Execution failed with:\n{tb}"
197+
if stderr:
198+
msg = f"{msg}\nstderr:\n{stderr}"
199+
raise RuntimeError(msg)
194200
else:
195201
# other output, ignored
196202
pass

0 commit comments

Comments
 (0)