Skip to content
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

More Cleanup #12

Merged
merged 7 commits into from
Apr 3, 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
55 changes: 44 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ jobs:
- name: Install the Python dependencies
run: |
pip install -e ".[test]" codecov
- name: Point at Jupyter Server branch with terminals removed.
run: |
pip install -U git+https://github.com/Zsailer/jupyter_server.git@jupyter_server_terminals
- name: List installed packages
run: |
pip freeze
Expand All @@ -75,6 +72,42 @@ jobs:
run: |
codecov

test_minimum_versions:
name: Test Minimum Versions
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
with:
python_version: "3.7"
- name: Install miniumum versions
uses: jupyterlab/maintainer-tools/.github/actions/install-minimums@v1
- name: Run the unit tests
run: |
pytest -vv -W default || pytest -vv -W default --lf

test_prereleases:
name: Test Prereleases
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Install the Python dependencies
run: |
pip install --pre -e ".[test]"
- name: List installed packages
run: |
pip freeze
pip check
- name: Run the tests
run: |
pytest -vv || pytest -vv --lf

make_sdist:
name: Make SDist
runs-on: ubuntu-latest
Expand All @@ -84,11 +117,11 @@ jobs:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1

# test_sdist:
# runs-on: ubuntu-latest
# needs: [make_sdist]
# name: Install from SDist and Test
# timeout-minutes: 15
# steps:
# - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
# - uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
test_sdist:
runs-on: ubuntu-latest
needs: [make_sdist]
name: Install from SDist and Test
timeout-minutes: 15
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1
22 changes: 22 additions & 0 deletions jupyter_server_terminals/api_handlers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from pathlib import Path

from tornado import web

Expand Down Expand Up @@ -31,6 +32,27 @@ def post(self):
"""POST /terminals creates a new terminal and redirects to it"""
data = self.get_json_body() or {}

# if cwd is a relative path, it should be relative to the root_dir,
# but if we pass it as relative, it will we be considered as relative to
# the path jupyter_server was started in
if "cwd" in data:
cwd = Path(data["cwd"])
if not cwd.resolve().exists():
cwd = Path(self.settings["server_root_dir"]).expanduser() / cwd
if not cwd.resolve().exists():
cwd = None

if cwd is None:
server_root_dir = self.settings["server_root_dir"]
self.log.debug(
f"Failed to find requested terminal cwd: {data.get('cwd')}\n"
f" It was not found within the server root neither: {server_root_dir}."
)
del data["cwd"]
else:
self.log.debug(f"Opening terminal in: {cwd.resolve()!s}")
data["cwd"] = str(cwd.resolve())

model = self.terminal_manager.create(**data)
self.finish(json.dumps(model))

Expand Down
5 changes: 4 additions & 1 deletion jupyter_server_terminals/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ def initialize_handlers(self):
)
self.handlers.extend(api_handlers.default_handlers)
self.serverapp.web_app.settings["terminal_manager"] = self.terminal_manager
self.serverapp.web_app.settings["terminals_available"] = self.settings[
"terminals_available"
]

def current_activity(self):
if self.terminals_available:
Expand All @@ -89,7 +92,7 @@ async def cleanup_terminals(self):
if not self.terminals_available:
return

terminal_manager = self.web_app.settings["terminal_manager"]
terminal_manager = self.terminal_manager
n_terminals = len(terminal_manager.list())
terminal_msg = trans.ngettext(
"Shutting down %d terminal", "Shutting down %d terminals", n_terminals
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test =
pytest_tornasync
pytest-cov
pytest-timeout
jupyter_server[test]>=1
jupyter_server[test] @ git+https://github.com/Zsailer/jupyter_server.git@jupyter_server_terminals

[options.packages.find]
exclude = ['docs*', 'tests*']
Expand Down
43 changes: 14 additions & 29 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""Tests for authorization"""
import asyncio

import pytest
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
from jupyter_server.auth.authorizer import Authorizer
from jupyter_server.auth.utils import HTTP_METHOD_TO_AUTH_ACTION, match_url_to_resource
from nbformat import writes
from nbformat.v4 import new_notebook
from tornado.httpclient import HTTPClientError
from tornado.websocket import WebSocketHandler
from traitlets.config import Config
Expand Down Expand Up @@ -123,37 +121,19 @@ async def test_authorized_requests(
request,
io_loop,
send_request,
tmp_path,
jp_serverapp,
jp_cleanup_subprocesses,
method,
url,
body,
allowed,
):
# Setup stuff for the Contents API
# Add a notebook on disk
contents_dir = tmp_path / jp_serverapp.root_dir
p = contents_dir / "dir_for_testing"
p.mkdir(parents=True, exist_ok=True)

# Create a notebook
nb = writes(new_notebook(), version=4)
nbname = p.joinpath("nb_for_testing.ipynb")
nbname.write_text(nb, encoding="utf-8")

# Setup
nbpath = "dir_for_testing/nb_for_testing.ipynb"
kernelspec = NATIVE_KERNEL_NAME
km = jp_serverapp.kernel_manager

if "terminal" in url:
term_manager = jp_serverapp.web_app.settings["terminal_manager"]
request.addfinalizer(lambda: io_loop.run_sync(term_manager.terminate_all))
term_model = term_manager.create()
term_name = term_model["name"]

url = url.format(**locals())
term_manager = jp_serverapp.web_app.settings["terminal_manager"]
request.addfinalizer(lambda: io_loop.run_sync(term_manager.terminate_all))
term_model = term_manager.create()
term_name = term_model["name"]

url = url.format(term_name=term_name)
if allowed:
# Create a server with full permissions
permissions = {
Expand All @@ -168,7 +148,12 @@ async def test_authorized_requests(
expected_codes = {403}
jp_serverapp.authorizer.permissions = permissions

code = await send_request(url, body=body, method=method)
assert code in expected_codes
while True:
code = await send_request(url, body=body, method=method)
if code == 404:
await asyncio.sleep(1)
continue
assert code in expected_codes
break

await jp_cleanup_subprocesses()
114 changes: 104 additions & 10 deletions tests/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
import os
import shutil
import time
import sys

import pytest
from tornado.httpclient import HTTPClientError
Expand All @@ -19,6 +19,16 @@ def terminal_path(tmp_path):
shutil.rmtree(str(subdir), ignore_errors=True)


@pytest.fixture
def terminal_root_dir(jp_root_dir):
subdir = jp_root_dir.joinpath("terminal_path")
subdir.mkdir()

yield subdir

shutil.rmtree(str(subdir), ignore_errors=True)


CULL_TIMEOUT = 10
CULL_INTERVAL = 3

Expand All @@ -38,7 +48,7 @@ def jp_server_config():
)


async def test_no_terminals(jp_fetch, jp_server_config):
async def test_no_terminals(jp_fetch):
resp_list = await jp_fetch(
"api",
"terminals",
Expand All @@ -51,7 +61,7 @@ async def test_no_terminals(jp_fetch, jp_server_config):
assert len(data) == 0


async def test_terminal_create(jp_server_config, jp_fetch, jp_cleanup_subprocesses):
async def test_terminal_create(jp_fetch, jp_cleanup_subprocesses):
resp = await jp_fetch(
"api",
"terminals",
Expand All @@ -77,9 +87,7 @@ async def test_terminal_create(jp_server_config, jp_fetch, jp_cleanup_subprocess
await jp_cleanup_subprocesses()


async def test_terminal_create_with_kwargs(
jp_server_config, jp_fetch, terminal_path, jp_cleanup_subprocesses
):
async def test_terminal_create_with_kwargs(jp_fetch, terminal_path, jp_cleanup_subprocesses):
resp_create = await jp_fetch(
"api",
"terminals",
Expand All @@ -106,7 +114,7 @@ async def test_terminal_create_with_kwargs(


async def test_terminal_create_with_cwd(
jp_server_config, jp_fetch, jp_ws_fetch, terminal_path, jp_cleanup_subprocesses
jp_fetch, jp_ws_fetch, terminal_path, jp_cleanup_subprocesses
):
resp = await jp_fetch(
"api",
Expand All @@ -126,7 +134,7 @@ async def test_terminal_create_with_cwd(
except HTTPClientError as e:
if e.code != 404:
raise
time.sleep(1)
await asyncio.sleep(1)

ws.write_message(json.dumps(["stdin", "pwd\r\n"]))

Expand All @@ -148,7 +156,93 @@ async def test_terminal_create_with_cwd(
await jp_cleanup_subprocesses()


async def test_culling_config(jp_server_config, jp_configurable_serverapp):
async def test_terminal_create_with_relative_cwd(
jp_fetch, jp_ws_fetch, jp_root_dir, terminal_root_dir, jp_cleanup_subprocesses
):
resp = await jp_fetch(
"api",
"terminals",
method="POST",
body=json.dumps({"cwd": str(terminal_root_dir.relative_to(jp_root_dir))}),
allow_nonstandard_methods=True,
)

data = json.loads(resp.body.decode())
term_name = data["name"]

while True:
try:
ws = await jp_ws_fetch("terminals", "websocket", term_name)
break
except HTTPClientError as e:
if e.code != 404:
raise
await asyncio.sleep(1)

ws.write_message(json.dumps(["stdin", "pwd\r\n"]))

message_stdout = ""
while True:
try:
message = await asyncio.wait_for(ws.read_message(), timeout=5.0)
except asyncio.TimeoutError:
break

message = json.loads(message)

if message[0] == "stdout":
message_stdout += message[1]

ws.close()

expected = terminal_root_dir.name if sys.platform == "win32" else str(terminal_root_dir)
assert expected in message_stdout
await jp_cleanup_subprocesses()


async def test_terminal_create_with_bad_cwd(jp_fetch, jp_ws_fetch, jp_cleanup_subprocesses):
non_existing_path = "/tmp/path/to/nowhere"
resp = await jp_fetch(
"api",
"terminals",
method="POST",
body=json.dumps({"cwd": non_existing_path}),
allow_nonstandard_methods=True,
)

data = json.loads(resp.body.decode())
term_name = data["name"]

while True:
try:
ws = await jp_ws_fetch("terminals", "websocket", term_name)
break
except HTTPClientError as e:
if e.code != 404:
raise
await asyncio.sleep(1)

ws.write_message(json.dumps(["stdin", "pwd\r\n"]))

message_stdout = ""
while True:
try:
message = await asyncio.wait_for(ws.read_message(), timeout=5.0)
except asyncio.TimeoutError:
break

message = json.loads(message)

if message[0] == "stdout":
message_stdout += message[1]

ws.close()

assert non_existing_path not in message_stdout
await jp_cleanup_subprocesses()


async def test_culling_config(jp_configurable_serverapp):
terminal_mgr_config = jp_configurable_serverapp().config.ServerApp.TerminalManager
assert terminal_mgr_config.cull_inactive_timeout == CULL_TIMEOUT
assert terminal_mgr_config.cull_interval == CULL_INTERVAL
Expand All @@ -158,7 +252,7 @@ async def test_culling_config(jp_server_config, jp_configurable_serverapp):


@pytest.mark.skipif(os.name == "nt", reason="Not currently working on Windows")
async def test_culling(jp_server_config, jp_fetch, jp_cleanup_subprocesses):
async def test_culling(jp_fetch, jp_cleanup_subprocesses):
# POST request
resp = await jp_fetch(
"api",
Expand Down