Skip to content
Open
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
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ build: ## Build the python package

tests: ${TESTS} ## Run tests for each package
${TESTS}: %/tests:
uv run coverage run --parallel -m pytest -v $*/tests
uv run coverage run --parallel -m pytest -v $*/tests

quick-core-tests: ## Run core tests excluding long_running
uv run coverage run --parallel -m pytest -v -m "not long_running" core/tests

coverage: ## Target to combine and report coverage.
uv run coverage combine
Expand Down Expand Up @@ -61,7 +64,7 @@ clean-all: clean ## Remove all generated files and reset the local virtual envir
rm -rf .venv

# Targets that do not generate file-level artifacts.
.PHONY: clean docs doctests image tests ${TESTS}
.PHONY: clean docs doctests image tests quick-core-tests ${TESTS}


# Implements this pattern for autodocumenting Makefiles:
Expand Down
2 changes: 2 additions & 0 deletions core/testcontainers/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def read_tc_properties() -> dict[str, str]:

@dataclass
class TestcontainersConfiguration:
__test__ = False

def _render_bool(self, env_name: str, prop_name: str) -> bool:
env_val = environ.get(env_name, None)
if env_val is not None:
Expand Down
10 changes: 7 additions & 3 deletions core/tests/test_compose.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import subprocess
from pathlib import Path
from re import split
Expand Down Expand Up @@ -150,7 +151,7 @@ def test_compose_logs():
assert not line or container.Service in next(iter(line.split("|")))


def test_compose_volumes():
def test_compose_volumes(caplog):
_file_in_volume = "/var/lib/example/data/hello"
volumes = DockerCompose(context=FIXTURES / "basic_volume", keep_volumes=True)
with volumes:
Expand All @@ -167,8 +168,11 @@ def test_compose_volumes():
assert "hello" in stdout

# third time we expect the file to be missing
with volumes, pytest.raises(subprocess.CalledProcessError):
volumes.exec_in_container(["cat", _file_in_volume], "alpine")
with caplog.at_level(
logging.CRITICAL, logger="testcontainers.compose.compose"
): # suppress expected error logs about missing volume
with volumes, pytest.raises(subprocess.CalledProcessError):
volumes.exec_in_container(["cat", _file_in_volume], "alpine")


# noinspection HttpUrlsUsage
Expand Down
2 changes: 2 additions & 0 deletions core/tests/test_docker_in_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def test_find_host_network_in_dood() -> None:
assert DockerClient().find_host_network() == os.environ[EXPECTED_NETWORK_VAR]


@pytest.mark.long_running
@pytest.mark.skipif(
is_mac(),
reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS",
Expand Down Expand Up @@ -219,6 +220,7 @@ def test_dood(python_testcontainer_image: str) -> None:
assert status["StatusCode"] == 0


@pytest.mark.long_running
@pytest.mark.skipif(
is_mac(),
reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS",
Expand Down
28 changes: 26 additions & 2 deletions core/tests/test_ryuk.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from time import sleep
from time import sleep, perf_counter
import pytest
from pytest import MonkeyPatch

Expand All @@ -12,6 +12,27 @@
from testcontainers.core.waiting_utils import wait_for_logs


def _wait_for_container_removed(client: DockerClient, container_id: str, timeout: float = 30) -> None:
"""Poll until a container is fully removed (raises NotFound)."""
start = perf_counter()
while perf_counter() - start < timeout:
try:
client.containers.get(container_id)
except NotFound:
return
sleep(0.5)

try:
c = client.containers.get(container_id)
name = c.name
status = c.status
started_at = c.attrs.get("State", {}).get("StartedAt", "unknown")
detail = f"name={name}, status={status}, started_at={started_at}"
except NotFound:
detail = "container disappeared just after timeout"
raise TimeoutError(f"Container {container_id} was not removed within {timeout}s ({detail})")


@pytest.mark.skipif(
is_mac(),
reason="Ryuk container reaping is unreliable on Docker Desktop for macOS due to VM-based container lifecycle handling",
Expand Down Expand Up @@ -39,8 +60,11 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch):
assert rs
rs.close()

sleep(0.6) # Sleep until Ryuk reaps all dangling containers. 0.5 extra seconds for good measure.
# Ryuk will reap containers then auto-remove itself.
# Wait for the reaper container to disappear and once it's gone, all labeled containers are guaranteed reaped.
_wait_for_container_removed(docker_client, reaper_id)

# Verify both containers were reaped
with pytest.raises(NotFound):
docker_client.containers.get(container_id)
with pytest.raises(NotFound):
Expand Down
11 changes: 7 additions & 4 deletions core/tests/test_wait_strategies.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import re
import time
from datetime import timedelta
Expand Down Expand Up @@ -528,7 +529,7 @@ def test_file_exists_wait_strategy_initialization(self, file_path):
@patch("pathlib.Path.is_file")
@patch("time.time")
@patch("time.sleep")
def test_wait_until_ready(self, mock_sleep, mock_time, mock_is_file, file_exists, expected_behavior):
def test_wait_until_ready(self, mock_sleep, mock_time, mock_is_file, file_exists, expected_behavior, caplog):
strategy = FileExistsWaitStrategy("/tmp/test.txt").with_startup_timeout(1)
mock_container = Mock()

Expand All @@ -547,7 +548,8 @@ def test_wait_until_ready(self, mock_sleep, mock_time, mock_is_file, file_exists
mock_is_file.assert_called()
else:
with pytest.raises(TimeoutError, match="File.*did not exist within.*seconds"):
strategy.wait_until_ready(mock_container)
with caplog.at_level(logging.CRITICAL, logger="testcontainers.core.wait_strategies"):
strategy.wait_until_ready(mock_container)


class TestCompositeWaitStrategy:
Expand Down Expand Up @@ -615,7 +617,7 @@ def test_wait_until_ready_all_strategies_succeed(self):
strategy2.wait_until_ready.assert_called_once_with(mock_container)
strategy3.wait_until_ready.assert_called_once_with(mock_container)

def test_wait_until_ready_first_strategy_fails(self):
def test_wait_until_ready_first_strategy_fails(self, caplog):
"""Test that execution stops when first strategy fails."""
strategy1 = Mock()
strategy2 = Mock()
Expand All @@ -628,7 +630,8 @@ def test_wait_until_ready_first_strategy_fails(self):
strategy1.wait_until_ready.side_effect = TimeoutError("First strategy failed")

with pytest.raises(TimeoutError, match="First strategy failed"):
composite.wait_until_ready(mock_container)
with caplog.at_level(logging.CRITICAL, logger="testcontainers.core.wait_strategies"):
composite.wait_until_ready(mock_container)

# Only first strategy should be called
strategy1.wait_until_ready.assert_called_once_with(mock_container)
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ rabbitmq = ["pika>=1"]
redis = ["redis>=7"]
registry = ["bcrypt>=5"]
selenium = ["selenium>=4"]
scylla = ["cassandra-driver>=3"]
scylla = ["cassandra-driver>=3; python_version < '3.14'"]
sftp = ["cryptography"]
vault = []
weaviate = ["weaviate-client>=4"]
Expand All @@ -120,7 +120,7 @@ test = [
"psycopg2-binary==2.9.11",
"pg8000==1.31.5",
"psycopg>=3",
"cassandra-driver>=3",
"cassandra-driver>=3; python_version < '3.14'",
"kafka-python-ng>=2",
"hvac>=2; python_version < '4.0'",
"pymilvus>=2",
Expand Down Expand Up @@ -277,6 +277,7 @@ log_cli = true
log_cli_level = "INFO"
markers = [
"inside_docker_check: mark test to be used to validate DinD/DooD is working as expected",
"long_running: mark test as very long running",
]
filterwarnings = [
"ignore:The @wait_container_is_ready decorator is deprecated.*:DeprecationWarning",
Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading