Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: flaky garbage collection resulting in testing errors #423

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions core/testcontainers/core/container.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Iterable, Optional, Tuple
from typing import Optional, Tuple

from docker.models.containers import Container

Expand All @@ -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 = {}
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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)
16 changes: 12 additions & 4 deletions core/testcontainers/core/docker_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion core/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading