Skip to content

Commit cc1b87c

Browse files
authored
add a pytest fixture for capturing logging stream (#588)
* add a pytest fixture for capturing logging stream * Update jupyter_server/pytest_plugin.py
1 parent ba3dacf commit cc1b87c

File tree

5 files changed

+97
-36
lines changed

5 files changed

+97
-36
lines changed

jupyter_server/pytest_plugin.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright (c) Jupyter Development Team.
22
# Distributed under the terms of the Modified BSD License.
3+
import io
34
import json
5+
import logging
46
import os
57
import shutil
68
import sys
@@ -18,7 +20,6 @@
1820
from jupyter_server.serverapp import ServerApp
1921
from jupyter_server.services.contents.filemanager import FileContentsManager
2022
from jupyter_server.services.contents.largefilemanager import LargeFileManager
21-
from jupyter_server.utils import run_sync
2223
from jupyter_server.utils import url_path_join
2324

2425

@@ -180,6 +181,22 @@ def jp_nbconvert_templates(jp_data_dir):
180181
shutil.copytree(nbconvert_path, str(nbconvert_target))
181182

182183

184+
@pytest.fixture
185+
def jp_logging_stream():
186+
"""StringIO stream intended to be used by the core
187+
Jupyter ServerApp logger's default StreamHandler. This
188+
helps avoid collision with stdout which is hijacked
189+
by Pytest.
190+
"""
191+
logging_stream = io.StringIO()
192+
yield logging_stream
193+
output = logging_stream.getvalue()
194+
# If output exists, print it.
195+
if output:
196+
print(output)
197+
return output
198+
199+
183200
@pytest.fixture(scope="function")
184201
def jp_configurable_serverapp(
185202
jp_nbconvert_templates, # this fixture must preceed jp_environ
@@ -191,6 +208,7 @@ def jp_configurable_serverapp(
191208
tmp_path,
192209
jp_root_dir,
193210
io_loop,
211+
jp_logging_stream,
194212
):
195213
"""Starts a Jupyter Server instance based on
196214
the provided configuration values.
@@ -240,6 +258,11 @@ def _configurable_serverapp(
240258
app.log.handlers = []
241259
# Initialize app without httpserver
242260
app.initialize(argv=argv, new_httpserver=False)
261+
# Reroute all logging StreamHandlers away from stdin/stdout since pytest hijacks
262+
# these streams and closes them at unfortunate times.
263+
stream_handlers = [h for h in app.log.handlers if isinstance(h, logging.StreamHandler)]
264+
for handler in stream_handlers:
265+
handler.setStream(jp_logging_stream)
243266
app.log.propagate = True
244267
app.log.handlers = []
245268
# Start app without ioloop
@@ -279,7 +302,8 @@ def jp_serverapp(jp_ensure_app_fixture, jp_server_config, jp_argv, jp_configurab
279302
"""Starts a Jupyter Server instance based on the established configuration values."""
280303
app = jp_configurable_serverapp(config=jp_server_config, argv=jp_argv)
281304
yield app
282-
run_sync(app._cleanup())
305+
app.remove_server_info_file()
306+
app.remove_browser_open_files()
283307

284308

285309
@pytest.fixture
@@ -440,3 +464,22 @@ def inner(nbpath):
440464
def jp_server_cleanup():
441465
yield
442466
ServerApp.clear_instance()
467+
468+
469+
@pytest.fixture
470+
def jp_cleanup_subprocesses(jp_serverapp):
471+
"""Clean up subprocesses started by a Jupyter Server, i.e. kernels and terminal."""
472+
473+
async def _():
474+
terminal_cleanup = jp_serverapp.web_app.settings["terminal_manager"].terminate_all
475+
kernel_cleanup = jp_serverapp.kernel_manager.shutdown_all
476+
if asyncio.iscoroutinefunction(terminal_cleanup):
477+
await terminal_cleanup()
478+
else:
479+
terminal_cleanup()
480+
if asyncio.iscoroutinefunction(kernel_cleanup):
481+
await kernel_cleanup()
482+
else:
483+
kernel_cleanup()
484+
485+
return _

jupyter_server/tests/services/kernels/test_api.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def test_no_kernels(jp_fetch):
2323
assert kernels == []
2424

2525

26-
async def test_default_kernels(jp_fetch, jp_base_url):
26+
async def test_default_kernels(jp_fetch, jp_base_url, jp_cleanup_subprocesses):
2727
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
2828
kernel = json.loads(r.body.decode())
2929
assert r.headers["location"] == url_path_join(jp_base_url, "/api/kernels/", kernel["id"])
@@ -35,9 +35,10 @@ async def test_default_kernels(jp_fetch, jp_base_url):
3535
["frame-ancestors 'self'", "report-uri " + report_uri, "default-src 'none'"]
3636
)
3737
assert r.headers["Content-Security-Policy"] == expected_csp
38+
await jp_cleanup_subprocesses()
3839

3940

40-
async def test_main_kernel_handler(jp_fetch, jp_base_url):
41+
async def test_main_kernel_handler(jp_fetch, jp_base_url, jp_cleanup_subprocesses):
4142
# Start the first kernel
4243
r = await jp_fetch(
4344
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -98,9 +99,10 @@ async def test_main_kernel_handler(jp_fetch, jp_base_url):
9899
)
99100
kernel3 = json.loads(r.body.decode())
100101
assert isinstance(kernel3, dict)
102+
await jp_cleanup_subprocesses()
101103

102104

103-
async def test_kernel_handler(jp_fetch):
105+
async def test_kernel_handler(jp_fetch, jp_cleanup_subprocesses):
104106
# Create a kernel
105107
r = await jp_fetch(
106108
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -138,10 +140,12 @@ async def test_kernel_handler(jp_fetch):
138140
with pytest.raises(tornado.httpclient.HTTPClientError) as e:
139141
await jp_fetch("api", "kernels", bad_id, method="DELETE")
140142
assert expected_http_error(e, 404, "Kernel does not exist: " + bad_id)
143+
await jp_cleanup_subprocesses()
141144

142145

143-
async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
144-
print("hello")
146+
async def test_connection(
147+
jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header, jp_cleanup_subprocesses
148+
):
145149
# Create kernel
146150
r = await jp_fetch(
147151
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
@@ -175,3 +179,4 @@ async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
175179
r = await jp_fetch("api", "kernels", kid, method="GET")
176180
model = json.loads(r.body.decode())
177181
assert model["connections"] == 0
182+
await jp_cleanup_subprocesses()

jupyter_server/tests/services/kernels/test_cull.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def jp_server_config():
3434
)
3535

3636

37-
async def test_culling(jp_fetch, jp_ws_fetch):
37+
async def test_culling(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
3838
r = await jp_fetch("api", "kernels", method="POST", allow_nonstandard_methods=True)
3939
kernel = json.loads(r.body.decode())
4040
kid = kernel["id"]
@@ -50,6 +50,7 @@ async def test_culling(jp_fetch, jp_ws_fetch):
5050
ws.close()
5151
culled = await get_cull_status(kid, jp_fetch) # not connected, should be culled
5252
assert culled
53+
await jp_cleanup_subprocesses()
5354

5455

5556
async def get_cull_status(kid, jp_fetch):

jupyter_server/tests/services/sessions/test_api.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def assert_session_equality(actual, expected):
151151
assert_kernel_equality(actual["kernel"], expected["kernel"])
152152

153153

154-
async def test_create(session_client, jp_base_url):
154+
async def test_create(session_client, jp_base_url, jp_cleanup_subprocesses):
155155
# Make sure no sessions exist.
156156
resp = await session_client.list()
157157
sessions = j(resp)
@@ -182,28 +182,31 @@ async def test_create(session_client, jp_base_url):
182182

183183
# Need to find a better solution to this.
184184
await session_client.cleanup()
185+
await jp_cleanup_subprocesses()
185186

186187

187-
async def test_create_file_session(session_client):
188+
async def test_create_file_session(session_client, jp_cleanup_subprocesses):
188189
resp = await session_client.create("foo/nb1.py", type="file")
189190
assert resp.code == 201
190191
newsession = j(resp)
191192
assert newsession["path"] == "foo/nb1.py"
192193
assert newsession["type"] == "file"
193194
await session_client.cleanup()
195+
await jp_cleanup_subprocesses()
194196

195197

196-
async def test_create_console_session(session_client):
198+
async def test_create_console_session(session_client, jp_cleanup_subprocesses):
197199
resp = await session_client.create("foo/abc123", type="console")
198200
assert resp.code == 201
199201
newsession = j(resp)
200202
assert newsession["path"] == "foo/abc123"
201203
assert newsession["type"] == "console"
202204
# Need to find a better solution to this.
203205
await session_client.cleanup()
206+
await jp_cleanup_subprocesses()
204207

205208

206-
async def test_create_deprecated(session_client):
209+
async def test_create_deprecated(session_client, jp_cleanup_subprocesses):
207210
resp = await session_client.create_deprecated("foo/nb1.ipynb")
208211
assert resp.code == 201
209212
newsession = j(resp)
@@ -212,9 +215,12 @@ async def test_create_deprecated(session_client):
212215
assert newsession["notebook"]["path"] == "foo/nb1.ipynb"
213216
# Need to find a better solution to this.
214217
await session_client.cleanup()
218+
await jp_cleanup_subprocesses()
215219

216220

217-
async def test_create_with_kernel_id(session_client, jp_fetch, jp_base_url):
221+
async def test_create_with_kernel_id(
222+
session_client, jp_fetch, jp_base_url, jp_cleanup_subprocesses
223+
):
218224
# create a new kernel
219225
resp = await jp_fetch("api/kernels", method="POST", allow_nonstandard_methods=True)
220226
kernel = j(resp)
@@ -241,9 +247,10 @@ async def test_create_with_kernel_id(session_client, jp_fetch, jp_base_url):
241247
assert_session_equality(got, new_session)
242248
# Need to find a better solution to this.
243249
await session_client.cleanup()
250+
await jp_cleanup_subprocesses()
244251

245252

246-
async def test_delete(session_client):
253+
async def test_delete(session_client, jp_cleanup_subprocesses):
247254
resp = await session_client.create("foo/nb1.ipynb")
248255
newsession = j(resp)
249256
sid = newsession["id"]
@@ -260,9 +267,10 @@ async def test_delete(session_client):
260267
assert expected_http_error(e, 404)
261268
# Need to find a better solution to this.
262269
await session_client.cleanup()
270+
await jp_cleanup_subprocesses()
263271

264272

265-
async def test_modify_path(session_client):
273+
async def test_modify_path(session_client, jp_cleanup_subprocesses):
266274
resp = await session_client.create("foo/nb1.ipynb")
267275
newsession = j(resp)
268276
sid = newsession["id"]
@@ -273,9 +281,10 @@ async def test_modify_path(session_client):
273281
assert changed["path"] == "nb2.ipynb"
274282
# Need to find a better solution to this.
275283
await session_client.cleanup()
284+
await jp_cleanup_subprocesses()
276285

277286

278-
async def test_modify_path_deprecated(session_client):
287+
async def test_modify_path_deprecated(session_client, jp_cleanup_subprocesses):
279288
resp = await session_client.create("foo/nb1.ipynb")
280289
newsession = j(resp)
281290
sid = newsession["id"]
@@ -286,9 +295,10 @@ async def test_modify_path_deprecated(session_client):
286295
assert changed["notebook"]["path"] == "nb2.ipynb"
287296
# Need to find a better solution to this.
288297
await session_client.cleanup()
298+
await jp_cleanup_subprocesses()
289299

290300

291-
async def test_modify_type(session_client):
301+
async def test_modify_type(session_client, jp_cleanup_subprocesses):
292302
resp = await session_client.create("foo/nb1.ipynb")
293303
newsession = j(resp)
294304
sid = newsession["id"]
@@ -299,9 +309,10 @@ async def test_modify_type(session_client):
299309
assert changed["type"] == "console"
300310
# Need to find a better solution to this.
301311
await session_client.cleanup()
312+
await jp_cleanup_subprocesses()
302313

303314

304-
async def test_modify_kernel_name(session_client, jp_fetch):
315+
async def test_modify_kernel_name(session_client, jp_fetch, jp_cleanup_subprocesses):
305316
resp = await session_client.create("foo/nb1.ipynb")
306317
before = j(resp)
307318
sid = before["id"]
@@ -321,9 +332,10 @@ async def test_modify_kernel_name(session_client, jp_fetch):
321332
assert kernel_list == [after["kernel"]]
322333
# Need to find a better solution to this.
323334
await session_client.cleanup()
335+
await jp_cleanup_subprocesses()
324336

325337

326-
async def test_modify_kernel_id(session_client, jp_fetch):
338+
async def test_modify_kernel_id(session_client, jp_fetch, jp_cleanup_subprocesses):
327339
resp = await session_client.create("foo/nb1.ipynb")
328340
before = j(resp)
329341
sid = before["id"]
@@ -351,9 +363,12 @@ async def test_modify_kernel_id(session_client, jp_fetch):
351363

352364
# Need to find a better solution to this.
353365
await session_client.cleanup()
366+
await jp_cleanup_subprocesses()
354367

355368

356-
async def test_restart_kernel(session_client, jp_base_url, jp_fetch, jp_ws_fetch):
369+
async def test_restart_kernel(
370+
session_client, jp_base_url, jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses
371+
):
357372

358373
# Create a session.
359374
resp = await session_client.create("foo/nb1.ipynb")
@@ -412,3 +427,4 @@ async def test_restart_kernel(session_client, jp_base_url, jp_fetch, jp_ws_fetch
412427

413428
# Need to find a better solution to this.
414429
await session_client.cleanup()
430+
await jp_cleanup_subprocesses()

jupyter_server/tests/test_terminal.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@
88
from traitlets.config import Config
99

1010

11-
# Kill all running terminals after each test to avoid cross-test issues
12-
# with still running terminals.
13-
@pytest.fixture
14-
def kill_all(jp_serverapp):
15-
async def _():
16-
await jp_serverapp.web_app.settings["terminal_manager"].kill_all()
17-
18-
return _
19-
20-
2111
@pytest.fixture
2212
def terminal_path(tmp_path):
2313
subdir = tmp_path.joinpath("terminal_path")
@@ -59,7 +49,7 @@ async def test_no_terminals(jp_fetch):
5949
assert len(data) == 0
6050

6151

62-
async def test_terminal_create(jp_fetch, kill_all):
52+
async def test_terminal_create(jp_fetch, jp_cleanup_subprocesses):
6353
resp = await jp_fetch(
6454
"api",
6555
"terminals",
@@ -80,10 +70,12 @@ async def test_terminal_create(jp_fetch, kill_all):
8070

8171
assert len(data) == 1
8272
assert data[0] == term
83-
await kill_all()
73+
await jp_cleanup_subprocesses()
8474

8575

86-
async def test_terminal_create_with_kwargs(jp_fetch, jp_ws_fetch, terminal_path, kill_all):
76+
async def test_terminal_create_with_kwargs(
77+
jp_fetch, jp_ws_fetch, terminal_path, jp_cleanup_subprocesses
78+
):
8779
resp_create = await jp_fetch(
8880
"api",
8981
"terminals",
@@ -106,10 +98,12 @@ async def test_terminal_create_with_kwargs(jp_fetch, jp_ws_fetch, terminal_path,
10698
data = json.loads(resp_get.body.decode())
10799

108100
assert data["name"] == term_name
109-
await kill_all()
101+
await jp_cleanup_subprocesses()
110102

111103

112-
async def test_terminal_create_with_cwd(jp_fetch, jp_ws_fetch, terminal_path):
104+
async def test_terminal_create_with_cwd(
105+
jp_fetch, jp_ws_fetch, terminal_path, jp_cleanup_subprocesses
106+
):
113107
resp = await jp_fetch(
114108
"api",
115109
"terminals",
@@ -140,6 +134,7 @@ async def test_terminal_create_with_cwd(jp_fetch, jp_ws_fetch, terminal_path):
140134
ws.close()
141135

142136
assert os.path.basename(terminal_path) in message_stdout
137+
await jp_cleanup_subprocesses()
143138

144139

145140
async def test_culling_config(jp_server_config, jp_configurable_serverapp):
@@ -151,7 +146,7 @@ async def test_culling_config(jp_server_config, jp_configurable_serverapp):
151146
assert terminal_mgr_settings.cull_interval == CULL_INTERVAL
152147

153148

154-
async def test_culling(jp_server_config, jp_fetch):
149+
async def test_culling(jp_server_config, jp_fetch, jp_cleanup_subprocesses):
155150
# POST request
156151
resp = await jp_fetch(
157152
"api",
@@ -181,3 +176,4 @@ async def test_culling(jp_server_config, jp_fetch):
181176
await asyncio.sleep(1)
182177

183178
assert culled
179+
await jp_cleanup_subprocesses()

0 commit comments

Comments
 (0)