2022-05-23 00:16:32 +04:00

586 lines
20 KiB
Python

"""Tests for the KernelManager"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import asyncio
import concurrent.futures
import json
import os
import signal
import sys
import time
from subprocess import PIPE
import pytest
from jupyter_core import paths
from traitlets.config.loader import Config
from ..manager import _ShutdownStatus
from ..manager import start_new_async_kernel
from ..manager import start_new_kernel
from .utils import AsyncKMSubclass
from .utils import SyncKMSubclass
from jupyter_client import AsyncKernelManager
from jupyter_client import KernelManager
pjoin = os.path.join
TIMEOUT = 30
@pytest.fixture(params=["tcp", "ipc"])
def transport(request):
if sys.platform == "win32" and request.param == "ipc": #
pytest.skip("Transport 'ipc' not supported on Windows.")
return request.param
@pytest.fixture
def config(transport):
c = Config()
c.KernelManager.transport = transport
if transport == "ipc":
c.KernelManager.ip = "test"
return c
def _install_kernel(name="signaltest", extra_env=None):
if extra_env is None:
extra_env = {}
kernel_dir = pjoin(paths.jupyter_data_dir(), "kernels", name)
os.makedirs(kernel_dir)
with open(pjoin(kernel_dir, "kernel.json"), "w") as f:
f.write(
json.dumps(
{
"argv": [
sys.executable,
"-m",
"jupyter_client.tests.signalkernel",
"-f",
"{connection_file}",
],
"display_name": "Signal Test Kernel",
"env": {"TEST_VARS": "${TEST_VARS}:test_var_2", **extra_env},
}
)
)
@pytest.fixture
def install_kernel():
return _install_kernel()
def install_kernel_dont_shutdown():
_install_kernel("signaltest-no-shutdown", {"NO_SHUTDOWN_REPLY": "1"})
def install_kernel_dont_terminate():
return _install_kernel(
"signaltest-no-terminate", {"NO_SHUTDOWN_REPLY": "1", "NO_SIGTERM_REPLY": "1"}
)
@pytest.fixture
def start_kernel():
km, kc = start_new_kernel(kernel_name="signaltest")
yield km, kc
kc.stop_channels()
km.shutdown_kernel()
assert km.context.closed
@pytest.fixture
def km(config):
km = KernelManager(config=config)
return km
@pytest.fixture
def km_subclass(config):
km = SyncKMSubclass(config=config)
return km
@pytest.fixture
def zmq_context():
import zmq
ctx = zmq.Context()
yield ctx
ctx.term()
@pytest.fixture(params=[AsyncKernelManager, AsyncKMSubclass])
def async_km(request, config):
km = request.param(config=config)
return km
@pytest.fixture
def async_km_subclass(config):
km = AsyncKMSubclass(config=config)
return km
@pytest.fixture
async def start_async_kernel():
km, kc = await start_new_async_kernel(kernel_name="signaltest")
yield km, kc
kc.stop_channels()
await km.shutdown_kernel()
assert km.context.closed
class TestKernelManagerShutDownGracefully:
parameters = (
"name, install, expected",
[
("signaltest", _install_kernel, _ShutdownStatus.ShutdownRequest),
(
"signaltest-no-shutdown",
install_kernel_dont_shutdown,
_ShutdownStatus.SigtermRequest,
),
(
"signaltest-no-terminate",
install_kernel_dont_terminate,
_ShutdownStatus.SigkillRequest,
),
],
)
@pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals")
@pytest.mark.parametrize(*parameters)
def test_signal_kernel_subprocesses(self, name, install, expected):
# ipykernel doesn't support 3.6 and this test uses async shutdown_request
if expected == _ShutdownStatus.ShutdownRequest and sys.version_info < (3, 7):
pytest.skip()
install()
km, kc = start_new_kernel(kernel_name=name)
assert km._shutdown_status == _ShutdownStatus.Unset
assert km.is_alive()
# kc.execute("1")
kc.stop_channels()
km.shutdown_kernel()
if expected == _ShutdownStatus.ShutdownRequest:
expected = [expected, _ShutdownStatus.SigtermRequest]
else:
expected = [expected]
assert km._shutdown_status in expected
@pytest.mark.asyncio
@pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals")
@pytest.mark.parametrize(*parameters)
async def test_async_signal_kernel_subprocesses(self, name, install, expected):
install()
km, kc = await start_new_async_kernel(kernel_name=name)
assert km._shutdown_status == _ShutdownStatus.Unset
assert await km.is_alive()
# kc.execute("1")
kc.stop_channels()
await km.shutdown_kernel()
if expected == _ShutdownStatus.ShutdownRequest:
expected = [expected, _ShutdownStatus.SigtermRequest]
else:
expected = [expected]
assert km._shutdown_status in expected
class TestKernelManager:
def test_lifecycle(self, km):
km.start_kernel(stdout=PIPE, stderr=PIPE)
kc = km.client()
assert km.is_alive()
is_done = km.ready.done()
assert is_done
km.restart_kernel(now=True)
assert km.is_alive()
km.interrupt_kernel()
assert isinstance(km, KernelManager)
kc.stop_channels()
km.shutdown_kernel(now=True)
assert km.context.closed
def test_get_connect_info(self, km):
cinfo = km.get_connection_info()
keys = sorted(cinfo.keys())
expected = sorted(
[
"ip",
"transport",
"hb_port",
"shell_port",
"stdin_port",
"iopub_port",
"control_port",
"key",
"signature_scheme",
]
)
assert keys == expected
@pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals")
def test_signal_kernel_subprocesses(self, install_kernel, start_kernel):
km, kc = start_kernel
def execute(cmd):
request_id = kc.execute(cmd)
while True:
reply = kc.get_shell_msg(TIMEOUT)
if reply["parent_header"]["msg_id"] == request_id:
break
content = reply["content"]
assert content["status"] == "ok"
return content
N = 5
for i in range(N):
execute("start")
time.sleep(1) # make sure subprocs stay up
reply = execute("check")
assert reply["user_expressions"]["poll"] == [None] * N
# start a job on the kernel to be interrupted
kc.execute("sleep")
time.sleep(1) # ensure sleep message has been handled before we interrupt
km.interrupt_kernel()
reply = kc.get_shell_msg(TIMEOUT)
content = reply["content"]
assert content["status"] == "ok"
assert content["user_expressions"]["interrupted"]
# wait up to 10s for subprocesses to handle signal
for i in range(100):
reply = execute("check")
if reply["user_expressions"]["poll"] != [-signal.SIGINT] * N:
time.sleep(0.1)
else:
break
# verify that subprocesses were interrupted
assert reply["user_expressions"]["poll"] == [-signal.SIGINT] * N
def test_start_new_kernel(self, install_kernel, start_kernel):
km, kc = start_kernel
assert km.is_alive()
assert kc.is_alive()
assert km.context.closed is False
def _env_test_body(self, kc):
def execute(cmd):
request_id = kc.execute(cmd)
while True:
reply = kc.get_shell_msg(TIMEOUT)
if reply["parent_header"]["msg_id"] == request_id:
break
content = reply["content"]
assert content["status"] == "ok"
return content
reply = execute("env")
assert reply is not None
assert reply["user_expressions"]["env"] == "test_var_1:test_var_2"
def test_templated_kspec_env(self, install_kernel, start_kernel):
km, kc = start_kernel
assert km.is_alive()
assert kc.is_alive()
assert km.context.closed is False
self._env_test_body(kc)
def test_cleanup_context(self, km):
assert km.context is not None
km.cleanup_resources(restart=False)
assert km.context.closed
def test_no_cleanup_shared_context(self, zmq_context):
"""kernel manager does not terminate shared context"""
km = KernelManager(context=zmq_context)
assert km.context == zmq_context
assert km.context is not None
km.cleanup_resources(restart=False)
assert km.context.closed is False
assert zmq_context.closed is False
def test_subclass_callables(self, km_subclass):
km_subclass.reset_counts()
km_subclass.start_kernel(stdout=PIPE, stderr=PIPE)
assert km_subclass.call_count("start_kernel") == 1
assert km_subclass.call_count("_launch_kernel") == 1
is_alive = km_subclass.is_alive()
assert is_alive
km_subclass.reset_counts()
km_subclass.restart_kernel(now=True)
assert km_subclass.call_count("restart_kernel") == 1
assert km_subclass.call_count("shutdown_kernel") == 1
assert km_subclass.call_count("interrupt_kernel") == 1
assert km_subclass.call_count("_kill_kernel") == 1
assert km_subclass.call_count("cleanup_resources") == 1
assert km_subclass.call_count("start_kernel") == 1
assert km_subclass.call_count("_launch_kernel") == 1
assert km_subclass.call_count("signal_kernel") == 1
is_alive = km_subclass.is_alive()
assert is_alive
assert km_subclass.call_count("is_alive") >= 1
km_subclass.reset_counts()
km_subclass.interrupt_kernel()
assert km_subclass.call_count("interrupt_kernel") == 1
assert km_subclass.call_count("signal_kernel") == 1
assert isinstance(km_subclass, KernelManager)
km_subclass.reset_counts()
km_subclass.shutdown_kernel(now=False)
assert km_subclass.call_count("shutdown_kernel") == 1
assert km_subclass.call_count("interrupt_kernel") == 1
assert km_subclass.call_count("request_shutdown") == 1
assert km_subclass.call_count("finish_shutdown") == 1
assert km_subclass.call_count("cleanup_resources") == 1
assert km_subclass.call_count("signal_kernel") == 1
assert km_subclass.call_count("is_alive") >= 1
is_alive = km_subclass.is_alive()
assert is_alive is False
assert km_subclass.call_count("is_alive") >= 1
assert km_subclass.context.closed
class TestParallel:
@pytest.mark.timeout(TIMEOUT)
def test_start_sequence_kernels(self, config, install_kernel):
"""Ensure that a sequence of kernel startups doesn't break anything."""
self._run_signaltest_lifecycle(config)
self._run_signaltest_lifecycle(config)
self._run_signaltest_lifecycle(config)
@pytest.mark.timeout(TIMEOUT + 10)
def test_start_parallel_thread_kernels(self, config, install_kernel):
if config.KernelManager.transport == "ipc": # FIXME
pytest.skip("IPC transport is currently not working for this test!")
self._run_signaltest_lifecycle(config)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread_executor:
future1 = thread_executor.submit(self._run_signaltest_lifecycle, config)
future2 = thread_executor.submit(self._run_signaltest_lifecycle, config)
future1.result()
future2.result()
@pytest.mark.timeout(TIMEOUT)
@pytest.mark.skipif(
(sys.platform == "darwin") and (sys.version_info >= (3, 6)) and (sys.version_info < (3, 8)),
reason='"Bad file descriptor" error',
)
def test_start_parallel_process_kernels(self, config, install_kernel):
if config.KernelManager.transport == "ipc": # FIXME
pytest.skip("IPC transport is currently not working for this test!")
self._run_signaltest_lifecycle(config)
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread_executor:
future1 = thread_executor.submit(self._run_signaltest_lifecycle, config)
with concurrent.futures.ProcessPoolExecutor(max_workers=1) as process_executor:
future2 = process_executor.submit(self._run_signaltest_lifecycle, config)
future2.result()
future1.result()
@pytest.mark.timeout(TIMEOUT)
@pytest.mark.skipif(
(sys.platform == "darwin") and (sys.version_info >= (3, 6)) and (sys.version_info < (3, 8)),
reason='"Bad file descriptor" error',
)
def test_start_sequence_process_kernels(self, config, install_kernel):
if config.KernelManager.transport == "ipc": # FIXME
pytest.skip("IPC transport is currently not working for this test!")
self._run_signaltest_lifecycle(config)
with concurrent.futures.ProcessPoolExecutor(max_workers=1) as pool_executor:
future = pool_executor.submit(self._run_signaltest_lifecycle, config)
future.result()
def _prepare_kernel(self, km, startup_timeout=TIMEOUT, **kwargs):
km.start_kernel(**kwargs)
kc = km.client()
kc.start_channels()
try:
kc.wait_for_ready(timeout=startup_timeout)
except RuntimeError:
kc.stop_channels()
km.shutdown_kernel()
raise
return kc
def _run_signaltest_lifecycle(self, config=None):
km = KernelManager(config=config, kernel_name="signaltest")
kc = self._prepare_kernel(km, stdout=PIPE, stderr=PIPE)
def execute(cmd):
request_id = kc.execute(cmd)
while True:
reply = kc.get_shell_msg(TIMEOUT)
if reply["parent_header"]["msg_id"] == request_id:
break
content = reply["content"]
assert content["status"] == "ok"
return content
execute("start")
assert km.is_alive()
execute("check")
assert km.is_alive()
km.restart_kernel(now=True)
assert km.is_alive()
execute("check")
km.shutdown_kernel()
assert km.context.closed
kc.stop_channels()
@pytest.mark.asyncio
class TestAsyncKernelManager:
async def test_lifecycle(self, async_km):
await async_km.start_kernel(stdout=PIPE, stderr=PIPE)
is_alive = await async_km.is_alive()
assert is_alive
is_ready = async_km.ready.done()
assert is_ready
await async_km.restart_kernel(now=True)
is_alive = await async_km.is_alive()
assert is_alive
await async_km.interrupt_kernel()
assert isinstance(async_km, AsyncKernelManager)
await async_km.shutdown_kernel(now=True)
is_alive = await async_km.is_alive()
assert is_alive is False
assert async_km.context.closed
async def test_get_connect_info(self, async_km):
cinfo = async_km.get_connection_info()
keys = sorted(cinfo.keys())
expected = sorted(
[
"ip",
"transport",
"hb_port",
"shell_port",
"stdin_port",
"iopub_port",
"control_port",
"key",
"signature_scheme",
]
)
assert keys == expected
@pytest.mark.timeout(10)
@pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals")
async def test_signal_kernel_subprocesses(self, install_kernel, start_async_kernel):
km, kc = start_async_kernel
async def execute(cmd):
request_id = kc.execute(cmd)
while True:
reply = await kc.get_shell_msg(TIMEOUT)
if reply["parent_header"]["msg_id"] == request_id:
break
content = reply["content"]
assert content["status"] == "ok"
return content
# Ensure that shutdown_kernel and stop_channels are called at the end of the test.
# Note: we cannot use addCleanup(<func>) for these since it doesn't prpperly handle
# coroutines - which km.shutdown_kernel now is.
N = 5
for i in range(N):
await execute("start")
await asyncio.sleep(1) # make sure subprocs stay up
reply = await execute("check")
assert reply["user_expressions"]["poll"] == [None] * N
# start a job on the kernel to be interrupted
request_id = kc.execute("sleep")
await asyncio.sleep(1) # ensure sleep message has been handled before we interrupt
await km.interrupt_kernel()
while True:
reply = await kc.get_shell_msg(TIMEOUT)
if reply["parent_header"]["msg_id"] == request_id:
break
content = reply["content"]
assert content["status"] == "ok"
assert content["user_expressions"]["interrupted"] is True
# wait up to 5s for subprocesses to handle signal
for i in range(50):
reply = await execute("check")
if reply["user_expressions"]["poll"] != [-signal.SIGINT] * N:
await asyncio.sleep(0.1)
else:
break
# verify that subprocesses were interrupted
assert reply["user_expressions"]["poll"] == [-signal.SIGINT] * N
@pytest.mark.timeout(10)
async def test_start_new_async_kernel(self, install_kernel, start_async_kernel):
km, kc = start_async_kernel
is_alive = await km.is_alive()
assert is_alive
is_alive = await kc.is_alive()
assert is_alive
async def test_subclass_callables(self, async_km_subclass):
async_km_subclass.reset_counts()
await async_km_subclass.start_kernel(stdout=PIPE, stderr=PIPE)
assert async_km_subclass.call_count("start_kernel") == 1
assert async_km_subclass.call_count("_launch_kernel") == 1
is_alive = await async_km_subclass.is_alive()
assert is_alive
assert async_km_subclass.call_count("is_alive") >= 1
async_km_subclass.reset_counts()
await async_km_subclass.restart_kernel(now=True)
assert async_km_subclass.call_count("restart_kernel") == 1
assert async_km_subclass.call_count("shutdown_kernel") == 1
assert async_km_subclass.call_count("interrupt_kernel") == 1
assert async_km_subclass.call_count("_kill_kernel") == 1
assert async_km_subclass.call_count("cleanup_resources") == 1
assert async_km_subclass.call_count("start_kernel") == 1
assert async_km_subclass.call_count("_launch_kernel") == 1
assert async_km_subclass.call_count("signal_kernel") == 1
is_alive = await async_km_subclass.is_alive()
assert is_alive
assert async_km_subclass.call_count("is_alive") >= 1
async_km_subclass.reset_counts()
await async_km_subclass.interrupt_kernel()
assert async_km_subclass.call_count("interrupt_kernel") == 1
assert async_km_subclass.call_count("signal_kernel") == 1
assert isinstance(async_km_subclass, AsyncKernelManager)
async_km_subclass.reset_counts()
await async_km_subclass.shutdown_kernel(now=False)
assert async_km_subclass.call_count("shutdown_kernel") == 1
assert async_km_subclass.call_count("interrupt_kernel") == 1
assert async_km_subclass.call_count("request_shutdown") == 1
assert async_km_subclass.call_count("finish_shutdown") == 1
assert async_km_subclass.call_count("cleanup_resources") == 1
assert async_km_subclass.call_count("signal_kernel") == 1
assert async_km_subclass.call_count("is_alive") >= 1
is_alive = await async_km_subclass.is_alive()
assert is_alive is False
assert async_km_subclass.call_count("is_alive") >= 1
assert async_km_subclass.context.closed