From f1665f3aa66eff443923d897ec553e09e47f6a78 Mon Sep 17 00:00:00 2001 From: Roy Moore Date: Sun, 8 Sep 2024 09:07:21 +0300 Subject: [PATCH] fix(core): Reorganize core tests and improve (#693) Found some loss ends while working on fixing the typing. The idea in this PR is to reorganize the tests to **better reflect** the use case (not all just in `test_core`) This should help help avoid later issues when we fix more typing (allowing better isolation/management) Also took this opportunity to **add missing tests** for `utils` and `config` So now we have: 1. `test_image` - stripped from `test_core` 2. `test_waiting_utils` - stripped from `test_core` 3. a much cleaner `test_core` 4. `test_utils` - new 5. `test_config` - new ```bash ---------- coverage: platform linux, python 3.10.12-final-0 ---------- Name Stmts Miss Branch BrPart Cover Missing --------------------------------------------------------------------------------------- core/testcontainers/core/__init__.py 0 0 0 0 100% core/testcontainers/core/auth.py 31 0 12 0 100% core/testcontainers/core/config.py 59 0 20 0 100% core/testcontainers/core/container.py 159 23 40 9 80% 20, 61-62, 82-84, 121->123, 135, 138, 158-162, 186, 191, 244->260, 250-259, 261 core/testcontainers/core/docker_client.py 118 18 32 9 79% 63-65, 96, 134->133, 138-139, 141, 152, 161, 179, 196, 200-201, 204-208 core/testcontainers/core/exceptions.py 3 0 0 0 100% core/testcontainers/core/generic.py 28 28 0 0 0% 13-82 core/testcontainers/core/image.py 47 2 6 2 92% 10, 68 core/testcontainers/core/labels.py 23 0 8 0 100% core/testcontainers/core/network.py 20 1 0 0 95% 30 core/testcontainers/core/utils.py 46 9 10 2 77% 27->exit, 63-70, 79 core/testcontainers/core/version.py 20 0 12 0 100% core/testcontainers/core/waiting_utils.py 48 8 14 4 81% 26, 60-67, 77, 104->106, 122 --------------------------------------------------------------------------------------- TOTAL 602 89 154 26 83% ``` --- core/tests/test_config.py | 62 +++++++++++++++++++++++++ core/tests/test_core.py | 79 +------------------------------- core/tests/test_image.py | 66 ++++++++++++++++++++++++++ core/tests/test_utils.py | 53 +++++++++++++++++++++ core/tests/test_waiting_utils.py | 14 ++++++ 5 files changed, 197 insertions(+), 77 deletions(-) create mode 100644 core/tests/test_config.py create mode 100644 core/tests/test_image.py create mode 100644 core/tests/test_utils.py create mode 100644 core/tests/test_waiting_utils.py diff --git a/core/tests/test_config.py b/core/tests/test_config.py new file mode 100644 index 00000000..a6597fd4 --- /dev/null +++ b/core/tests/test_config.py @@ -0,0 +1,62 @@ +from testcontainers.core.config import TestcontainersConfiguration as TCC, TC_FILE + +from pytest import MonkeyPatch, mark, LogCaptureFixture + +import logging +import tempfile + + +def test_read_tc_properties(monkeypatch: MonkeyPatch) -> None: + with tempfile.TemporaryDirectory() as tmpdirname: + file = f"{tmpdirname}/{TC_FILE}" + with open(file, "w") as f: + f.write("tc.host=some_value\n") + + monkeypatch.setattr("testcontainers.core.config.TC_GLOBAL", file) + + config = TCC() + assert config.tc_properties == {"tc.host": "some_value"} + + +@mark.parametrize("docker_auth_config_env", ["key=value", ""]) +@mark.parametrize("warning_dict", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}]) +@mark.parametrize("warning_dict_post", [{}, {"key": "value"}, {"DOCKER_AUTH_CONFIG": "TEST"}]) +def test_docker_auth_config( + caplog: LogCaptureFixture, + monkeypatch: MonkeyPatch, + docker_auth_config_env: str, + warning_dict: dict[str, str], + warning_dict_post: dict[str, str], +) -> None: + monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict) + monkeypatch.setenv("DOCKER_AUTH_CONFIG", docker_auth_config_env) + caplog.set_level(logging.WARNING) + + config = TCC() + if not docker_auth_config_env: + assert config.docker_auth_config == "" + assert caplog.text == "" + else: + assert config.docker_auth_config == docker_auth_config_env + + if "DOCKER_AUTH_CONFIG" in warning_dict: + assert warning_dict["DOCKER_AUTH_CONFIG"] in caplog.text + + if warning_dict == {}: + monkeypatch.setattr("testcontainers.core.config._WARNINGS", warning_dict_post) + + config.docker_auth_config = "new_value" + assert config.docker_auth_config == "new_value" + + +def test_tc_properties_get_tc_host() -> None: + config = TCC() + config.tc_properties = {"tc.host": "some_value"} + assert config.tc_properties_get_tc_host() == "some_value" + + +def test_timeout() -> None: + config = TCC() + config.max_tries = 2 + config.sleep_time = 3 + assert config.timeout == 6 diff --git a/core/tests/test_core.py b/core/tests/test_core.py index 8d0c7794..42321e28 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -1,19 +1,4 @@ -import pytest -import tempfile -import random -import os - -from pathlib import Path -from typing import Optional - from testcontainers.core.container import DockerContainer -from testcontainers.core.image import DockerImage -from testcontainers.core.waiting_utils import wait_for_logs - - -def test_timeout_is_raised_when_waiting_for_logs(): - with pytest.raises(TimeoutError), DockerContainer("alpine").with_command("sleep 2") as container: - wait_for_logs(container, "Hello from Docker!", timeout=1e-3) def test_garbage_collection_is_defensive(): @@ -26,69 +11,9 @@ def test_garbage_collection_is_defensive(): del container -def test_wait_for_hello(): - with DockerContainer("hello-world") as container: - wait_for_logs(container, "Hello from Docker!") - - -def test_can_get_logs(): +def test_get_logs(): with DockerContainer("hello-world") as container: - wait_for_logs(container, "Hello from Docker!") stdout, stderr = container.get_logs() assert isinstance(stdout, bytes) assert isinstance(stderr, bytes) - assert stdout, "There should be something on stdout" - - -@pytest.mark.parametrize("test_cleanup", [True, False]) -@pytest.mark.parametrize("test_image_tag", [None, "test-image:latest"]) -def test_docker_image(test_image_tag: Optional[str], test_cleanup: bool, check_for_image): - with tempfile.TemporaryDirectory() as temp_directory: - # It's important to use a random string to avoid image caching - random_string = "Hello from Docker Image! " + str(random.randint(0, 1000)) - with open(f"{temp_directory}/Dockerfile", "w") as f: - f.write( - f""" - FROM alpine:latest - CMD echo "{random_string}" - """ - ) - with DockerImage(path=temp_directory, tag=test_image_tag, clean_up=test_cleanup) as image: - image_short_id = image.short_id - assert image.tag is test_image_tag, f"Expected {test_image_tag}, got {image.tag}" - assert image.short_id is not None, "Short ID should not be None" - logs = image.get_logs() - assert isinstance(logs, list), "Logs should be a list" - assert logs[0] == {"stream": "Step 1/2 : FROM alpine:latest"} - assert logs[3] == {"stream": f'Step 2/2 : CMD echo "{random_string}"'} - with DockerContainer(str(image)) as container: - assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch" - assert container.get_logs() == ((random_string + "\n").encode(), b""), "Container logs mismatch" - - check_for_image(image_short_id, test_cleanup) - - -@pytest.mark.parametrize("dockerfile_path", [None, Path("subdir/my.Dockerfile")]) -def test_docker_image_with_custom_dockerfile_path(dockerfile_path: Optional[Path]): - with tempfile.TemporaryDirectory() as temp_directory: - temp_dir_path = Path(temp_directory) - if dockerfile_path: - os.makedirs(temp_dir_path / dockerfile_path.parent, exist_ok=True) - dockerfile_rel_path = dockerfile_path - dockerfile_kwargs = {"dockerfile_path": dockerfile_path} - else: - dockerfile_rel_path = Path("Dockerfile") # default - dockerfile_kwargs = {} - - with open(temp_dir_path / dockerfile_rel_path, "x") as f: - f.write( - f""" - FROM alpine:latest - CMD echo "Hello world!" - """ - ) - with DockerImage(path=temp_directory, tag="test", clean_up=True, no_cache=True, **dockerfile_kwargs) as image: - image_short_id = image.short_id - with DockerContainer(str(image)) as container: - assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch" - assert container.get_logs() == (("Hello world!\n").encode(), b""), "Container logs mismatch" + assert "Hello from Docker".encode() in stdout, "There should be something on stdout" diff --git a/core/tests/test_image.py b/core/tests/test_image.py new file mode 100644 index 00000000..da35eda0 --- /dev/null +++ b/core/tests/test_image.py @@ -0,0 +1,66 @@ +import pytest +import tempfile +import random +import os + +from pathlib import Path +from typing import Optional + +from testcontainers.core.container import DockerContainer +from testcontainers.core.image import DockerImage + + +@pytest.mark.parametrize("test_cleanup", [True, False]) +@pytest.mark.parametrize("test_image_tag", [None, "test-image:latest"]) +def test_docker_image(test_image_tag: Optional[str], test_cleanup: bool, check_for_image) -> None: + with tempfile.TemporaryDirectory() as temp_directory: + # It's important to use a random string to avoid image caching + random_string = "Hello from Docker Image! " + str(random.randint(0, 1000)) + with open(f"{temp_directory}/Dockerfile", "w") as f: + f.write( + f""" + FROM alpine:latest + CMD echo "{random_string}" + """ + ) + with DockerImage(path=temp_directory, tag=test_image_tag, clean_up=test_cleanup) as image: + image_short_id = image.short_id + assert image.tag is test_image_tag, f"Expected {test_image_tag}, got {image.tag}" + assert image.short_id is not None, "Short ID should not be None" + assert image.get_wrapped_image() is not None + logs = image.get_logs() + assert isinstance(logs, list), "Logs should be a list" + assert logs[0] == {"stream": "Step 1/2 : FROM alpine:latest"} + assert logs[3] == {"stream": f'Step 2/2 : CMD echo "{random_string}"'} + with DockerContainer(str(image)) as container: + assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch" + assert container.get_logs() == ((random_string + "\n").encode(), b""), "Container logs mismatch" + + check_for_image(image_short_id, test_cleanup) + + +@pytest.mark.parametrize("dockerfile_path", [None, Path("subdir/my.Dockerfile")]) +def test_docker_image_with_custom_dockerfile_path(dockerfile_path: Optional[Path]) -> None: + with tempfile.TemporaryDirectory() as temp_directory: + temp_dir_path = Path(temp_directory) + if dockerfile_path: + os.makedirs(temp_dir_path / dockerfile_path.parent, exist_ok=True) + dockerfile_rel_path = dockerfile_path + dockerfile_kwargs = {"dockerfile_path": dockerfile_path} + else: + dockerfile_rel_path = Path("Dockerfile") # default + dockerfile_kwargs = {} + + with open(temp_dir_path / dockerfile_rel_path, "x") as f: + f.write( + f""" + FROM alpine:latest + CMD echo "Hello world!" + """ + ) + with DockerImage(path=temp_directory, tag="test", clean_up=True, no_cache=True, **dockerfile_kwargs) as image: + image_short_id = image.short_id + assert image.get_wrapped_image() is not None + with DockerContainer(str(image)) as container: + assert container._container.image.short_id.endswith(image_short_id), "Image ID mismatch" + assert container.get_logs() == (("Hello world!\n").encode(), b""), "Container logs mismatch" diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py new file mode 100644 index 00000000..01134327 --- /dev/null +++ b/core/tests/test_utils.py @@ -0,0 +1,53 @@ +from pytest import MonkeyPatch, raises, mark + +from testcontainers.core import utils + + +def test_setup_logger() -> None: + assert utils.setup_logger("test") is not None + + +@mark.parametrize("platform, expected", [("linux", "linux"), ("linux2", "linux"), ("darwin", "mac"), ("win32", "win")]) +def test_os_name(monkeypatch: MonkeyPatch, platform: str, expected: str) -> None: + assert utils.os_name() is not None + monkeypatch.setattr("sys.platform", platform) + assert utils.os_name() == expected + + +def test_is_mac(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr("testcontainers.core.utils.os_name", lambda: "mac") + assert utils.is_mac() + + +def test_is_linux(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr("testcontainers.core.utils.os_name", lambda: "linux") + assert utils.is_linux() + + +def test_is_windows(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setattr("testcontainers.core.utils.os_name", lambda: "win") + assert utils.is_windows() + + +def test_is_arm(monkeypatch: MonkeyPatch) -> None: + assert not utils.is_arm() + monkeypatch.setattr("platform.machine", lambda: "arm64") + assert utils.is_arm() + monkeypatch.setattr("platform.machine", lambda: "aarch64") + assert utils.is_arm() + + +def test_inside_container(monkeypatch: MonkeyPatch) -> None: + assert not utils.inside_container() + monkeypatch.setattr("os.path.exists", lambda _: True) + assert utils.inside_container() + + +def test_raise_for_deprecated_parameters() -> None: + kwargs = {"key": "value"} + current = "key" + replacement = "new_key" + with raises(ValueError) as e: + result = utils.raise_for_deprecated_parameter(kwargs, current, replacement) + assert str(e.value) == "Parameter 'deprecated' is deprecated and should be replaced by 'replacement'." + assert result == {} diff --git a/core/tests/test_waiting_utils.py b/core/tests/test_waiting_utils.py new file mode 100644 index 00000000..1e684fc4 --- /dev/null +++ b/core/tests/test_waiting_utils.py @@ -0,0 +1,14 @@ +import pytest + +from testcontainers.core.container import DockerContainer +from testcontainers.core.waiting_utils import wait_for_logs + + +def test_wait_for_logs() -> None: + with DockerContainer("hello-world") as container: + wait_for_logs(container, "Hello from Docker!") + + +def test_timeout_is_raised_when_waiting_for_logs() -> None: + with pytest.raises(TimeoutError), DockerContainer("alpine").with_command("sleep 2") as container: + wait_for_logs(container, "Hello from Docker!", timeout=1e-3)