mirror of
https://github.com/jupyter/docker-stacks.git
synced 2025-10-10 03:23:00 +00:00
Add some typing to tests
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
@@ -4,10 +4,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = Path(__file__).absolute()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -15,9 +17,9 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|||||||
# TODO: add local_sparklyr
|
# TODO: add local_sparklyr
|
||||||
["local_pyspark", "local_spylon", "local_sparkR", "issue_1168"],
|
["local_pyspark", "local_spylon", "local_sparkR", "issue_1168"],
|
||||||
)
|
)
|
||||||
def test_nbconvert(container, test_file):
|
def test_nbconvert(container: TrackedContainer, test_file: str) -> None:
|
||||||
"""Check if Spark notebooks can be executed"""
|
"""Check if Spark notebooks can be executed"""
|
||||||
host_data_dir = os.path.join(THIS_DIR, "data")
|
host_data_dir = THIS_DIR / "data"
|
||||||
cont_data_dir = "/home/jovyan/data"
|
cont_data_dir = "/home/jovyan/data"
|
||||||
output_dir = "/tmp"
|
output_dir = "/tmp"
|
||||||
timeout_ms = 600
|
timeout_ms = 600
|
||||||
@@ -29,7 +31,7 @@ def test_nbconvert(container, test_file):
|
|||||||
+ f"--execute {cont_data_dir}/{test_file}.ipynb"
|
+ f"--execute {cont_data_dir}/{test_file}.ipynb"
|
||||||
)
|
)
|
||||||
c = container.run(
|
c = container.run(
|
||||||
volumes={host_data_dir: {"bind": cont_data_dir, "mode": "ro"}},
|
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
|
||||||
tty=True,
|
tty=True,
|
||||||
command=["start.sh", "bash", "-c", command],
|
command=["start.sh", "bash", "-c", command],
|
||||||
)
|
)
|
||||||
|
@@ -4,11 +4,14 @@ import time
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_cli_args(container, http_client):
|
def test_cli_args(container: TrackedContainer, http_client: requests.Session) -> None:
|
||||||
"""Container should respect notebook server command line args
|
"""Container should respect notebook server command line args
|
||||||
(e.g., disabling token security)"""
|
(e.g., disabling token security)"""
|
||||||
c = container.run(command=["start-notebook.sh", "--NotebookApp.token=''"])
|
c = container.run(command=["start-notebook.sh", "--NotebookApp.token=''"])
|
||||||
@@ -26,7 +29,9 @@ def test_cli_args(container, http_client):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:Unverified HTTPS request")
|
@pytest.mark.filterwarnings("ignore:Unverified HTTPS request")
|
||||||
def test_unsigned_ssl(container, http_client):
|
def test_unsigned_ssl(
|
||||||
|
container: TrackedContainer, http_client: requests.Session
|
||||||
|
) -> None:
|
||||||
"""Container should generate a self-signed SSL certificate
|
"""Container should generate a self-signed SSL certificate
|
||||||
and notebook server should use it to enable HTTPS.
|
and notebook server should use it to enable HTTPS.
|
||||||
"""
|
"""
|
||||||
@@ -48,7 +53,7 @@ def test_unsigned_ssl(container, http_client):
|
|||||||
assert warnings[0].startswith("WARNING: Jupyter Notebook deprecation notice")
|
assert warnings[0].startswith("WARNING: Jupyter Notebook deprecation notice")
|
||||||
|
|
||||||
|
|
||||||
def test_uid_change(container):
|
def test_uid_change(container: TrackedContainer) -> None:
|
||||||
"""Container should change the UID of the default user."""
|
"""Container should change the UID of the default user."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
@@ -65,7 +70,7 @@ def test_uid_change(container):
|
|||||||
assert "uid=1010(jovyan)" in c.logs(stdout=True).decode("utf-8")
|
assert "uid=1010(jovyan)" in c.logs(stdout=True).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_gid_change(container):
|
def test_gid_change(container: TrackedContainer) -> None:
|
||||||
"""Container should change the GID of the default user."""
|
"""Container should change the GID of the default user."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
@@ -82,7 +87,7 @@ def test_gid_change(container):
|
|||||||
assert "groups=110(jovyan),100(users)" in logs
|
assert "groups=110(jovyan),100(users)" in logs
|
||||||
|
|
||||||
|
|
||||||
def test_nb_user_change(container):
|
def test_nb_user_change(container: TrackedContainer) -> None:
|
||||||
"""Container should change the user name (`NB_USER`) of the default user."""
|
"""Container should change the user name (`NB_USER`) of the default user."""
|
||||||
nb_user = "nayvoj"
|
nb_user = "nayvoj"
|
||||||
running_container = container.run(
|
running_container = container.run(
|
||||||
@@ -131,7 +136,7 @@ def test_nb_user_change(container):
|
|||||||
), f"Hidden folder .jupyter was not copied properly to {nb_user} home folder. stat: {output}, expected {expected_output}"
|
), f"Hidden folder .jupyter was not copied properly to {nb_user} home folder. stat: {output}, expected {expected_output}"
|
||||||
|
|
||||||
|
|
||||||
def test_chown_extra(container):
|
def test_chown_extra(container: TrackedContainer) -> None:
|
||||||
"""Container should change the UID/GID of a comma separated
|
"""Container should change the UID/GID of a comma separated
|
||||||
CHOWN_EXTRA list of folders."""
|
CHOWN_EXTRA list of folders."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
@@ -160,7 +165,7 @@ def test_chown_extra(container):
|
|||||||
assert "/opt/conda/bin/jupyter:1010:101" in logs
|
assert "/opt/conda/bin/jupyter:1010:101" in logs
|
||||||
|
|
||||||
|
|
||||||
def test_chown_home(container):
|
def test_chown_home(container: TrackedContainer) -> None:
|
||||||
"""Container should change the NB_USER home directory owner and
|
"""Container should change the NB_USER home directory owner and
|
||||||
group to the current value of NB_UID and NB_GID."""
|
group to the current value of NB_UID and NB_GID."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
@@ -183,7 +188,7 @@ def test_chown_home(container):
|
|||||||
assert "/home/kitten/.bashrc:1010:101" in logs
|
assert "/home/kitten/.bashrc:1010:101" in logs
|
||||||
|
|
||||||
|
|
||||||
def test_sudo(container):
|
def test_sudo(container: TrackedContainer) -> None:
|
||||||
"""Container should grant passwordless sudo to the default user."""
|
"""Container should grant passwordless sudo to the default user."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
@@ -199,7 +204,7 @@ def test_sudo(container):
|
|||||||
assert "uid=0(root)" in logs
|
assert "uid=0(root)" in logs
|
||||||
|
|
||||||
|
|
||||||
def test_sudo_path(container):
|
def test_sudo_path(container: TrackedContainer) -> None:
|
||||||
"""Container should include /opt/conda/bin in the sudo secure_path."""
|
"""Container should include /opt/conda/bin in the sudo secure_path."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
@@ -215,7 +220,7 @@ def test_sudo_path(container):
|
|||||||
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
|
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
|
||||||
|
|
||||||
|
|
||||||
def test_sudo_path_without_grant(container):
|
def test_sudo_path_without_grant(container: TrackedContainer) -> None:
|
||||||
"""Container should include /opt/conda/bin in the sudo secure_path."""
|
"""Container should include /opt/conda/bin in the sudo secure_path."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
@@ -230,7 +235,7 @@ def test_sudo_path_without_grant(container):
|
|||||||
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
|
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
|
||||||
|
|
||||||
|
|
||||||
def test_group_add(container):
|
def test_group_add(container: TrackedContainer) -> None:
|
||||||
"""Container should run with the specified uid, gid, and secondary
|
"""Container should run with the specified uid, gid, and secondary
|
||||||
group. It won't be possible to modify /etc/passwd since gid is nonzero, so
|
group. It won't be possible to modify /etc/passwd since gid is nonzero, so
|
||||||
additionally verify that setting gid=0 is suggested in a warning.
|
additionally verify that setting gid=0 is suggested in a warning.
|
||||||
@@ -252,7 +257,7 @@ def test_group_add(container):
|
|||||||
assert "uid=1010 gid=1010 groups=1010,100(users)" in logs
|
assert "uid=1010 gid=1010 groups=1010,100(users)" in logs
|
||||||
|
|
||||||
|
|
||||||
def test_set_uid(container):
|
def test_set_uid(container: TrackedContainer) -> None:
|
||||||
"""Container should run with the specified uid and NB_USER.
|
"""Container should run with the specified uid and NB_USER.
|
||||||
The /home/jovyan directory will not be writable since it's owned by 1000:users.
|
The /home/jovyan directory will not be writable since it's owned by 1000:users.
|
||||||
Additionally verify that "--group-add=users" is suggested in a warning to restore
|
Additionally verify that "--group-add=users" is suggested in a warning to restore
|
||||||
@@ -274,7 +279,7 @@ def test_set_uid(container):
|
|||||||
assert "--group-add=users" in warnings[0]
|
assert "--group-add=users" in warnings[0]
|
||||||
|
|
||||||
|
|
||||||
def test_set_uid_and_nb_user(container):
|
def test_set_uid_and_nb_user(container: TrackedContainer) -> None:
|
||||||
"""Container should run with the specified uid and NB_USER."""
|
"""Container should run with the specified uid and NB_USER."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
user="1010",
|
user="1010",
|
||||||
@@ -294,7 +299,7 @@ def test_set_uid_and_nb_user(container):
|
|||||||
assert "user is kitten but home is /home/jovyan" in warnings[0]
|
assert "user is kitten but home is /home/jovyan" in warnings[0]
|
||||||
|
|
||||||
|
|
||||||
def test_container_not_delete_bind_mount(container, tmp_path):
|
def test_container_not_delete_bind_mount(container: TrackedContainer, tmp_path) -> None:
|
||||||
"""Container should not delete host system files when using the (docker)
|
"""Container should not delete host system files when using the (docker)
|
||||||
-v bind mount flag and mapping to /home/jovyan.
|
-v bind mount flag and mapping to /home/jovyan.
|
||||||
"""
|
"""
|
||||||
@@ -324,7 +329,9 @@ def test_container_not_delete_bind_mount(container, tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("enable_root", [False, True])
|
@pytest.mark.parametrize("enable_root", [False, True])
|
||||||
def test_jupyter_env_vars_to_unset_as_root(container, enable_root):
|
def test_jupyter_env_vars_to_unset_as_root(
|
||||||
|
container: TrackedContainer, enable_root: bool
|
||||||
|
) -> None:
|
||||||
"""Environment variables names listed in JUPYTER_ENV_VARS_TO_UNSET
|
"""Environment variables names listed in JUPYTER_ENV_VARS_TO_UNSET
|
||||||
should be unset in the final environment."""
|
should be unset in the final environment."""
|
||||||
root_args = {"user": "root"} if enable_root else {}
|
root_args = {"user": "root"} if enable_root else {}
|
||||||
|
@@ -4,6 +4,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +19,9 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
("pip", "--version"),
|
("pip", "--version"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_package_manager(container, package_manager, version_arg):
|
def test_package_manager(
|
||||||
|
container: TrackedContainer, package_manager: str, version_arg: tuple[str, ...]
|
||||||
|
) -> None:
|
||||||
"""Test the notebook start-notebook script"""
|
"""Test the notebook start-notebook script"""
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
f"Test that the package manager {package_manager} is working properly ..."
|
f"Test that the package manager {package_manager} is working properly ..."
|
||||||
|
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_pandoc(container):
|
def test_pandoc(container: TrackedContainer) -> None:
|
||||||
"""Pandoc shall be able to convert MD to HTML."""
|
"""Pandoc shall be able to convert MD to HTML."""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
|
@@ -4,10 +4,14 @@ import logging
|
|||||||
|
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_python_version(container, python_next_version="3.10"):
|
def test_python_version(
|
||||||
|
container: TrackedContainer, python_next_version: str = "3.10"
|
||||||
|
) -> None:
|
||||||
"""Check that python version is lower than the next version"""
|
"""Check that python version is lower than the next version"""
|
||||||
LOGGER.info(f"Checking that python version is lower than {python_next_version}")
|
LOGGER.info(f"Checking that python version is lower than {python_next_version}")
|
||||||
c = container.run(
|
c = container.run(
|
||||||
|
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -14,7 +17,12 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
(None, "notebook"),
|
(None, "notebook"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_start_notebook(container, http_client, env, expected_server):
|
def test_start_notebook(
|
||||||
|
container: TrackedContainer,
|
||||||
|
http_client: requests.Session,
|
||||||
|
env,
|
||||||
|
expected_server: str,
|
||||||
|
) -> None:
|
||||||
"""Test the notebook start-notebook script"""
|
"""Test the notebook start-notebook script"""
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
f"Test that the start-notebook launches the {expected_server} server from the env {env} ..."
|
f"Test that the start-notebook launches the {expected_server} server from the env {env} ..."
|
||||||
@@ -46,7 +54,9 @@ def test_start_notebook(container, http_client, env, expected_server):
|
|||||||
assert msg in logs, f"Expected warning message {msg} not printed"
|
assert msg in logs, f"Expected warning message {msg} not printed"
|
||||||
|
|
||||||
|
|
||||||
def test_tini_entrypoint(container, pid=1, command="tini"):
|
def test_tini_entrypoint(
|
||||||
|
container: TrackedContainer, pid: int = 1, command: str = "tini"
|
||||||
|
) -> None:
|
||||||
"""Check that tini is launched as PID 1
|
"""Check that tini is launched as PID 1
|
||||||
|
|
||||||
Credits to the following answer for the ps options used in the test:
|
Credits to the following answer for the ps options used in the test:
|
||||||
|
19
conftest.py
19
conftest.py
@@ -2,6 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pytest
|
import pytest
|
||||||
@@ -15,7 +16,7 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def http_client():
|
def http_client() -> requests.Session:
|
||||||
"""Requests session with retries and backoff."""
|
"""Requests session with retries and backoff."""
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
retries = Retry(total=5, backoff_factor=1)
|
retries = Retry(total=5, backoff_factor=1)
|
||||||
@@ -25,13 +26,13 @@ def http_client():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def docker_client():
|
def docker_client() -> docker.DockerClient:
|
||||||
"""Docker client configured based on the host environment"""
|
"""Docker client configured based on the host environment"""
|
||||||
return docker.from_env()
|
return docker.from_env()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def image_name():
|
def image_name() -> str:
|
||||||
"""Image name to test"""
|
"""Image name to test"""
|
||||||
return os.getenv("TEST_IMAGE")
|
return os.getenv("TEST_IMAGE")
|
||||||
|
|
||||||
@@ -50,13 +51,15 @@ class TrackedContainer:
|
|||||||
Default keyword arguments to pass to docker.DockerClient.containers.run
|
Default keyword arguments to pass to docker.DockerClient.containers.run
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, docker_client, image_name, **kwargs):
|
def __init__(
|
||||||
|
self, docker_client: docker.DockerClient, image_name: str, **kwargs: typing.Any
|
||||||
|
):
|
||||||
self.container = None
|
self.container = None
|
||||||
self.docker_client = docker_client
|
self.docker_client = docker_client
|
||||||
self.image_name = image_name
|
self.image_name = image_name
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def run(self, **kwargs):
|
def run(self, **kwargs: typing.Any):
|
||||||
"""Runs a docker container using the preconfigured image name
|
"""Runs a docker container using the preconfigured image name
|
||||||
and a mix of the preconfigured container options and those passed
|
and a mix of the preconfigured container options and those passed
|
||||||
to this method.
|
to this method.
|
||||||
@@ -74,9 +77,7 @@ class TrackedContainer:
|
|||||||
-------
|
-------
|
||||||
docker.Container
|
docker.Container
|
||||||
"""
|
"""
|
||||||
all_kwargs = {}
|
all_kwargs = self.kwargs | kwargs
|
||||||
all_kwargs.update(self.kwargs)
|
|
||||||
all_kwargs.update(kwargs)
|
|
||||||
LOGGER.info(f"Running {self.image_name} with args {all_kwargs} ...")
|
LOGGER.info(f"Running {self.image_name} with args {all_kwargs} ...")
|
||||||
self.container = self.docker_client.containers.run(
|
self.container = self.docker_client.containers.run(
|
||||||
self.image_name,
|
self.image_name,
|
||||||
@@ -91,7 +92,7 @@ class TrackedContainer:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def container(docker_client, image_name):
|
def container(docker_client: docker.DockerClient, image_name: str):
|
||||||
"""Notebook container with initial configuration appropriate for testing
|
"""Notebook container with initial configuration appropriate for testing
|
||||||
(e.g., HTTP port exposed to the host for HTTP calls).
|
(e.g., HTTP port exposed to the host for HTTP calls).
|
||||||
|
|
||||||
|
@@ -2,10 +2,12 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_julia(container):
|
def test_julia(container: TrackedContainer) -> None:
|
||||||
"""Basic julia test"""
|
"""Basic julia test"""
|
||||||
LOGGER.info("Test that julia is correctly installed ...")
|
LOGGER.info("Test that julia is correctly installed ...")
|
||||||
running_container = container.run(
|
running_container = container.run(
|
||||||
|
@@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_inkscape(container):
|
def test_inkscape(container: TrackedContainer) -> None:
|
||||||
"""Inkscape shall be installed to be able to convert SVG files."""
|
"""Inkscape shall be installed to be able to convert SVG files."""
|
||||||
LOGGER.info("Test that inkscape is working by printing its version ...")
|
LOGGER.info("Test that inkscape is working by printing its version ...")
|
||||||
c = container.run(
|
c = container.run(
|
||||||
|
@@ -4,10 +4,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = Path(__file__).absolute()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -19,9 +21,11 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|||||||
("notebook_svg", "html"),
|
("notebook_svg", "html"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_nbconvert(container, test_file, output_format):
|
def test_nbconvert(
|
||||||
|
container: TrackedContainer, test_file: str, output_format: str
|
||||||
|
) -> None:
|
||||||
"""Check if nbconvert is able to convert a notebook file"""
|
"""Check if nbconvert is able to convert a notebook file"""
|
||||||
host_data_dir = os.path.join(THIS_DIR, "data")
|
host_data_dir = THIS_DIR / "data"
|
||||||
cont_data_dir = "/home/jovyan/data"
|
cont_data_dir = "/home/jovyan/data"
|
||||||
output_dir = "/tmp"
|
output_dir = "/tmp"
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
@@ -29,7 +33,7 @@ def test_nbconvert(container, test_file, output_format):
|
|||||||
)
|
)
|
||||||
command = f"jupyter nbconvert {cont_data_dir}/{test_file}.ipynb --output-dir {output_dir} --to {output_format}"
|
command = f"jupyter nbconvert {cont_data_dir}/{test_file}.ipynb --output-dir {output_dir} --to {output_format}"
|
||||||
c = container.run(
|
c = container.run(
|
||||||
volumes={host_data_dir: {"bind": cont_data_dir, "mode": "ro"}},
|
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
|
||||||
tty=True,
|
tty=True,
|
||||||
command=["start.sh", "bash", "-c", command],
|
command=["start.sh", "bash", "-c", command],
|
||||||
)
|
)
|
||||||
|
@@ -2,11 +2,13 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def test_spark_shell(container):
|
def test_spark_shell(container: TrackedContainer) -> None:
|
||||||
"""Checking if Spark (spark-shell) is running properly"""
|
"""Checking if Spark (spark-shell) is running properly"""
|
||||||
c = container.run(
|
c = container.run(
|
||||||
tty=True,
|
tty=True,
|
||||||
|
@@ -4,6 +4,8 @@ import logging
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
"jupyter-matplotlib",
|
"jupyter-matplotlib",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_check_extension(container, extension):
|
def test_check_extension(container: TrackedContainer, extension: str) -> None:
|
||||||
"""Basic check of each extension
|
"""Basic check of each extension
|
||||||
|
|
||||||
The list of extensions can be obtained through this command
|
The list of extensions can be obtained through this command
|
||||||
|
@@ -4,10 +4,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = Path(__file__).absolute()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -25,19 +27,21 @@ THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_matplotlib(container, test_file, expected_file, description):
|
def test_matplotlib(
|
||||||
|
container: TrackedContainer, test_file: str, expected_file: str, description: str
|
||||||
|
):
|
||||||
"""Various tests performed on matplotlib
|
"""Various tests performed on matplotlib
|
||||||
|
|
||||||
- Test that matplotlib is able to plot a graph and write it as an image
|
- Test that matplotlib is able to plot a graph and write it as an image
|
||||||
- Test matplotlib latex fonts, which depend on the cm-super package
|
- Test matplotlib latex fonts, which depend on the cm-super package
|
||||||
"""
|
"""
|
||||||
host_data_dir = os.path.join(THIS_DIR, "data")
|
host_data_dir = THIS_DIR / "data"
|
||||||
cont_data_dir = "/home/jovyan/data"
|
cont_data_dir = "/home/jovyan/data"
|
||||||
output_dir = "/tmp"
|
output_dir = "/tmp"
|
||||||
LOGGER.info(description)
|
LOGGER.info(description)
|
||||||
command = "sleep infinity"
|
command = "sleep infinity"
|
||||||
running_container = container.run(
|
running_container = container.run(
|
||||||
volumes={host_data_dir: {"bind": cont_data_dir, "mode": "ro"}},
|
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
|
||||||
tty=True,
|
tty=True,
|
||||||
command=["start.sh", "bash", "-c", command],
|
command=["start.sh", "bash", "-c", command],
|
||||||
)
|
)
|
||||||
|
@@ -11,7 +11,7 @@ from .git_helper import GitHelper
|
|||||||
from .manifests import ManifestHeader, ManifestInterface
|
from .manifests import ManifestHeader, ManifestInterface
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
BUILD_TIMESTAMP = datetime.datetime.utcnow().isoformat()[:-7] + "Z"
|
BUILD_TIMESTAMP = datetime.datetime.utcnow().isoformat()[:-7] + "Z"
|
||||||
@@ -24,7 +24,7 @@ def append_build_history_line(
|
|||||||
wiki_path: str,
|
wiki_path: str,
|
||||||
all_tags: list[str],
|
all_tags: list[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.info("Appending build history line")
|
LOGGER.info("Appending build history line")
|
||||||
|
|
||||||
date_column = f"`{BUILD_TIMESTAMP}`"
|
date_column = f"`{BUILD_TIMESTAMP}`"
|
||||||
image_column = MARKDOWN_LINE_BREAK.join(
|
image_column = MARKDOWN_LINE_BREAK.join(
|
||||||
@@ -58,7 +58,7 @@ def create_manifest_file(
|
|||||||
container,
|
container,
|
||||||
) -> None:
|
) -> None:
|
||||||
manifest_names = [manifest.__name__ for manifest in manifests]
|
manifest_names = [manifest.__name__ for manifest in manifests]
|
||||||
logger.info(f"Using manifests: {manifest_names}")
|
LOGGER.info(f"Using manifests: {manifest_names}")
|
||||||
|
|
||||||
commit_hash_tag = GitHelper.commit_hash_tag()
|
commit_hash_tag = GitHelper.commit_hash_tag()
|
||||||
manifest_file = os.path.join(
|
manifest_file = os.path.join(
|
||||||
@@ -76,7 +76,7 @@ def create_manifest_file(
|
|||||||
|
|
||||||
|
|
||||||
def create_manifests(short_image_name: str, owner: str, wiki_path: str) -> None:
|
def create_manifests(short_image_name: str, owner: str, wiki_path: str) -> None:
|
||||||
logger.info(f"Creating manifests for image: {short_image_name}")
|
LOGGER.info(f"Creating manifests for image: {short_image_name}")
|
||||||
taggers, manifests = get_taggers_and_manifests(short_image_name)
|
taggers, manifests = get_taggers_and_manifests(short_image_name)
|
||||||
|
|
||||||
image = f"{owner}/{short_image_name}:latest"
|
image = f"{owner}/{short_image_name}:latest"
|
||||||
@@ -100,6 +100,6 @@ if __name__ == "__main__":
|
|||||||
arg_parser.add_argument("--wiki-path", required=True, help="Path to the wiki pages")
|
arg_parser.add_argument("--wiki-path", required=True, help="Path to the wiki pages")
|
||||||
args = arg_parser.parse_args()
|
args = arg_parser.parse_args()
|
||||||
|
|
||||||
logger.info(f"Current build timestamp: {BUILD_TIMESTAMP}")
|
LOGGER.info(f"Current build timestamp: {BUILD_TIMESTAMP}")
|
||||||
|
|
||||||
create_manifests(args.short_image_name, args.owner, args.wiki_path)
|
create_manifests(args.short_image_name, args.owner, args.wiki_path)
|
||||||
|
@@ -3,8 +3,10 @@
|
|||||||
import docker
|
import docker
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DockerRunner:
|
class DockerRunner:
|
||||||
@@ -20,27 +22,29 @@ class DockerRunner:
|
|||||||
self.docker_client = docker_client
|
self.docker_client = docker_client
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
logger.info(f"Creating container for image {self.image_name} ...")
|
LOGGER.info(f"Creating container for image {self.image_name} ...")
|
||||||
self.container = self.docker_client.containers.run(
|
self.container = self.docker_client.containers.run(
|
||||||
image=self.image_name,
|
image=self.image_name,
|
||||||
command=self.command,
|
command=self.command,
|
||||||
detach=True,
|
detach=True,
|
||||||
)
|
)
|
||||||
logger.info(f"Container {self.container.name} created")
|
LOGGER.info(f"Container {self.container.name} created")
|
||||||
return self.container
|
return self.container
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
logger.info(f"Removing container {self.container.name} ...")
|
LOGGER.info(f"Removing container {self.container.name} ...")
|
||||||
if self.container:
|
if self.container:
|
||||||
self.container.remove(force=True)
|
self.container.remove(force=True)
|
||||||
logger.info(f"Container {self.container.name} removed")
|
LOGGER.info(f"Container {self.container.name} removed")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run_simple_command(container, cmd: str, print_result: bool = True):
|
def run_simple_command(
|
||||||
logger.info(f"Running cmd: '{cmd}' on container: {container}")
|
container: TrackedContainer, cmd: str, print_result: bool = True
|
||||||
|
):
|
||||||
|
LOGGER.info(f"Running cmd: '{cmd}' on container: {container}")
|
||||||
out = container.exec_run(cmd)
|
out = container.exec_run(cmd)
|
||||||
result = out.output.decode("utf-8").rstrip()
|
result = out.output.decode("utf-8").rstrip()
|
||||||
if print_result:
|
if print_result:
|
||||||
logger.info(f"Command result: {result}")
|
LOGGER.info(f"Command result: {result}")
|
||||||
assert out.exit_code == 0, f"Command: {cmd} failed"
|
assert out.exit_code == 0, f"Command: {cmd} failed"
|
||||||
return result
|
return result
|
||||||
|
@@ -1,14 +1,10 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
|
||||||
from plumbum.cmd import docker
|
from plumbum.cmd import docker
|
||||||
from .docker_runner import DockerRunner
|
from .docker_runner import DockerRunner
|
||||||
from .git_helper import GitHelper
|
from .git_helper import GitHelper
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def quoted_output(container, cmd: str) -> str:
|
def quoted_output(container, cmd: str) -> str:
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
|
@@ -9,7 +9,7 @@ from .get_taggers_and_manifests import get_taggers_and_manifests
|
|||||||
from .github_set_env import github_set_env
|
from .github_set_env import github_set_env
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def tag_image(short_image_name: str, owner: str) -> None:
|
def tag_image(short_image_name: str, owner: str) -> None:
|
||||||
@@ -20,7 +20,7 @@ def tag_image(short_image_name: str, owner: str) -> None:
|
|||||||
Tags are in a GitHub Actions environment also saved to environment variables
|
Tags are in a GitHub Actions environment also saved to environment variables
|
||||||
in a format making it easy to append them.
|
in a format making it easy to append them.
|
||||||
"""
|
"""
|
||||||
logger.info(f"Tagging image: {short_image_name}")
|
LOGGER.info(f"Tagging image: {short_image_name}")
|
||||||
taggers, _ = get_taggers_and_manifests(short_image_name)
|
taggers, _ = get_taggers_and_manifests(short_image_name)
|
||||||
|
|
||||||
image = f"{owner}/{short_image_name}:latest"
|
image = f"{owner}/{short_image_name}:latest"
|
||||||
@@ -31,7 +31,7 @@ def tag_image(short_image_name: str, owner: str) -> None:
|
|||||||
tagger_name = tagger.__name__
|
tagger_name = tagger.__name__
|
||||||
tag_value = tagger.tag_value(container)
|
tag_value = tagger.tag_value(container)
|
||||||
tags.append(tag_value)
|
tags.append(tag_value)
|
||||||
logger.info(
|
LOGGER.info(
|
||||||
f"Applying tag tagger_name: {tagger_name} tag_value: {tag_value}"
|
f"Applying tag tagger_name: {tagger_name} tag_value: {tag_value}"
|
||||||
)
|
)
|
||||||
docker["tag", image, f"{owner}/{short_image_name}:{tag_value}"]()
|
docker["tag", image, f"{owner}/{short_image_name}:{tag_value}"]()
|
||||||
|
@@ -1,14 +1,10 @@
|
|||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
|
||||||
from .git_helper import GitHelper
|
from .git_helper import GitHelper
|
||||||
from .docker_runner import DockerRunner
|
from .docker_runner import DockerRunner
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_program_version(container, program: str) -> str:
|
def _get_program_version(container, program: str) -> str:
|
||||||
return DockerRunner.run_simple_command(container, cmd=f"{program} --version")
|
return DockerRunner.run_simple_command(container, cmd=f"{program} --version")
|
||||||
|
|
||||||
|
@@ -27,25 +27,27 @@ from collections import defaultdict
|
|||||||
from itertools import chain
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CondaPackageHelper:
|
class CondaPackageHelper:
|
||||||
"""Conda package helper permitting to get information about packages"""
|
"""Conda package helper permitting to get information about packages"""
|
||||||
|
|
||||||
def __init__(self, container):
|
def __init__(self, container: TrackedContainer):
|
||||||
# if isinstance(container, TrackedContainer):
|
|
||||||
self.running_container = CondaPackageHelper.start_container(container)
|
self.running_container = CondaPackageHelper.start_container(container)
|
||||||
self.specs = None
|
self.requested: Optional[dict[str, set[str]]] = None
|
||||||
self.installed = None
|
self.installed: Optional[dict[str, set[str]]] = None
|
||||||
self.available = None
|
self.available: Optional[dict[str, set[str]]] = None
|
||||||
self.comparison = None
|
self.comparison: list[dict[str, str]] = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def start_container(container):
|
def start_container(container: TrackedContainer):
|
||||||
"""Start the TrackedContainer and return an instance of a running container"""
|
"""Start the TrackedContainer and return an instance of a running container"""
|
||||||
LOGGER.info(f"Starting container {container.image_name} ...")
|
LOGGER.info(f"Starting container {container.image_name} ...")
|
||||||
return container.run(
|
return container.run(
|
||||||
@@ -54,32 +56,34 @@ class CondaPackageHelper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _conda_export_command(from_history=False):
|
def _conda_export_command(from_history: bool) -> list[str]:
|
||||||
"""Return the mamba export command with or without history"""
|
"""Return the mamba export command with or without history"""
|
||||||
cmd = ["mamba", "env", "export", "-n", "base", "--json", "--no-builds"]
|
cmd = ["mamba", "env", "export", "-n", "base", "--json", "--no-builds"]
|
||||||
if from_history:
|
if from_history:
|
||||||
cmd.append("--from-history")
|
cmd.append("--from-history")
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
def installed_packages(self):
|
def installed_packages(self) -> dict[str, set[str]]:
|
||||||
"""Return the installed packages"""
|
"""Return the installed packages"""
|
||||||
if self.installed is None:
|
if self.installed is None:
|
||||||
LOGGER.info("Grabing the list of installed packages ...")
|
LOGGER.info("Grabing the list of installed packages ...")
|
||||||
self.installed = CondaPackageHelper._packages_from_json(
|
self.installed = CondaPackageHelper._packages_from_json(
|
||||||
self._execute_command(CondaPackageHelper._conda_export_command())
|
self._execute_command(
|
||||||
|
CondaPackageHelper._conda_export_command(from_history=False)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return self.installed
|
return self.installed
|
||||||
|
|
||||||
def specified_packages(self):
|
def requested_packages(self) -> dict[str, set[str]]:
|
||||||
"""Return the specifications (i.e. packages installation requested)"""
|
"""Return the requested package (i.e. `mamba install <package>`)"""
|
||||||
if self.specs is None:
|
if self.requested is None:
|
||||||
LOGGER.info("Grabing the list of specifications ...")
|
LOGGER.info("Grabing the list of manually requested packages ...")
|
||||||
self.specs = CondaPackageHelper._packages_from_json(
|
self.requested = CondaPackageHelper._packages_from_json(
|
||||||
self._execute_command(
|
self._execute_command(
|
||||||
CondaPackageHelper._conda_export_command(from_history=True)
|
CondaPackageHelper._conda_export_command(from_history=True)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return self.specs
|
return self.requested
|
||||||
|
|
||||||
def _execute_command(self, command):
|
def _execute_command(self, command):
|
||||||
"""Execute a command on a running container"""
|
"""Execute a command on a running container"""
|
||||||
@@ -87,14 +91,14 @@ class CondaPackageHelper:
|
|||||||
return rc.output.decode("utf-8")
|
return rc.output.decode("utf-8")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _packages_from_json(env_export):
|
def _packages_from_json(env_export) -> dict[str, set[str]]:
|
||||||
"""Extract packages and versions from the lines returned by the list of specifications"""
|
"""Extract packages and versions from the lines returned by the list of specifications"""
|
||||||
# dependencies = filter(lambda x: isinstance(x, str), json.loads(env_export).get("dependencies"))
|
# dependencies = filter(lambda x: isinstance(x, str), json.loads(env_export).get("dependencies"))
|
||||||
dependencies = json.loads(env_export).get("dependencies")
|
dependencies = json.loads(env_export).get("dependencies")
|
||||||
# Filtering packages installed through pip in this case it's a dict {'pip': ['toree==0.3.0']}
|
# Filtering packages installed through pip in this case it's a dict {'pip': ['toree==0.3.0']}
|
||||||
# Since we only manage packages installed through mamba here
|
# Since we only manage packages installed through mamba here
|
||||||
dependencies = filter(lambda x: isinstance(x, str), dependencies)
|
dependencies = filter(lambda x: isinstance(x, str), dependencies)
|
||||||
packages_dict = dict()
|
packages_dict: dict[str, set[str]] = dict()
|
||||||
for split in map(lambda x: re.split("=?=", x), dependencies):
|
for split in map(lambda x: re.split("=?=", x), dependencies):
|
||||||
# default values
|
# default values
|
||||||
package = split[0]
|
package = split[0]
|
||||||
@@ -129,14 +133,16 @@ class CondaPackageHelper:
|
|||||||
ddict[pkg].add(version)
|
ddict[pkg].add(version)
|
||||||
return ddict
|
return ddict
|
||||||
|
|
||||||
def check_updatable_packages(self, specifications_only=True):
|
def check_updatable_packages(
|
||||||
|
self, requested_only: bool = True
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
"""Check the updatable packages including or not dependencies"""
|
"""Check the updatable packages including or not dependencies"""
|
||||||
specs = self.specified_packages()
|
requested = self.requested_packages()
|
||||||
installed = self.installed_packages()
|
installed = self.installed_packages()
|
||||||
available = self.available_packages()
|
available = self.available_packages()
|
||||||
self.comparison = list()
|
self.comparison = []
|
||||||
for pkg, inst_vs in installed.items():
|
for pkg, inst_vs in installed.items():
|
||||||
if not specifications_only or pkg in specs:
|
if not requested_only or pkg in requested:
|
||||||
avail_vs = sorted(
|
avail_vs = sorted(
|
||||||
list(available[pkg]), key=CondaPackageHelper.semantic_cmp
|
list(available[pkg]), key=CondaPackageHelper.semantic_cmp
|
||||||
)
|
)
|
||||||
@@ -156,7 +162,7 @@ class CondaPackageHelper:
|
|||||||
return self.comparison
|
return self.comparison
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def semantic_cmp(version_string):
|
def semantic_cmp(version_string: str):
|
||||||
"""Manage semantic versioning for comparison"""
|
"""Manage semantic versioning for comparison"""
|
||||||
|
|
||||||
def mysplit(string):
|
def mysplit(string):
|
||||||
@@ -165,14 +171,14 @@ class CondaPackageHelper:
|
|||||||
|
|
||||||
return list(chain(map(version_substrs, string.split("."))))
|
return list(chain(map(version_substrs, string.split("."))))
|
||||||
|
|
||||||
def str_ord(string):
|
def str_ord(string: str) -> int:
|
||||||
num = 0
|
num = 0
|
||||||
for char in string:
|
for char in string:
|
||||||
num *= 255
|
num *= 255
|
||||||
num += ord(char)
|
num += ord(char)
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def try_int(version_str):
|
def try_int(version_str: str) -> int:
|
||||||
try:
|
try:
|
||||||
return int(version_str)
|
return int(version_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -181,13 +187,13 @@ class CondaPackageHelper:
|
|||||||
mss = list(chain(*mysplit(version_string)))
|
mss = list(chain(*mysplit(version_string)))
|
||||||
return tuple(map(try_int, mss))
|
return tuple(map(try_int, mss))
|
||||||
|
|
||||||
def get_outdated_summary(self, specifications_only=True):
|
def get_outdated_summary(self, requested_only: bool = True) -> str:
|
||||||
"""Return a summary of outdated packages"""
|
"""Return a summary of outdated packages"""
|
||||||
nb_packages = len(self.specs if specifications_only else self.installed)
|
nb_packages = len(self.requested if requested_only else self.installed)
|
||||||
nb_updatable = len(self.comparison)
|
nb_updatable = len(self.comparison)
|
||||||
updatable_ratio = nb_updatable / nb_packages
|
updatable_ratio = nb_updatable / nb_packages
|
||||||
return f"{nb_updatable}/{nb_packages} ({updatable_ratio:.0%}) packages could be updated"
|
return f"{nb_updatable}/{nb_packages} ({updatable_ratio:.0%}) packages could be updated"
|
||||||
|
|
||||||
def get_outdated_table(self):
|
def get_outdated_table(self) -> str:
|
||||||
"""Return a table of outdated packages"""
|
"""Return a table of outdated packages"""
|
||||||
return tabulate(self.comparison, headers="keys")
|
return tabulate(self.comparison, headers="keys")
|
||||||
|
@@ -2,7 +2,13 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
|
||||||
def test_secured_server(container, http_client):
|
import requests
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
|
def test_secured_server(
|
||||||
|
container: TrackedContainer, http_client: requests.Session
|
||||||
|
) -> None:
|
||||||
"""Notebook server should eventually request user login."""
|
"""Notebook server should eventually request user login."""
|
||||||
container.run()
|
container.run()
|
||||||
resp = http_client.get("http://localhost:8888")
|
resp = http_client.get("http://localhost:8888")
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
from helpers import CondaPackageHelper
|
from helpers import CondaPackageHelper
|
||||||
|
|
||||||
@@ -11,10 +12,10 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.info
|
@pytest.mark.info
|
||||||
def test_outdated_packages(container, specifications_only=True):
|
def test_outdated_packages(container: TrackedContainer, requested_only: bool = True):
|
||||||
"""Getting the list of updatable packages"""
|
"""Getting the list of updatable packages"""
|
||||||
LOGGER.info(f"Checking outdated packages in {container.image_name} ...")
|
LOGGER.info(f"Checking outdated packages in {container.image_name} ...")
|
||||||
pkg_helper = CondaPackageHelper(container)
|
pkg_helper = CondaPackageHelper(container)
|
||||||
pkg_helper.check_updatable_packages(specifications_only)
|
pkg_helper.check_updatable_packages(requested_only)
|
||||||
LOGGER.info(pkg_helper.get_outdated_summary(specifications_only))
|
LOGGER.info(pkg_helper.get_outdated_summary(requested_only))
|
||||||
LOGGER.info(f"\n{pkg_helper.get_outdated_table()}\n")
|
LOGGER.info(f"\n{pkg_helper.get_outdated_table()}\n")
|
||||||
|
@@ -12,9 +12,9 @@ The goal is to detect import errors that can be caused by incompatibilities betw
|
|||||||
- #1012: issue importing `sympy`
|
- #1012: issue importing `sympy`
|
||||||
- #966: isssue importing `pyarrow`
|
- #966: isssue importing `pyarrow`
|
||||||
|
|
||||||
This module checks dynamically, through the `CondaPackageHelper`, only the specified packages i.e. packages requested by `mamba install` in the `Dockerfile`s.
|
This module checks dynamically, through the `CondaPackageHelper`, only the requested packages i.e. packages requested by `mamba install` in the `Dockerfile`s.
|
||||||
This means that it does not check dependencies. This choice is a tradeoff to cover the main requirements while achieving reasonable test duration.
|
This means that it does not check dependencies. This choice is a tradeoff to cover the main requirements while achieving reasonable test duration.
|
||||||
However it could be easily changed (or completed) to cover also dependencies `package_helper.installed_packages()` instead of `package_helper.specified_packages()`.
|
However it could be easily changed (or completed) to cover also dependencies `package_helper.installed_packages()` instead of `package_helper.requested_packages()`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Example:
|
|||||||
# --------------------------------------------------------------------------------------------- live log setup ----------------------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------------------- live log setup ----------------------------------------------------------------------------------------------
|
||||||
# 2020-03-08 09:56:04 [ INFO] Starting container jupyter/datascience-notebook ... (helpers.py:51)
|
# 2020-03-08 09:56:04 [ INFO] Starting container jupyter/datascience-notebook ... (helpers.py:51)
|
||||||
# 2020-03-08 09:56:04 [ INFO] Running jupyter/datascience-notebook with args {'detach': True, 'ports': {'8888/tcp': 8888}, 'tty': True, 'command': ['start.sh', 'bash', '-c', 'sleep infinity']} ... (conftest.py:78)
|
# 2020-03-08 09:56:04 [ INFO] Running jupyter/datascience-notebook with args {'detach': True, 'ports': {'8888/tcp': 8888}, 'tty': True, 'command': ['start.sh', 'bash', '-c', 'sleep infinity']} ... (conftest.py:78)
|
||||||
# 2020-03-08 09:56:04 [ INFO] Grabing the list of specifications ... (helpers.py:76)
|
# 2020-03-08 09:56:04 [ INFO] Grabing the list of manually requested packages ... (helpers.py:76)
|
||||||
# ---------------------------------------------------------------------------------------------- live log call ----------------------------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------------------------- live log call ----------------------------------------------------------------------------------------------
|
||||||
# 2020-03-08 09:56:07 [ INFO] Testing the import of packages ... (test_packages.py:125)
|
# 2020-03-08 09:56:07 [ INFO] Testing the import of packages ... (test_packages.py:125)
|
||||||
# 2020-03-08 09:56:07 [ INFO] Trying to import conda (test_packages.py:127)
|
# 2020-03-08 09:56:07 [ INFO] Trying to import conda (test_packages.py:127)
|
||||||
@@ -38,6 +38,7 @@ Example:
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
from helpers import CondaPackageHelper
|
from helpers import CondaPackageHelper
|
||||||
|
|
||||||
@@ -75,57 +76,66 @@ EXCLUDED_PACKAGES = [
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def package_helper(container):
|
def package_helper(container: TrackedContainer) -> CondaPackageHelper:
|
||||||
"""Return a package helper object that can be used to perform tests on installed packages"""
|
"""Return a package helper object that can be used to perform tests on installed packages"""
|
||||||
return CondaPackageHelper(container)
|
return CondaPackageHelper(container)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def packages(package_helper):
|
def packages(package_helper: CondaPackageHelper) -> dict[str, set[str]]:
|
||||||
"""Return the list of specified packages (i.e. packages explicitely installed excluding dependencies)"""
|
"""Return the list of requested packages (i.e. packages explicitly installed excluding dependencies)"""
|
||||||
return package_helper.specified_packages()
|
return package_helper.requested_packages()
|
||||||
|
|
||||||
|
|
||||||
def package_map(package):
|
def package_map(package: str) -> str:
|
||||||
"""Perform a mapping between the python package name and the name used for the import"""
|
"""Perform a mapping between the python package name and the name used for the import"""
|
||||||
return PACKAGE_MAPPING.get(package, package)
|
return PACKAGE_MAPPING.get(package, package)
|
||||||
|
|
||||||
|
|
||||||
def excluded_package_predicate(package):
|
def excluded_package_predicate(package: str) -> bool:
|
||||||
"""Return whether a package is excluded from the list (i.e. a package that cannot be tested with standard imports)"""
|
"""Return whether a package is excluded from the list (i.e. a package that cannot be tested with standard imports)"""
|
||||||
return package in EXCLUDED_PACKAGES
|
return package in EXCLUDED_PACKAGES
|
||||||
|
|
||||||
|
|
||||||
def python_package_predicate(package):
|
def python_package_predicate(package: str) -> bool:
|
||||||
"""Predicate matching python packages"""
|
"""Predicate matching python packages"""
|
||||||
return not excluded_package_predicate(package) and not r_package_predicate(package)
|
return not excluded_package_predicate(package) and not r_package_predicate(package)
|
||||||
|
|
||||||
|
|
||||||
def r_package_predicate(package):
|
def r_package_predicate(package: str) -> bool:
|
||||||
"""Predicate matching R packages"""
|
"""Predicate matching R packages"""
|
||||||
return not excluded_package_predicate(package) and package.startswith("r-")
|
return not excluded_package_predicate(package) and package.startswith("r-")
|
||||||
|
|
||||||
|
|
||||||
def _check_import_package(package_helper, command):
|
def _check_import_package(
|
||||||
|
package_helper: CondaPackageHelper, command: list[str]
|
||||||
|
) -> int:
|
||||||
"""Generic function executing a command"""
|
"""Generic function executing a command"""
|
||||||
LOGGER.debug(f"Trying to import a package with [{command}] ...")
|
LOGGER.debug(f"Trying to import a package with [{command}] ...")
|
||||||
rc = package_helper.running_container.exec_run(command)
|
rc = package_helper.running_container.exec_run(command)
|
||||||
return rc.exit_code
|
return rc.exit_code
|
||||||
|
|
||||||
|
|
||||||
def check_import_python_package(package_helper, package):
|
def check_import_python_package(
|
||||||
|
package_helper: CondaPackageHelper, package: str
|
||||||
|
) -> int:
|
||||||
"""Try to import a Python package from the command line"""
|
"""Try to import a Python package from the command line"""
|
||||||
return _check_import_package(package_helper, ["python", "-c", f"import {package}"])
|
return _check_import_package(package_helper, ["python", "-c", f"import {package}"])
|
||||||
|
|
||||||
|
|
||||||
def check_import_r_package(package_helper, package):
|
def check_import_r_package(package_helper: CondaPackageHelper, package: str) -> int:
|
||||||
"""Try to import a R package from the command line"""
|
"""Try to import a R package from the command line"""
|
||||||
return _check_import_package(
|
return _check_import_package(
|
||||||
package_helper, ["R", "--slave", "-e", f"library({package})"]
|
package_helper, ["R", "--slave", "-e", f"library({package})"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _import_packages(package_helper, filtered_packages, check_function, max_failures):
|
def _import_packages(
|
||||||
|
package_helper: CondaPackageHelper,
|
||||||
|
filtered_packages: dict[str, set[str]],
|
||||||
|
check_function,
|
||||||
|
max_failures: int,
|
||||||
|
) -> None:
|
||||||
"""Test if packages can be imported
|
"""Test if packages can be imported
|
||||||
|
|
||||||
Note: using a list of packages instead of a fixture for the list of packages since pytest prevents use of multiple yields
|
Note: using a list of packages instead of a fixture for the list of packages since pytest prevents use of multiple yields
|
||||||
@@ -147,7 +157,7 @@ def _import_packages(package_helper, filtered_packages, check_function, max_fail
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def r_packages(packages):
|
def r_packages(packages: dict[str, set[str]]):
|
||||||
"""Return an iterable of R packages"""
|
"""Return an iterable of R packages"""
|
||||||
# package[2:] is to remove the leading "r-" appended on R packages
|
# package[2:] is to remove the leading "r-" appended on R packages
|
||||||
return map(
|
return map(
|
||||||
@@ -155,21 +165,25 @@ def r_packages(packages):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_python_packages(package_helper, python_packages, max_failures=0):
|
def test_r_packages(
|
||||||
"""Test the import of specified python packages"""
|
package_helper: CondaPackageHelper, r_packages, max_failures: int = 0
|
||||||
return _import_packages(
|
):
|
||||||
package_helper, python_packages, check_import_python_package, max_failures
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
|
||||||
def python_packages(packages):
|
|
||||||
"""Return an iterable of Python packages"""
|
|
||||||
return map(package_map, filter(python_package_predicate, packages))
|
|
||||||
|
|
||||||
|
|
||||||
def test_r_packages(package_helper, r_packages, max_failures=0):
|
|
||||||
"""Test the import of specified R packages"""
|
"""Test the import of specified R packages"""
|
||||||
return _import_packages(
|
return _import_packages(
|
||||||
package_helper, r_packages, check_import_r_package, max_failures
|
package_helper, r_packages, check_import_r_package, max_failures
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def python_packages(packages: dict[str, set[str]]):
|
||||||
|
"""Return an iterable of Python packages"""
|
||||||
|
return map(package_map, filter(python_package_predicate, packages))
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_packages(
|
||||||
|
package_helper: CondaPackageHelper, python_packages, max_failures: int = 0
|
||||||
|
):
|
||||||
|
"""Test the import of specified python packages"""
|
||||||
|
return _import_packages(
|
||||||
|
package_helper, python_packages, check_import_python_package, max_failures
|
||||||
|
)
|
||||||
|
@@ -2,26 +2,28 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
from pathlib import Path
|
||||||
|
|
||||||
|
from conftest import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
THIS_DIR = Path(__file__).absolute()
|
||||||
|
|
||||||
|
|
||||||
def test_units(container):
|
def test_units(container: TrackedContainer) -> None:
|
||||||
"""Various units tests
|
"""Various units tests
|
||||||
Add a py file in the {image}/test/units dir and it will be automatically tested
|
Add a py file in the {image}/test/units dir and it will be automatically tested
|
||||||
"""
|
"""
|
||||||
short_image_name = container.image_name[container.image_name.rfind("/") + 1 :]
|
short_image_name = container.image_name[container.image_name.rfind("/") + 1 :]
|
||||||
host_data_dir = os.path.join(THIS_DIR, f"../{short_image_name}/test/units")
|
host_data_dir = THIS_DIR / f"../{short_image_name}/test/units"
|
||||||
LOGGER.info(f"Searching for units tests in {host_data_dir}")
|
LOGGER.info(f"Searching for units tests in {host_data_dir}")
|
||||||
cont_data_dir = "/home/jovyan/data"
|
cont_data_dir = "/home/jovyan/data"
|
||||||
|
|
||||||
if not os.path.exists(host_data_dir):
|
if not host_data_dir.exists():
|
||||||
LOGGER.info(f"Not found unit tests for image: {container.image_name}")
|
LOGGER.info(f"Not found unit tests for image: {container.image_name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
for test_file in os.listdir(host_data_dir):
|
for test_file in host_data_dir.iterdir():
|
||||||
LOGGER.info(f"Running unit test: {test_file}")
|
LOGGER.info(f"Running unit test: {test_file}")
|
||||||
|
|
||||||
c = container.run(
|
c = container.run(
|
||||||
|
Reference in New Issue
Block a user