Skip to content

Commit

Permalink
Merge pull request #120 from consideRatio/pr/e22-test
Browse files Browse the repository at this point in the history
Add basic start/stop test against a jupyterhub
  • Loading branch information
minrk authored May 31, 2023
2 parents c03fbbd + d59e532 commit c4cac49
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 5 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ jobs:
pip freeze
- name: Run tests
# Running tests requires sudo permissions, but we also need to preserve
# the PATH to have pytest available etc.
# Tests needs to be run as root and we have to specify a non-root
# non-nobody system user to test with. We also need to preserve the PATH
# when running as root.
run: |
sudo -E "PATH=$PATH" bash -c "pytest --cov=systemdspawner"
sudo -E "PATH=$PATH" bash -c "pytest --cov=systemdspawner --system-test-user=$(whoami)"
# GitHub action reference: https://github.com/codecov/codecov-action
- uses: codecov/codecov-action@v3
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repos:
hooks:
- id: pyupgrade
args:
- --py37-plus
- --py38-plus

# Autoformat: Python code
- repo: https://github.com/PyCQA/autoflake
Expand Down
70 changes: 69 additions & 1 deletion CONTRIBUTING.md
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"
```
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ target_version = [
addopts = "--verbose --color=yes --durations=10"
asyncio_mode = "auto"
testpaths = ["tests"]
# warnings we can safely ignore stemming from jupyterhub 3 + sqlalchemy 2
filterwarnings = [
'ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SADeprecationWarning',
'ignore:.*The new signature is "def engine_connect\(conn\)"*:sqlalchemy.exc.SAWarning',
]


# tbump is used to simplify and standardize the release process when updating
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"pytest",
"pytest-asyncio",
"pytest-cov",
"pytest-jupyterhub",
],
},
)
61 changes: 61 additions & 0 deletions tests/conftest.py
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
70 changes: 70 additions & 0 deletions tests/test_systemdspawner.py
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)

0 comments on commit c4cac49

Please sign in to comment.