Skip to content

Commit d032a55

Browse files
authored
Merge branch 'main' into copy_file_tests
2 parents f4b9dca + 2ca2321 commit d032a55

23 files changed

+555
-31
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "4.14.1"
2+
".": "4.14.2"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [4.14.2](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.1...testcontainers-v4.14.2) (2026-03-18)
4+
5+
6+
### Features
7+
8+
* **kafka:** allow configurable listener name and security protocol ([#966](https://github.com/testcontainers/testcontainers-python/issues/966)) ([44dd40b](https://github.com/testcontainers/testcontainers-python/commit/44dd40b48c3a5020b487bae5d460124d9e594ac3))
9+
310
## [4.14.1](https://github.com/testcontainers/testcontainers-python/compare/testcontainers-v4.14.0...testcontainers-v4.14.1) (2026-01-31)
411

512

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ build: ## Build the python package
2020

2121
tests: ${TESTS} ## Run tests for each package
2222
${TESTS}: %/tests:
23-
uv run coverage run --parallel -m pytest -v $*/tests
23+
uv run coverage run --parallel -m pytest -v $*/tests
24+
25+
quick-core-tests: ## Run core tests excluding long_running
26+
uv run coverage run --parallel -m pytest -v -m "not long_running" core/tests
2427

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

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

6669

6770
# Implements this pattern for autodocumenting Makefiles:

core/testcontainers/core/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def read_tc_properties() -> dict[str, str]:
8888

8989
@dataclass
9090
class TestcontainersConfiguration:
91+
__test__ = False
92+
9193
def _render_bool(self, env_name: str, prop_name: str) -> bool:
9294
env_val = environ.get(env_name, None)
9395
if env_val is not None:

core/testcontainers/core/container.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def __init__(
8787
for vol in volumes:
8888
self.with_volume_mapping(*vol)
8989

90+
self.tmpfs: dict[str, str] = {}
91+
9092
self.image = image
9193
self._docker = DockerClient(**(docker_client_kw or {}))
9294
self._container: Optional[Container] = None
@@ -208,6 +210,7 @@ def start(self) -> Self:
208210
ports=cast("dict[int, Optional[int]]", self.ports),
209211
name=self._name,
210212
volumes=self.volumes,
213+
tmpfs=self.tmpfs,
211214
**{**network_kwargs, **self._kwargs},
212215
)
213216

@@ -284,6 +287,16 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m
284287
self.volumes[str(host)] = mapping
285288
return self
286289

290+
def with_tmpfs_mount(self, container_path: str, size: Optional[str] = None) -> Self:
291+
"""Mount a tmpfs volume on the container.
292+
293+
:param container_path: Container path to mount tmpfs on (e.g., '/data')
294+
:param size: Optional size limit (e.g., '256m', '1g'). If None, unbounded.
295+
:return: Self for chaining
296+
"""
297+
self.tmpfs[container_path] = size or ""
298+
return self
299+
287300
def get_wrapped_container(self) -> "Container":
288301
return self._container
289302

core/testcontainers/core/generic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
class DbContainer(DockerContainer):
3030
"""
3131
**DEPRECATED (for removal)**
32+
Please use database-specific container classes or `SqlContainer` instead.
33+
# from testcontainers.generic.sql import SqlContainer
3234
3335
Generic database container.
3436
"""

core/tests/test_compose.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import subprocess
23
from pathlib import Path
34
from re import split
@@ -150,7 +151,7 @@ def test_compose_logs():
150151
assert not line or container.Service in next(iter(line.split("|")))
151152

152153

153-
def test_compose_volumes():
154+
def test_compose_volumes(caplog):
154155
_file_in_volume = "/var/lib/example/data/hello"
155156
volumes = DockerCompose(context=FIXTURES / "basic_volume", keep_volumes=True)
156157
with volumes:
@@ -167,8 +168,11 @@ def test_compose_volumes():
167168
assert "hello" in stdout
168169

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

173177

174178
# noinspection HttpUrlsUsage

core/tests/test_docker_in_docker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def test_find_host_network_in_dood() -> None:
179179
assert DockerClient().find_host_network() == os.environ[EXPECTED_NETWORK_VAR]
180180

181181

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

221222

223+
@pytest.mark.long_running
222224
@pytest.mark.skipif(
223225
is_mac(),
224226
reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS",

core/tests/test_ryuk.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from time import sleep
1+
from time import sleep, perf_counter
22
import pytest
33
from pytest import MonkeyPatch
44

@@ -12,6 +12,27 @@
1212
from testcontainers.core.waiting_utils import wait_for_logs
1313

1414

15+
def _wait_for_container_removed(client: DockerClient, container_id: str, timeout: float = 30) -> None:
16+
"""Poll until a container is fully removed (raises NotFound)."""
17+
start = perf_counter()
18+
while perf_counter() - start < timeout:
19+
try:
20+
client.containers.get(container_id)
21+
except NotFound:
22+
return
23+
sleep(0.5)
24+
25+
try:
26+
c = client.containers.get(container_id)
27+
name = c.name
28+
status = c.status
29+
started_at = c.attrs.get("State", {}).get("StartedAt", "unknown")
30+
detail = f"name={name}, status={status}, started_at={started_at}"
31+
except NotFound:
32+
detail = "container disappeared just after timeout"
33+
raise TimeoutError(f"Container {container_id} was not removed within {timeout}s ({detail})")
34+
35+
1536
@pytest.mark.skipif(
1637
is_mac(),
1738
reason="Ryuk container reaping is unreliable on Docker Desktop for macOS due to VM-based container lifecycle handling",
@@ -39,8 +60,11 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch):
3960
assert rs
4061
rs.close()
4162

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

67+
# Verify both containers were reaped
4468
with pytest.raises(NotFound):
4569
docker_client.containers.get(container_id)
4670
with pytest.raises(NotFound):

core/tests/test_wait_strategies.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import re
23
import time
34
from datetime import timedelta
@@ -528,7 +529,7 @@ def test_file_exists_wait_strategy_initialization(self, file_path):
528529
@patch("pathlib.Path.is_file")
529530
@patch("time.time")
530531
@patch("time.sleep")
531-
def test_wait_until_ready(self, mock_sleep, mock_time, mock_is_file, file_exists, expected_behavior):
532+
def test_wait_until_ready(self, mock_sleep, mock_time, mock_is_file, file_exists, expected_behavior, caplog):
532533
strategy = FileExistsWaitStrategy("/tmp/test.txt").with_startup_timeout(1)
533534
mock_container = Mock()
534535

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

552554

553555
class TestCompositeWaitStrategy:
@@ -615,7 +617,7 @@ def test_wait_until_ready_all_strategies_succeed(self):
615617
strategy2.wait_until_ready.assert_called_once_with(mock_container)
616618
strategy3.wait_until_ready.assert_called_once_with(mock_container)
617619

618-
def test_wait_until_ready_first_strategy_fails(self):
620+
def test_wait_until_ready_first_strategy_fails(self, caplog):
619621
"""Test that execution stops when first strategy fails."""
620622
strategy1 = Mock()
621623
strategy2 = Mock()
@@ -628,7 +630,8 @@ def test_wait_until_ready_first_strategy_fails(self):
628630
strategy1.wait_until_ready.side_effect = TimeoutError("First strategy failed")
629631

630632
with pytest.raises(TimeoutError, match="First strategy failed"):
631-
composite.wait_until_ready(mock_container)
633+
with caplog.at_level(logging.CRITICAL, logger="testcontainers.core.wait_strategies"):
634+
composite.wait_until_ready(mock_container)
632635

633636
# Only first strategy should be called
634637
strategy1.wait_until_ready.assert_called_once_with(mock_container)

0 commit comments

Comments
 (0)