-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #120 from consideRatio/pr/e22-test
Add basic start/stop test against a jupyterhub
- Loading branch information
Showing
7 changed files
with
211 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,71 @@ | ||
# Contributing | ||
|
||
Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). | ||
Welcome! As a [Jupyter] project, you can follow the [Jupyter contributor guide]. | ||
|
||
Make sure to also follow [Project Jupyter's Code of Conduct] for a friendly and | ||
welcoming collaborative environment. | ||
|
||
[jupyter]: https://jupyter.org | ||
[project jupyter's code of conduct]: https://github.com/jupyter/governance/blob/HEAD/conduct/code_of_conduct.md | ||
[jupyter contributor guide]: https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html | ||
|
||
## Setting up a local development environment | ||
|
||
To setup a local development environment to test changes to systemdspawner | ||
locally, a pre-requisite is to have systemd running in your system environment. | ||
You can check if you do by running `systemctl --version` in a terminal. | ||
|
||
Start by setting up Python, Node, and Git by reading the _System requirements_ | ||
section in [jupyterhub's contribution guide]. | ||
|
||
Then do the following: | ||
|
||
```shell | ||
# install configurable-http-proxy, a dependency for running a jupyterhub | ||
npm install -g configurable-http-proxy | ||
``` | ||
|
||
```shell | ||
# clone the systemdspawner github repository to your local computer | ||
git clone https://github.com/jupyterhub/systemdspawner | ||
cd systemdspawner | ||
``` | ||
|
||
```shell | ||
# install systemdspawner and test dependencies based on code in this folder | ||
pip install --editable ".[test]" | ||
``` | ||
|
||
We recommend installing `pre-commit` and configuring it to automatically run | ||
autoformatting before you make a git commit. This can be done by: | ||
|
||
```shell | ||
# configure pre-commit to help with autoformatting checks before commits are made | ||
pip install pre-commit | ||
pre-commit install --install-hooks | ||
``` | ||
|
||
[jupyterhub's contribution guide]: https://jupyterhub.readthedocs.io/en/stable/contributing/setup.html#system-requirements | ||
|
||
## Running tests | ||
|
||
A JupyterHub configured to use SystemdSpawner needs to be run as root, so due to | ||
that we need to run tests as root as well. To still have Python available, we | ||
may need to preserve the PATH when switching to root by using sudo as well, and | ||
perhaps also other environment variables. | ||
|
||
```shell | ||
# run pytest as root, preserving environment variables, including PATH | ||
sudo -E "PATH=$PATH" bash -c "pytest" | ||
``` | ||
|
||
To run all tests, there needs to be a non-root and non-nobody user specified | ||
explicitly via the systemdspawner defined `--system-test-user=USERNAME` flag for | ||
pytest. | ||
|
||
```shell | ||
# --system-test-user allows a user server to be started by SystemdSpawner as this | ||
# existing system user, which involves running a user server in the user's home | ||
# directory | ||
sudo -E "PATH=$PATH" bash -c "pytest --system-test-user=USERNAME" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ | |
"pytest", | ||
"pytest-asyncio", | ||
"pytest-cov", | ||
"pytest-jupyterhub", | ||
], | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import pytest | ||
from traitlets.config import Config | ||
|
||
# pytest-jupyterhub provides a pytest-plugin, and from it we get various | ||
# fixtures, where we make use of hub_app that builds on MockHub, which defaults | ||
# to providing a MockSpawner. | ||
# | ||
# ref: https://github.com/jupyterhub/pytest-jupyterhub | ||
# ref: https://github.com/jupyterhub/jupyterhub/blob/4.0.0/jupyterhub/tests/mocking.py#L224 | ||
# | ||
pytest_plugins = [ | ||
"jupyterhub-spawners-plugin", | ||
] | ||
|
||
|
||
def pytest_addoption(parser, pluginmanager): | ||
""" | ||
A pytest hook to register argparse-style options and ini-style config | ||
values. | ||
We use it to declare command-line arguments. | ||
ref: https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_addoption | ||
ref: https://docs.pytest.org/en/stable/reference/reference.html#pytest.Parser.addoption | ||
""" | ||
parser.addoption( | ||
"--system-test-user", | ||
help="Test server spawning for this existing system user", | ||
) | ||
|
||
|
||
def pytest_configure(config): | ||
""" | ||
A pytest hook to adjust configuration before running tests. | ||
We use it to declare pytest marks. | ||
ref: https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_configure | ||
ref: https://docs.pytest.org/en/stable/reference/reference.html#pytest.Config | ||
""" | ||
# These markers are registered to avoid warnings triggered by importing from | ||
# jupyterhub.tests.test_api in test_systemspawner.py. | ||
config.addinivalue_line("markers", "role: dummy") | ||
config.addinivalue_line("markers", "user: dummy") | ||
config.addinivalue_line("markers", "slow: dummy") | ||
config.addinivalue_line("markers", "group: dummy") | ||
config.addinivalue_line("markers", "services: dummy") | ||
|
||
|
||
@pytest.fixture | ||
async def systemdspawner_config(): | ||
""" | ||
Represents the base configuration of relevance to test SystemdSpawner. | ||
""" | ||
config = Config() | ||
config.JupyterHub.spawner_class = "systemd" | ||
|
||
# set cookie_secret to avoid having jupyterhub create a file | ||
config.JupyterHub.cookie_secret = "abc123" | ||
|
||
return config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from jupyterhub.tests.mocking import public_url | ||
from jupyterhub.tests.test_api import add_user, api_request | ||
from jupyterhub.utils import url_path_join | ||
from tornado.httpclient import AsyncHTTPClient | ||
|
||
from systemdspawner import systemd | ||
|
||
|
||
async def test_start_stop(hub_app, systemdspawner_config, pytestconfig): | ||
""" | ||
Starts a user server, verifies access to its /api/status endpoint, and stops | ||
the server. | ||
This test is skipped unless pytest is passed --system-test-user=USERNAME. | ||
The started user server process will run as the user in the user's home | ||
folder, which perhaps is fine, but maybe not. | ||
About using the root and nobody user: | ||
- A jupyter server started as root will error without the user server being | ||
passed the --allow-root flag. | ||
- SystemdSpawner runs the user server with a working directory set to the | ||
user's home home directory, which for the nobody user is /nonexistent on | ||
ubunutu. | ||
""" | ||
username = pytestconfig.getoption("--system-test-user", skip=True) | ||
unit_name = f"jupyter-{username}-singleuser.service" | ||
|
||
test_config = {} | ||
systemdspawner_config.merge(test_config) | ||
app = await hub_app(systemdspawner_config) | ||
|
||
add_user(app.db, app, name=username) | ||
user = app.users[username] | ||
|
||
# start the server with a HTTP POST request to jupyterhub's REST API | ||
r = await api_request(app, "users", username, "server", method="post") | ||
pending = r.status_code == 202 | ||
while pending: | ||
# check server status | ||
r = await api_request(app, "users", username) | ||
user_info = r.json() | ||
pending = user_info["servers"][""]["pending"] | ||
assert r.status_code in {201, 200}, r.text | ||
|
||
# verify the server is started via systemctl | ||
assert await systemd.service_running(unit_name) | ||
|
||
# verify the server is started by accessing the server's api/status | ||
token = user.new_api_token() | ||
url = url_path_join(public_url(app, user), "api/status") | ||
headers = {"Authorization": f"token {token}"} | ||
resp = await AsyncHTTPClient().fetch(url, headers=headers) | ||
assert resp.effective_url == url | ||
resp.rethrow() | ||
assert "kernels" in resp.body.decode("utf-8") | ||
|
||
# stop the server via a HTTP DELETE request to jupyterhub's REST API | ||
r = await api_request(app, "users", username, "server", method="delete") | ||
pending = r.status_code == 202 | ||
while pending: | ||
# check server status | ||
r = await api_request(app, "users", username) | ||
user_info = r.json() | ||
pending = user_info["servers"][""]["pending"] | ||
assert r.status_code in {204, 200}, r.text | ||
|
||
# verify the server is stopped via systemctl | ||
assert not await systemd.service_running(unit_name) |