mirror of
https://github.com/jupyter/docker-stacks.git
synced 2025-10-07 18:14:05 +00:00
Better tests directory structure (#2231)
This commit is contained in:
@@ -8,6 +8,7 @@ All image manifests can be found in [the wiki](https://github.com/jupyter/docker
|
|||||||
Affected: all images.
|
Affected: all images.
|
||||||
|
|
||||||
- **Non-breaking:** Better tagging directory structure ([#2228](https://github.com/jupyter/docker-stacks/pull/2228)).
|
- **Non-breaking:** Better tagging directory structure ([#2228](https://github.com/jupyter/docker-stacks/pull/2228)).
|
||||||
|
- **Non-breaking:** Better testing directory structure ([#2231](https://github.com/jupyter/docker-stacks/pull/2231)).
|
||||||
|
|
||||||
## 2025-02-18
|
## 2025-02-18
|
||||||
|
|
||||||
|
@@ -1,33 +1,15 @@
|
|||||||
# 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
|
|
||||||
import os
|
import os
|
||||||
import socket
|
from collections.abc import Generator
|
||||||
from contextlib import closing
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import requests
|
import requests
|
||||||
from docker.models.containers import Container
|
|
||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from urllib3.util.retry import Retry
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def find_free_port() -> str:
|
|
||||||
"""Returns the available host port. Can be called in multiple threads/processes."""
|
|
||||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
|
||||||
s.bind(("", 0))
|
|
||||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
return s.getsockname()[1] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def get_health(container: Container) -> str:
|
|
||||||
api_client = docker.APIClient()
|
|
||||||
inspect_results = api_client.inspect_container(container.name)
|
|
||||||
return inspect_results["State"]["Health"]["Status"] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
@@ -52,95 +34,10 @@ def image_name() -> str:
|
|||||||
return os.environ["TEST_IMAGE"]
|
return os.environ["TEST_IMAGE"]
|
||||||
|
|
||||||
|
|
||||||
class TrackedContainer:
|
|
||||||
"""Wrapper that collects docker container configuration and delays
|
|
||||||
container creation/execution.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
docker_client: docker.DockerClient
|
|
||||||
Docker client instance
|
|
||||||
image_name: str
|
|
||||||
Name of the docker image to launch
|
|
||||||
**kwargs: dict, optional
|
|
||||||
Default keyword arguments to pass to docker.DockerClient.containers.run
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
docker_client: docker.DockerClient,
|
|
||||||
image_name: str,
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
self.container: Container | None = None
|
|
||||||
self.docker_client: docker.DockerClient = docker_client
|
|
||||||
self.image_name: str = image_name
|
|
||||||
self.kwargs: Any = kwargs
|
|
||||||
|
|
||||||
def run_detached(self, **kwargs: Any) -> Container:
|
|
||||||
"""Runs a docker container using the pre-configured image name
|
|
||||||
and a mix of the pre-configured container options and those passed
|
|
||||||
to this method.
|
|
||||||
|
|
||||||
Keeps track of the docker.Container instance spawned to kill it
|
|
||||||
later.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
**kwargs: dict, optional
|
|
||||||
Keyword arguments to pass to docker.DockerClient.containers.run
|
|
||||||
extending and/or overriding key/value pairs passed to the constructor
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
docker.Container
|
|
||||||
"""
|
|
||||||
all_kwargs = self.kwargs | kwargs
|
|
||||||
LOGGER.info(f"Running {self.image_name} with args {all_kwargs} ...")
|
|
||||||
self.container = self.docker_client.containers.run(
|
|
||||||
self.image_name,
|
|
||||||
**all_kwargs,
|
|
||||||
)
|
|
||||||
return self.container
|
|
||||||
|
|
||||||
def run_and_wait(
|
|
||||||
self,
|
|
||||||
timeout: int,
|
|
||||||
no_warnings: bool = True,
|
|
||||||
no_errors: bool = True,
|
|
||||||
no_failure: bool = True,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> str:
|
|
||||||
running_container = self.run_detached(**kwargs)
|
|
||||||
rv = running_container.wait(timeout=timeout)
|
|
||||||
logs = running_container.logs().decode("utf-8")
|
|
||||||
assert isinstance(logs, str)
|
|
||||||
LOGGER.debug(logs)
|
|
||||||
assert no_warnings == (not self.get_warnings(logs))
|
|
||||||
assert no_errors == (not self.get_errors(logs))
|
|
||||||
assert no_failure == (rv["StatusCode"] == 0)
|
|
||||||
return logs
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_errors(logs: str) -> list[str]:
|
|
||||||
return TrackedContainer._lines_starting_with(logs, "ERROR")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_warnings(logs: str) -> list[str]:
|
|
||||||
return TrackedContainer._lines_starting_with(logs, "WARNING")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _lines_starting_with(logs: str, pattern: str) -> list[str]:
|
|
||||||
return [line for line in logs.splitlines() if line.startswith(pattern)]
|
|
||||||
|
|
||||||
def remove(self) -> None:
|
|
||||||
"""Kills and removes the tracked docker container."""
|
|
||||||
if self.container:
|
|
||||||
self.container.remove(force=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def container(docker_client: docker.DockerClient, image_name: str) -> Container:
|
def container(
|
||||||
|
docker_client: docker.DockerClient, image_name: str
|
||||||
|
) -> Generator[TrackedContainer]:
|
||||||
"""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).
|
||||||
|
|
||||||
|
0
tests/hierarchy/__init__.py
Normal file
0
tests/hierarchy/__init__.py
Normal file
@@ -3,6 +3,9 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
||||||
|
IMAGE_SPECIFIC_TESTS_DIR = THIS_DIR.parent / "image_specific_tests"
|
||||||
|
|
||||||
|
assert IMAGE_SPECIFIC_TESTS_DIR.exists(), f"{IMAGE_SPECIFIC_TESTS_DIR} does not exist."
|
||||||
|
|
||||||
# Please, take a look at the hierarchy of the images here:
|
# Please, take a look at the hierarchy of the images here:
|
||||||
# https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#image-relationships
|
# https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#image-relationships
|
||||||
@@ -28,6 +31,8 @@ def get_test_dirs(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
test_dirs = get_test_dirs(ALL_IMAGES[short_image_name])
|
test_dirs = get_test_dirs(ALL_IMAGES[short_image_name])
|
||||||
if (current_image_tests_dir := THIS_DIR / short_image_name).exists():
|
if (
|
||||||
|
current_image_tests_dir := IMAGE_SPECIFIC_TESTS_DIR / short_image_name
|
||||||
|
).exists():
|
||||||
test_dirs.append(current_image_tests_dir)
|
test_dirs.append(current_image_tests_dir)
|
||||||
return test_dirs
|
return test_dirs
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
@@ -6,7 +6,8 @@ import time
|
|||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer, find_free_port
|
from tests.utils.find_free_port import find_free_port
|
||||||
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -5,7 +5,8 @@ import time
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer, get_health
|
from tests.utils.get_container_health import get_health
|
||||||
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -2,7 +2,8 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer, find_free_port
|
from tests.utils.find_free_port import find_free_port
|
||||||
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_secured_server(
|
def test_secured_server(
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -6,7 +6,8 @@ import time
|
|||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer, find_free_port
|
from tests.utils.find_free_port import find_free_port
|
||||||
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -1,7 +1,7 @@
|
|||||||
# 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 tests.conftest import TrackedContainer
|
from tests.utils.run_command import run_command
|
||||||
from tests.run_command import run_command
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_julia(container: TrackedContainer) -> None:
|
def test_julia(container: TrackedContainer) -> None:
|
@@ -1,7 +1,7 @@
|
|||||||
# 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 tests.conftest import TrackedContainer
|
from tests.shared_checks.R_mimetype_check import check_r_mimetypes
|
||||||
from tests.R_mimetype_check import check_r_mimetypes
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_mimetypes(container: TrackedContainer) -> None:
|
def test_mimetypes(container: TrackedContainer) -> None:
|
@@ -2,8 +2,8 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.shared_checks.pluto_check import check_pluto_proxy
|
||||||
from tests.pluto_check import check_pluto_proxy
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_pluto_proxy(
|
def test_pluto_proxy(
|
@@ -4,8 +4,8 @@ import logging
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.conda_package_helper import CondaPackageHelper
|
||||||
from tests.package_helper import CondaPackageHelper
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -2,8 +2,8 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.run_command import run_command
|
||||||
from tests.run_command import run_command
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
@@ -44,8 +44,8 @@ from collections.abc import Callable, Iterable
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.conda_package_helper import CondaPackageHelper
|
||||||
from tests.package_helper import CondaPackageHelper
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
EXPECTED_PYTHON_VERSION = "3.12"
|
EXPECTED_PYTHON_VERSION = "3.12"
|
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
@@ -2,8 +2,8 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.hierarchy.images_hierarchy import get_test_dirs
|
||||||
from tests.images_hierarchy import get_test_dirs
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -6,7 +6,7 @@ import time
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -1,7 +1,7 @@
|
|||||||
# 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 tests.conftest import TrackedContainer
|
from tests.utils.run_command import run_command
|
||||||
from tests.run_command import run_command
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_julia(container: TrackedContainer) -> None:
|
def test_julia(container: TrackedContainer) -> None:
|
@@ -2,8 +2,8 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.shared_checks.pluto_check import check_pluto_proxy
|
||||||
from tests.pluto_check import check_pluto_proxy
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_pluto_proxy(
|
def test_pluto_proxy(
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -1,7 +1,7 @@
|
|||||||
# 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 tests.conftest import TrackedContainer
|
from tests.shared_checks.R_mimetype_check import check_r_mimetypes
|
||||||
from tests.R_mimetype_check import check_r_mimetypes
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
|
|
||||||
def test_mimetypes(container: TrackedContainer) -> None:
|
def test_mimetypes(container: TrackedContainer) -> None:
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
||||||
|
|
@@ -4,7 +4,7 @@ import logging
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest # type: ignore
|
import pytest # type: ignore
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
THIS_DIR = Path(__file__).parent.resolve()
|
THIS_DIR = Path(__file__).parent.resolve()
|
@@ -6,7 +6,7 @@ import logging
|
|||||||
|
|
||||||
import plumbum
|
import plumbum
|
||||||
|
|
||||||
from tests.images_hierarchy import get_test_dirs
|
from tests.hierarchy.images_hierarchy import get_test_dirs
|
||||||
|
|
||||||
python3 = plumbum.local["python3"]
|
python3 = plumbum.local["python3"]
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
0
tests/shared_checks/__init__.py
Normal file
0
tests/shared_checks/__init__.py
Normal file
@@ -6,7 +6,8 @@ import time
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer, find_free_port
|
from tests.utils.find_free_port import find_free_port
|
||||||
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
0
tests/utils/__init__.py
Normal file
0
tests/utils/__init__.py
Normal file
@@ -32,7 +32,7 @@ from typing import Any
|
|||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
12
tests/utils/find_free_port.py
Normal file
12
tests/utils/find_free_port.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import socket
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
|
||||||
|
def find_free_port() -> str:
|
||||||
|
"""Returns the available host port. Can be called in multiple threads/processes."""
|
||||||
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||||
|
s.bind(("", 0))
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
return s.getsockname()[1] # type: ignore
|
10
tests/utils/get_container_health.py
Normal file
10
tests/utils/get_container_health.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import docker
|
||||||
|
from docker.models.containers import Container
|
||||||
|
|
||||||
|
|
||||||
|
def get_health(container: Container) -> str:
|
||||||
|
api_client = docker.APIClient()
|
||||||
|
inspect_results = api_client.inspect_container(container.name)
|
||||||
|
return inspect_results["State"]["Health"]["Status"] # type: ignore
|
@@ -2,7 +2,7 @@
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from tests.conftest import TrackedContainer
|
from tests.utils.tracked_container import TrackedContainer
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
96
tests/utils/tracked_container.py
Normal file
96
tests/utils/tracked_container.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import docker
|
||||||
|
from docker.models.containers import Container
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackedContainer:
|
||||||
|
"""Wrapper that collects docker container configuration and delays
|
||||||
|
container creation/execution.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
docker_client: docker.DockerClient
|
||||||
|
Docker client instance
|
||||||
|
image_name: str
|
||||||
|
Name of the docker image to launch
|
||||||
|
**kwargs: dict, optional
|
||||||
|
Default keyword arguments to pass to docker.DockerClient.containers.run
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
docker_client: docker.DockerClient,
|
||||||
|
image_name: str,
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
self.container: Container | None = None
|
||||||
|
self.docker_client: docker.DockerClient = docker_client
|
||||||
|
self.image_name: str = image_name
|
||||||
|
self.kwargs: Any = kwargs
|
||||||
|
|
||||||
|
def run_detached(self, **kwargs: Any) -> Container:
|
||||||
|
"""Runs a docker container using the pre-configured image name
|
||||||
|
and a mix of the pre-configured container options and those passed
|
||||||
|
to this method.
|
||||||
|
|
||||||
|
Keeps track of the docker.Container instance spawned to kill it
|
||||||
|
later.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
**kwargs: dict, optional
|
||||||
|
Keyword arguments to pass to docker.DockerClient.containers.run
|
||||||
|
extending and/or overriding key/value pairs passed to the constructor
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
docker.Container
|
||||||
|
"""
|
||||||
|
all_kwargs = self.kwargs | kwargs
|
||||||
|
LOGGER.info(f"Running {self.image_name} with args {all_kwargs} ...")
|
||||||
|
self.container = self.docker_client.containers.run(
|
||||||
|
self.image_name,
|
||||||
|
**all_kwargs,
|
||||||
|
)
|
||||||
|
return self.container
|
||||||
|
|
||||||
|
def run_and_wait(
|
||||||
|
self,
|
||||||
|
timeout: int,
|
||||||
|
no_warnings: bool = True,
|
||||||
|
no_errors: bool = True,
|
||||||
|
no_failure: bool = True,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
running_container = self.run_detached(**kwargs)
|
||||||
|
rv = running_container.wait(timeout=timeout)
|
||||||
|
logs = running_container.logs().decode("utf-8")
|
||||||
|
assert isinstance(logs, str)
|
||||||
|
LOGGER.debug(logs)
|
||||||
|
assert no_warnings == (not self.get_warnings(logs))
|
||||||
|
assert no_errors == (not self.get_errors(logs))
|
||||||
|
assert no_failure == (rv["StatusCode"] == 0)
|
||||||
|
return logs
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_errors(logs: str) -> list[str]:
|
||||||
|
return TrackedContainer._lines_starting_with(logs, "ERROR")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_warnings(logs: str) -> list[str]:
|
||||||
|
return TrackedContainer._lines_starting_with(logs, "WARNING")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lines_starting_with(logs: str, pattern: str) -> list[str]:
|
||||||
|
return [line for line in logs.splitlines() if line.startswith(pattern)]
|
||||||
|
|
||||||
|
def remove(self) -> None:
|
||||||
|
"""Kills and removes the tracked docker container."""
|
||||||
|
if self.container:
|
||||||
|
self.container.remove(force=True)
|
Reference in New Issue
Block a user