Skip to content

Clean up server fixtures and increase coverage #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
dependency_type: minimum
- name: Run the unit tests
run: |
hatch run test:nowarn || hatch run test:nowarn --lf
hatch -v run test:nowarn || hatch run test:nowarn --lf

test_prereleases:
name: Test Prereleases
Expand Down Expand Up @@ -85,6 +85,8 @@ jobs:
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
with:
test_command: hatch run test:test

pre_commit:
name: pre-commit
Expand Down
14 changes: 7 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ docs = [
"Sphinx",
]
client = [
"jupyter_client"
"jupyter_client>=7.4.0"
]
server = [
"jupyter_server",
"nbformat",
"pytest-tornasync"
"jupyter_server>=1.21",
"ipykernel>=6.14",
"nbformat>=5.3",
"pytest-tornasync>=0.6"
]
test = [
"pytest-jupyter[server]",
"pytest-timeout"
]

Expand All @@ -67,13 +67,13 @@ features = ["docs"]
build = "make -C docs html SPHINXOPTS='-W'"

[tool.hatch.envs.test]
features = ["test"]
features = ["test", "server", "client"]
[tool.hatch.envs.test.scripts]
test = "python -m pytest -vv {args}"
nowarn = "test -W default {args}"

[tool.hatch.envs.cov]
features = ["test"]
features = ["test", "server", "client"]
dependencies = ["coverage", "pytest-cov"]
[tool.hatch.envs.cov.scripts]
test = "python -m pytest -vv --cov pytest_jupyter --cov-branch --cov-report term-missing:skip-covered {args}"
Expand Down
91 changes: 27 additions & 64 deletions pytest_jupyter/jupyter_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import asyncio
import importlib
import io
import json
import logging
import os
import re
Expand All @@ -22,18 +21,19 @@
import nbformat
import tornado
import tornado.testing
from jupyter_server._version import version_info
from jupyter_server.auth import Authorizer
from jupyter_server.extension import serverextension
from jupyter_server.serverapp import JUPYTER_SERVICE_HANDLERS, ServerApp
from jupyter_server.services.contents.filemanager import AsyncFileContentsManager
from jupyter_server.services.contents.largefilemanager import AsyncLargeFileManager
from jupyter_server.utils import url_path_join
from pytest_tornasync.plugin import AsyncHTTPServerClient
from tornado.escape import url_escape
from tornado.httpclient import HTTPClientError
from tornado.websocket import WebSocketHandler
from traitlets.config import Config

is_v2 = version_info[0] == 2

except ImportError:
import warnings

Expand All @@ -51,22 +51,9 @@
pytest_plugins = ["pytest_tornasync", "pytest_jupyter"]


# NOTE: This is a temporary fix for Windows 3.8
# We have to override the io_loop fixture with an
# asyncio patch. This will probably be removed in
# the future.
@pytest.fixture
def jp_asyncio_patch():
"""Appropriately configures the event loop policy if running on Windows w/ Python >= 3.8."""
ServerApp()._init_asyncio_patch()


@pytest.fixture
def asyncio_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
yield loop
loop.close()
async def asyncio_loop():
return asyncio.get_running_loop()


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -113,11 +100,14 @@ async def get_server():
@pytest.fixture
def jp_server_config():
"""Allows tests to setup their specific configuration values."""
return Config(
{
"jpserver_extensions": {"jupyter_server_terminals": True},
}
)
if is_v2:
return Config(
{
"jpserver_extensions": {"jupyter_server_terminals": True},
}
)
else:
return Config({})


@pytest.fixture
Expand Down Expand Up @@ -199,6 +189,7 @@ def jp_configurable_serverapp(
jp_root_dir,
jp_logging_stream,
asyncio_loop,
io_loop,
):
"""Starts a Jupyter Server instance based on
the provided configuration values.
Expand All @@ -218,7 +209,7 @@ def my_test(jp_configurable_serverapp):
# explicitly put in config.
serverapp_config = jp_server_config.setdefault("ServerApp", {})
exts = serverapp_config.setdefault("jpserver_extensions", {})
if "jupyter_server_terminals" not in exts:
if "jupyter_server_terminals" not in exts and is_v2:
exts["jupyter_server_terminals"] = True

def _configurable_serverapp(
Expand All @@ -228,14 +219,19 @@ def _configurable_serverapp(
environ=jp_environ,
http_port=jp_http_port,
tmp_path=tmp_path,
io_loop=io_loop,
root_dir=jp_root_dir,
**kwargs,
):
c = Config(config)
c.NotebookNotary.db_file = ":memory:"
if "token" not in c.ServerApp and not c.IdentityProvider.token:
token = hexlify(os.urandom(4)).decode("ascii")
c.IdentityProvider.token = token

default_token = hexlify(os.urandom(4)).decode("ascii")
if not is_v2:
kwargs["token"] = default_token

elif "token" not in c.ServerApp and not c.IdentityProvider.token:
c.IdentityProvider.token = default_token

# Allow tests to configure root_dir via a file, argv, or its
# default (cwd) by specifying a value of None.
Expand Down Expand Up @@ -294,6 +290,8 @@ def jp_web_app(jp_serverapp):
@pytest.fixture
def jp_auth_header(jp_serverapp):
"""Configures an authorization header using the token from the serverapp fixture."""
if not is_v2:
return {"Authorization": f"token {jp_serverapp.token}"}
return {"Authorization": f"token {jp_serverapp.identity_provider.token}"}


Expand Down Expand Up @@ -331,6 +329,7 @@ def client_fetch(*parts, headers=None, params=None, **kwargs):
for key, value in jp_auth_header.items():
headers.setdefault(key, value)
# Make request.
print(id(http_server_client.io_loop.asyncio_loop))
return http_server_client.fetch(url, headers=headers, request_timeout=20, **kwargs)

return client_fetch
Expand Down Expand Up @@ -382,43 +381,6 @@ def client_fetch(*parts, headers=None, params=None, **kwargs):
return client_fetch


some_resource = "The very model of a modern major general"
sample_kernel_json = {
"argv": ["cat", "{connection_file}"],
"display_name": "Test kernel",
}


@pytest.fixture
def jp_kernelspecs(jp_data_dir):
"""Configures some sample kernelspecs in the Jupyter data directory."""
spec_names = ["sample", "sample2", "bad"]
for name in spec_names:
sample_kernel_dir = jp_data_dir.joinpath("kernels", name)
sample_kernel_dir.mkdir(parents=True)
# Create kernel json file
sample_kernel_file = sample_kernel_dir.joinpath("kernel.json")
kernel_json = sample_kernel_json.copy()
if name == "bad":
kernel_json["argv"] = ["non_existent_path"]
sample_kernel_file.write_text(json.dumps(kernel_json))
# Create resources text
sample_kernel_resources = sample_kernel_dir.joinpath("resource.txt")
sample_kernel_resources.write_text(some_resource)


@pytest.fixture(params=[True, False])
def jp_contents_manager(request, tmp_path):
"""Returns an AsyncFileContentsManager instance based on the use_atomic_writing parameter value."""
return AsyncFileContentsManager(root_dir=str(tmp_path), use_atomic_writing=request.param)


@pytest.fixture
def jp_large_contents_manager(tmp_path):
"""Returns an AsyncLargeFileManager instance."""
return AsyncLargeFileManager(root_dir=str(tmp_path))


@pytest.fixture
def jp_create_notebook(jp_root_dir):
"""Creates a notebook in the test's home directory."""
Expand All @@ -435,6 +397,7 @@ def inner(nbpath):
nb = nbformat.v4.new_notebook()
nbtext = nbformat.writes(nb, version=4)
nbpath.write_text(nbtext)
return nb

return inner

Expand Down
63 changes: 62 additions & 1 deletion tests/test_jupyter_server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
import json
import os
from unittest.mock import MagicMock

from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
from jupyter_server.auth import Authorizer
from jupyter_server.serverapp import ServerApp
from tornado.websocket import WebSocketHandler


def test_serverapp(jp_serverapp):
async def test_serverapp(jp_serverapp):
assert isinstance(jp_serverapp, ServerApp)


async def test_get_api_spec(jp_fetch):
response = await jp_fetch("api", "spec.yaml", method="GET")
assert response.code == 200


async def test_send_request(send_request):
code = await send_request("api/spec.yaml", method="GET")
assert code == 200


async def test_connection(jp_fetch, jp_ws_fetch, jp_http_port, jp_auth_header):
# Create kernel
r = await jp_fetch(
"api", "kernels", method="POST", body=json.dumps({"name": NATIVE_KERNEL_NAME})
)
kid = json.loads(r.body.decode())["id"]

# Get kernel info
r = await jp_fetch("api", "kernels", kid, method="GET")
model = json.loads(r.body.decode())
assert model["connections"] == 0

# Open a websocket connection.
ws = await jp_ws_fetch("api", "kernels", kid, "channels")
ws.close()


async def test_authorizer(jp_server_authorizer, jp_serverapp, jp_base_url):
auth: Authorizer = jp_server_authorizer(parent=jp_serverapp)
assert isinstance(auth, Authorizer)
assert auth.normalize_url("foo") == "/foo"
assert auth.normalize_url(f"{jp_base_url}/foo") == "/foo"
request = MagicMock()
request.method = "GET"
request.path = "foo"
handler = WebSocketHandler(jp_serverapp.web_app, request=request)
assert not auth.is_authorized(handler, {}, "execute", None)
assert auth.match_url_to_resource("/api/kernels") == "kernels"
assert auth.match_url_to_resource("/api/shutdown") == "server"


async def test_create_notebook(jp_create_notebook):
nb = jp_create_notebook("foo.ipynb")
assert "nbformat" in nb


def test_template_dir(jp_template_dir):
assert os.path.exists(jp_template_dir)


def test_extension_environ(jp_extension_environ):
pass