diff --git a/core/testcontainers/core/container.py b/core/testcontainers/core/container.py index c3825b93..4caed7e3 100644 --- a/core/testcontainers/core/container.py +++ b/core/testcontainers/core/container.py @@ -1,5 +1,5 @@ import os -from typing import Iterable, Optional, Tuple +from typing import Optional, Tuple from docker.models.containers import Container @@ -23,6 +23,7 @@ class DockerContainer: >>> with DockerContainer("hello-world") as container: ... delay = wait_for_logs(container, "Hello from Docker!") """ + def __init__(self, image: str, docker_client_kw: Optional[dict] = None, **kwargs) -> None: self.env = {} self.ports = {} @@ -42,7 +43,7 @@ def with_bind_ports(self, container: int, host: int = None) -> 'DockerContainer' self.ports[container] = host return self - def with_exposed_ports(self, *ports: Iterable[int]) -> 'DockerContainer': + def with_exposed_ports(self, *ports: int) -> 'DockerContainer': for port in ports: self.ports[port] = None return self @@ -67,7 +68,7 @@ def start(self) -> 'DockerContainer': return self def stop(self, force=True, delete_volume=True) -> None: - self.get_wrapped_container().remove(force=force, v=delete_volume) + self._container.remove(force=force, v=delete_volume) def __enter__(self) -> 'DockerContainer': return self.start() @@ -77,13 +78,14 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None: def __del__(self) -> None: """ - Try to remove the container in all circumstances + __del__ runs when Python attempts to garbage collect the object. + In case of leaky test design, we still attempt to clean up the container. """ - if self._container is not None: - try: + try: + if self._container is not None: self.stop() - except: # noqa: E722 - pass + finally: + pass def get_container_host_ip(self) -> str: # infer from docker host @@ -143,4 +145,4 @@ def get_logs(self) -> Tuple[str, str]: def exec(self, command) -> Tuple[int, str]: if not self._container: raise ContainerStartException("Container should be started before executing a command") - return self.get_wrapped_container().exec_run(command) + return self._container.exec_run(command) diff --git a/core/testcontainers/core/docker_client.py b/core/testcontainers/core/docker_client.py index 228bfd1c..fb54838f 100644 --- a/core/testcontainers/core/docker_client.py +++ b/core/testcontainers/core/docker_client.py @@ -39,14 +39,22 @@ class DockerClient: """ Thin wrapper around :class:`docker.DockerClient` for a more functional interface. """ + def __init__(self, **kwargs) -> None: self.client = docker.from_env(**kwargs) @ft.wraps(ContainerCollection.run) - def run(self, image: str, command: Union[str, List[str]] = None, - environment: Optional[dict] = None, ports: Optional[dict] = None, - detach: bool = False, stdout: bool = True, stderr: bool = False, remove: bool = False, - **kwargs) -> Container: + def run( + self, image: str, + command: Union[str, List[str]] = None, + environment: Optional[dict] = None, + ports: Optional[dict] = None, + detach: bool = False, + stdout: bool = True, + stderr: bool = False, + remove: bool = False, + **kwargs + ) -> Container: container = self.client.containers.run( image, command=command, stdout=stdout, stderr=stderr, remove=remove, detach=detach, environment=environment, ports=ports, **kwargs diff --git a/core/tests/test_core.py b/core/tests/test_core.py index 5a666350..01e9c97c 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -4,12 +4,22 @@ from testcontainers.core.waiting_utils import wait_for_logs -def test_raise_timeout(): +def test_timeout_is_raised_when_waiting_for_logs(): with pytest.raises(TimeoutError): with DockerContainer("alpine").with_command("sleep 2") as container: wait_for_logs(container, "Hello from Docker!", timeout=1e-3) +def test_garbage_collection_is_defensive(): + # For more info, see https://github.com/testcontainers/testcontainers-python/issues/399 + # we simulate garbage collection: start, stop, then call `del` + container = DockerContainer("postgres:latest") + container.start() + container.stop(force=True, delete_volume=True) + delattr(container, "_container") + del container + + def test_wait_for_hello(): with DockerContainer("hello-world") as container: wait_for_logs(container, "Hello from Docker!") diff --git a/pyproject.toml b/pyproject.toml index 9bc87dfe..9139e3b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ priority = "primary" line-length = 120 [tool.pytest.ini_options] -addopts = "--cov-report=term --tb=short --strict-markers" +addopts = "--cov-report=term --cov-report=html --tb=short --strict-markers" log_cli = true log_cli_level = "INFO"