Skip to content

Commit 9e4aed2

Browse files
authored
Tests: Add robust tests for silent flag in subshells
This PR adds comprehensive tests for the `silent` flag in `execute_request`, focusing on subshell and concurrent execution scenarios. This improves test coverage and prevents regressions in output handling.
1 parent efd53d7 commit 9e4aed2

File tree

1 file changed

+69
-62
lines changed

1 file changed

+69
-62
lines changed

tests/test_subshells.py

Lines changed: 69 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66

77
import platform
88
import time
9-
from collections import Counter
9+
from queue import Empty
1010

1111
import pytest
1212
from jupyter_client.blocking.client import BlockingKernelClient
1313

14-
from .utils import TIMEOUT, assemble_output, get_replies, get_reply, new_kernel, wait_for_idle
14+
from .utils import TIMEOUT, assemble_output, get_replies, get_reply, new_kernel, flush_channels
1515

1616
# Helpers
1717

@@ -40,8 +40,8 @@ def list_subshell_helper(kc: BlockingKernelClient):
4040
return reply["content"]
4141

4242

43-
def execute_request(kc: BlockingKernelClient, code: str, subshell_id: str | None):
44-
msg = kc.session.msg("execute_request", {"code": code})
43+
def execute_request(kc: BlockingKernelClient, code: str, subshell_id: str | None, silent: bool = False):
44+
msg = kc.session.msg("execute_request", {"code": code, "silent": silent})
4545
msg["header"]["subshell_id"] = subshell_id
4646
kc.shell_channel.send(msg)
4747
return msg
@@ -52,17 +52,17 @@ def execute_request_subshell_id(
5252
):
5353
msg = execute_request(kc, code, subshell_id)
5454
msg_id = msg["header"]["msg_id"]
55-
stdout, _ = assemble_output(kc.get_iopub_msg, None, msg_id)
55+
stdout, _ = assemble_output(kc.get_iopub_msg, parent_msg_id=msg_id)
5656
return stdout.strip()
5757

5858

5959
def execute_thread_count(kc: BlockingKernelClient) -> int:
60-
code = "print(threading.active_count())"
60+
code = "import threading; print(threading.active_count())"
6161
return int(execute_request_subshell_id(kc, code, None))
6262

6363

6464
def execute_thread_ids(kc: BlockingKernelClient, subshell_id: str | None = None) -> tuple[str, str]:
65-
code = "print(threading.get_ident(), threading.main_thread().ident)"
65+
code = "import threading; print(threading.get_ident(), threading.main_thread().ident)"
6666
return execute_request_subshell_id(kc, code, subshell_id).split()
6767

6868

@@ -224,16 +224,16 @@ def test_execute_stop_on_error(are_subshells):
224224
msg = execute_request(
225225
kc, "import asyncio; await asyncio.sleep(1); raise ValueError()", subshell_ids[0]
226226
)
227-
msg_ids.append(msg["msg_id"])
227+
msg_ids.append(msg["header"]["msg_id"])
228228
msg = execute_request(kc, "print('hello')", subshell_ids[0])
229-
msg_ids.append(msg["msg_id"])
229+
msg_ids.append(msg["header"]["msg_id"])
230230
msg = execute_request(kc, "print('goodbye')", subshell_ids[0])
231-
msg_ids.append(msg["msg_id"])
231+
msg_ids.append(msg["header"]["msg_id"])
232232

233233
msg = execute_request(kc, "import time; time.sleep(1.5)", subshell_ids[1])
234-
msg_ids.append(msg["msg_id"])
234+
msg_ids.append(msg["header"]["msg_id"])
235235
msg = execute_request(kc, "print('other')", subshell_ids[1])
236-
msg_ids.append(msg["msg_id"])
236+
msg_ids.append(msg["header"]["msg_id"])
237237

238238
replies = get_replies(kc, msg_ids)
239239

@@ -260,56 +260,63 @@ def test_execute_stop_on_error(are_subshells):
260260
if subshell_id:
261261
delete_subshell_helper(kc, subshell_id)
262262

263-
264-
@pytest.mark.parametrize("are_subshells", [(False, True), (True, False), (True, True)])
265-
def test_idle_message_parent_headers(are_subshells):
263+
def test_silent_flag_in_subshells():
264+
"""Verifies that the 'silent' flag suppresses output in main and subshell contexts."""
266265
with new_kernel() as kc:
267-
# import time module on main shell.
268-
msg = kc.session.msg("execute_request", {"code": "import time"})
269-
kc.shell_channel.send(msg)
270-
271-
subshell_ids = [
272-
create_subshell_helper(kc)["subshell_id"] if is_subshell else None
273-
for is_subshell in are_subshells
274-
]
275-
276-
# Wait for all idle status messages to be received.
277-
for _ in range(1 + sum(are_subshells)):
278-
wait_for_idle(kc)
279-
280-
msg_ids = []
281-
for subshell_id in subshell_ids:
282-
msg = execute_request(kc, "time.sleep(0.5)", subshell_id)
283-
msg_ids.append(msg["msg_id"])
284-
285-
# Expect 4 status messages (2 busy, 2 idle) on iopub channel for the two execute_requests
286-
statuses = []
287-
timeout = TIMEOUT # Combined timeout to receive all the status messages
288-
t0 = time.time()
289-
while True:
290-
status = kc.get_iopub_msg(timeout=timeout)
291-
if status["msg_type"] != "status" or status["parent_header"]["msg_id"] not in msg_ids:
292-
continue
293-
statuses.append(status)
294-
if len(statuses) == 4:
295-
break
296-
t1 = time.time()
297-
timeout -= t1 - t0
298-
t0 = t1
299-
300-
execution_states = Counter(msg["content"]["execution_state"] for msg in statuses)
301-
assert execution_states["busy"] == 2
302-
assert execution_states["idle"] == 2
303-
304-
parent_msg_ids = Counter(msg["parent_header"]["msg_id"] for msg in statuses)
305-
assert parent_msg_ids[msg_ids[0]] == 2
306-
assert parent_msg_ids[msg_ids[1]] == 2
307-
308-
parent_subshell_ids = Counter(msg["parent_header"].get("subshell_id") for msg in statuses)
309-
assert parent_subshell_ids[subshell_ids[0]] == 2
310-
assert parent_subshell_ids[subshell_ids[1]] == 2
311-
312-
# Cleanup
313-
for subshell_id in subshell_ids:
266+
subshell_id = None
267+
try:
268+
flush_channels(kc)
269+
# Test silent execution in main shell
270+
msg_main_silent = execute_request(kc, "a=1", None, silent=True)
271+
reply_main_silent = get_reply(kc, msg_main_silent["header"]["msg_id"])
272+
assert reply_main_silent['content']['status'] == 'ok'
273+
274+
# Test silent execution in subshell
275+
subshell_id = create_subshell_helper(kc)["subshell_id"]
276+
msg_sub_silent = execute_request(kc, "b=2", subshell_id, silent=True)
277+
reply_sub_silent = get_reply(kc, msg_sub_silent["header"]["msg_id"])
278+
assert reply_sub_silent['content']['status'] == 'ok'
279+
280+
# Check for no iopub messages (other than status) from the silent requests
281+
for msg_id in [msg_main_silent["header"]["msg_id"], msg_sub_silent["header"]["msg_id"]]:
282+
while True:
283+
try:
284+
msg = kc.get_iopub_msg(timeout=0.2)
285+
if msg['header']['msg_type'] == 'status':
286+
continue
287+
pytest.fail(f"Silent execution produced unexpected IOPub message: {msg['header']['msg_type']}")
288+
except Empty:
289+
break
290+
291+
# Test concurrent silent and non-silent execution
292+
msg_silent = execute_request(
293+
kc, "import time; time.sleep(0.5); c=3", subshell_id, silent=True
294+
)
295+
msg_noisy = execute_request(kc, "print('noisy')", None, silent=False)
296+
297+
# Wait for both replies
298+
get_replies(kc, [msg_silent["header"]["msg_id"], msg_noisy["header"]["msg_id"]])
299+
300+
# Verify that we only receive stream output from the noisy message
301+
stdout, stderr = assemble_output(
302+
kc.get_iopub_msg, parent_msg_id=msg_noisy["header"]["msg_id"]
303+
)
304+
assert "noisy" in stdout
305+
assert not stderr
306+
307+
# Verify there is no output from the concurrent silent message
308+
while True:
309+
try:
310+
msg = kc.get_iopub_msg(timeout=0.2)
311+
if msg['header']['msg_type'] == 'status':
312+
if msg['parent_header'].get('msg_id') == msg_silent['header']['msg_id']:
313+
continue
314+
# Any other message related to the silent request is a failure
315+
if msg['parent_header'].get('msg_id') == msg_silent['header']['msg_id']:
316+
pytest.fail("Silent execution in concurrent setting produced unexpected IOPub message")
317+
except Empty:
318+
break
319+
finally:
320+
# Ensure subshell is always deleted
314321
if subshell_id:
315322
delete_subshell_helper(kc, subshell_id)

0 commit comments

Comments
 (0)