Skip to content

Commit e365dc1

Browse files
authored
Merge branch 'main' into fix_test_core_registry
2 parents aefa89b + 6b11268 commit e365dc1

File tree

20 files changed

+358
-37
lines changed

20 files changed

+358
-37
lines changed

.github/workflows/ci-core.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Contrinuous Integration for the core package
1+
# Continuous Integration for the core package
22

33
name: core
44

@@ -25,6 +25,8 @@ jobs:
2525
run: poetry install --all-extras
2626
- name: Run twine check
2727
run: poetry build && poetry run twine check dist/*.tar.gz
28+
- name: Set up Docker
29+
uses: docker/setup-docker-action@v4
2830
- name: Run tests
2931
run: make core/tests
3032
- name: Rename coverage file

core/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Testcontainers Core
1818

1919
.. autoclass:: testcontainers.core.generic.DbContainer
2020

21+
.. autoclass:: testcontainers.core.network.Network
22+
2123
.. raw:: html
2224

2325
<hr>

core/testcontainers/core/container.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ class DockerContainer:
2828
"""
2929
Basic container object to spin up Docker instances.
3030
31+
Args:
32+
image: The name of the image to start.
33+
docker_client_kw: Dictionary with arguments that will be passed to the
34+
docker.DockerClient init.
35+
command: Optional execution command for the container.
36+
name: Optional name for the container.
37+
ports: Ports to be exposed by the container. The port number will be
38+
automatically assigned on the host, use
39+
:code:`get_exposed_port(PORT)` method to get the port number on the host.
40+
volumes: Volumes to mount into the container. Each entry should be a tuple with
41+
three values: host path, container path and. mode (default 'ro').
42+
network: Optional network to connect the container to.
43+
network_aliases: Optional list of aliases for the container in the network.
44+
3145
.. doctest::
3246
3347
>>> from testcontainers.core.container import DockerContainer
@@ -41,18 +55,40 @@ def __init__(
4155
self,
4256
image: str,
4357
docker_client_kw: Optional[dict] = None,
58+
command: Optional[str] = None,
59+
env: Optional[dict[str, str]] = None,
60+
name: Optional[str] = None,
61+
ports: Optional[list[int]] = None,
62+
volumes: Optional[list[tuple[str, str, str]]] = None,
63+
network: Optional[Network] = None,
64+
network_aliases: Optional[list[str]] = None,
4465
**kwargs,
4566
) -> None:
46-
self.env = {}
67+
self.env = env or {}
68+
4769
self.ports = {}
70+
if ports:
71+
self.with_exposed_ports(*ports)
72+
4873
self.volumes = {}
74+
if volumes:
75+
for vol in volumes:
76+
self.with_volume_mapping(*vol)
77+
4978
self.image = image
5079
self._docker = DockerClient(**(docker_client_kw or {}))
5180
self._container = None
52-
self._command = None
53-
self._name = None
81+
self._command = command
82+
self._name = name
83+
5484
self._network: Optional[Network] = None
85+
if network is not None:
86+
self.with_network(network)
87+
5588
self._network_aliases: Optional[list[str]] = None
89+
if network_aliases:
90+
self.with_network_aliases(*network_aliases)
91+
5692
self._kwargs = kwargs
5793

5894
def with_env(self, key: str, value: str) -> Self:

core/testcontainers/core/docker_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def find_host_network(self) -> Optional[str]:
151151
except ipaddress.AddressValueError:
152152
continue
153153
if docker_host in subnet:
154-
return cast(str, network.name)
154+
return cast("str", network.name)
155155
except (ipaddress.AddressValueError, OSError):
156156
pass
157157
return None
@@ -163,7 +163,7 @@ def port(self, container_id: str, port: int) -> str:
163163
port_mappings = self.client.api.port(container_id, port)
164164
if not port_mappings:
165165
raise ConnectionError(f"Port mapping for container {container_id} and port {port} is not available")
166-
return cast(str, port_mappings[0]["HostPort"])
166+
return cast("str", port_mappings[0]["HostPort"])
167167

168168
def get_container(self, container_id: str) -> dict[str, Any]:
169169
"""
@@ -172,7 +172,7 @@ def get_container(self, container_id: str) -> dict[str, Any]:
172172
containers = self.client.api.containers(filters={"id": container_id})
173173
if not containers:
174174
raise RuntimeError(f"Could not get container with id {container_id}")
175-
return cast(dict[str, Any], containers[0])
175+
return cast("dict[str, Any]", containers[0])
176176

177177
def bridge_ip(self, container_id: str) -> str:
178178
"""
@@ -241,7 +241,7 @@ def host(self) -> str:
241241
hostname = url.hostname
242242
if not hostname or (hostname == "localnpipe" and utils.is_windows()):
243243
return "localhost"
244-
return cast(str, url.hostname)
244+
return cast("str", url.hostname)
245245
if utils.inside_container() and ("unix" in url.scheme or "npipe" in url.scheme):
246246
ip_address = utils.default_gateway_ip()
247247
if ip_address:
@@ -257,7 +257,7 @@ def login(self, auth_config: DockerAuthInfo) -> None:
257257

258258
def client_networks_create(self, name: str, param: dict[str, Any]) -> dict[str, Any]:
259259
labels = create_labels("", param.get("labels"))
260-
return cast(dict[str, Any], self.client.networks.create(name, **{**param, "labels": labels}))
260+
return cast("dict[str, Any]", self.client.networks.create(name, **{**param, "labels": labels}))
261261

262262

263263
def get_docker_host() -> Optional[str]:

core/tests/compose_fixtures/port_multiple/compose.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ services:
66
- '81'
77
- '82'
88
- target: 80
9+
published: "5000-5999"
910
host_ip: 127.0.0.1
1011
protocol: tcp
1112
command:
@@ -18,6 +19,7 @@ services:
1819
init: true
1920
ports:
2021
- target: 80
22+
published: "5000-5999"
2123
host_ip: 127.0.0.1
2224
protocol: tcp
2325
command:

core/tests/test_container.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,24 @@ def test_get_exposed_port_original(container: DockerContainer, monkeypatch: pyte
7575
monkeypatch.setattr(client, "get_connection_mode", lambda: ConnectionMode.bridge_ip)
7676

7777
assert container.get_exposed_port(8080) == 8080
78+
79+
80+
@pytest.mark.parametrize(
81+
"init_attr,init_value,class_attr,stored_value",
82+
[
83+
("command", "ps", "_command", "ps"),
84+
("env", {"e1": "v1"}, "env", {"e1": "v1"}),
85+
("name", "foo-bar", "_name", "foo-bar"),
86+
("ports", [22, 80], "ports", {22: None, 80: None}),
87+
(
88+
"volumes",
89+
[("/tmp", "/tmp2", "ro")],
90+
"volumes",
91+
{"/tmp": {"bind": "/tmp2", "mode": "ro"}},
92+
),
93+
],
94+
)
95+
def test_attribute(init_attr, init_value, class_attr, stored_value):
96+
"""Test that the attributes set through the __init__ function are properly stored."""
97+
with DockerContainer("ubuntu", **{init_attr: init_value}) as container:
98+
assert getattr(container, class_attr) == stored_value

core/tests/test_core_registry.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@
1818
from testcontainers.core.waiting_utils import wait_for_logs
1919

2020
from testcontainers.registry import DockerRegistryContainer
21+
from testcontainers.core.utils import is_mac
2122

2223

24+
@pytest.mark.skipif(
25+
is_mac(),
26+
reason="Docker Desktop on macOS does not support insecure private registries without daemon reconfiguration",
27+
)
2328
def test_missing_on_private_registry(monkeypatch):
2429
username = "user"
2530
password = "pass"
@@ -41,6 +46,10 @@ def test_missing_on_private_registry(monkeypatch):
4146
wait_for_logs(test_container, "Hello from Docker!")
4247

4348

49+
@pytest.mark.skipif(
50+
is_mac(),
51+
reason="Docker Desktop on macOS does not support local insecure registries over HTTP without modifying daemon settings",
52+
)
4453
@pytest.mark.parametrize(
4554
"image,tag,username,password,expected_output",
4655
[

core/tests/test_docker_in_docker.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from testcontainers.core.container import DockerContainer
1616
from testcontainers.core.docker_client import DockerClient, LOGGER
1717
from testcontainers.core.utils import inside_container
18+
from testcontainers.core.utils import is_mac
1819
from testcontainers.core.waiting_utils import wait_for_logs
1920

2021

@@ -36,6 +37,7 @@ def _wait_for_dind_return_ip(client, dind):
3637
return docker_host_ip
3738

3839

40+
@pytest.mark.skipif(is_mac(), reason="Docker socket forwarding (socat) is unsupported on Docker Desktop for macOS")
3941
def test_wait_for_logs_docker_in_docker():
4042
# real dind isn't possible (AFAIK) in CI
4143
# forwarding the socket to a container port is at least somewhat the same
@@ -64,6 +66,9 @@ def test_wait_for_logs_docker_in_docker():
6466
not_really_dind.remove()
6567

6668

69+
@pytest.mark.skipif(
70+
is_mac(), reason="Bridge networking and Docker socket forwarding are not supported on Docker Desktop for macOS"
71+
)
6772
def test_dind_inherits_network():
6873
client = DockerClient()
6974
try:
@@ -158,6 +163,9 @@ def test_find_host_network_in_dood() -> None:
158163
assert DockerClient().find_host_network() == os.environ[EXPECTED_NETWORK_VAR]
159164

160165

166+
@pytest.mark.skipif(
167+
is_mac(), reason="Docker socket mounting and container networking do not work reliably on Docker Desktop for macOS"
168+
)
161169
@pytest.mark.skipif(not Path(tcc.ryuk_docker_socket).exists(), reason="No docker socket available")
162170
def test_dood(python_testcontainer_image: str) -> None:
163171
"""

core/tests/test_ryuk.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
from testcontainers.core.config import testcontainers_config
99
from testcontainers.core.container import Reaper
1010
from testcontainers.core.container import DockerContainer
11+
from testcontainers.core.utils import is_mac
1112
from testcontainers.core.waiting_utils import wait_for_logs
1213

1314

15+
@pytest.mark.skipif(
16+
is_mac(),
17+
reason="Ryuk container reaping is unreliable on Docker Desktop for macOS due to VM-based container lifecycle handling",
18+
)
1419
@pytest.mark.inside_docker_check
1520
def test_wait_for_reaper(monkeypatch: MonkeyPatch):
1621
Reaper.delete_instance()
@@ -41,6 +46,9 @@ def test_wait_for_reaper(monkeypatch: MonkeyPatch):
4146
Reaper.delete_instance()
4247

4348

49+
@pytest.mark.skipif(
50+
is_mac(), reason="Ryuk disabling behavior is unreliable on Docker Desktop for macOS due to Docker socket emulation"
51+
)
4452
@pytest.mark.inside_docker_check
4553
def test_container_without_ryuk(monkeypatch: MonkeyPatch):
4654
Reaper.delete_instance()

core/tests/test_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_is_windows(monkeypatch: MonkeyPatch) -> None:
3333

3434

3535
def test_is_arm(monkeypatch: MonkeyPatch) -> None:
36+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
3637
assert not utils.is_arm()
3738
monkeypatch.setattr("platform.machine", lambda: "arm64")
3839
assert utils.is_arm()

0 commit comments

Comments
 (0)