Skip to content

Commit c2cda80

Browse files
committed
Tests demonstrating copy file interfaces
1 parent d40473f commit c2cda80

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed

core/testcontainers/core/container.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import contextlib
2+
import dataclasses
3+
import io
4+
import pathlib
5+
import tarfile
26
from os import PathLike
37
from socket import socket
48
from types import TracebackType
@@ -31,6 +35,13 @@ class Mount(TypedDict):
3135
mode: str
3236

3337

38+
@dataclasses.dataclass
39+
class Transferrable:
40+
source: Union[bytes, PathLike]
41+
destination_in_container: str
42+
mode: int = 0o644
43+
44+
3445
class DockerContainer:
3546
"""
3647
Basic container object to spin up Docker instances.
@@ -69,6 +80,7 @@ def __init__(
6980
volumes: Optional[list[tuple[str, str, str]]] = None,
7081
network: Optional[Network] = None,
7182
network_aliases: Optional[list[str]] = None,
83+
transferrables: Optional[tuple[Transferrable]] = None,
7284
**kwargs: Any,
7385
) -> None:
7486
self.env = env or {}
@@ -96,6 +108,7 @@ def __init__(
96108
self.with_network_aliases(*network_aliases)
97109

98110
self._kwargs = kwargs
111+
self._transferrables = transferrables or []
99112

100113
def with_env(self, key: str, value: str) -> Self:
101114
self.env[key] = value
@@ -196,6 +209,10 @@ def start(self) -> Self:
196209
)
197210

198211
logger.info("Container started: %s", self._container.short_id)
212+
213+
for t in self._transferrables:
214+
self._transfer_into_container(t.source, t.destination_in_container, t.mode)
215+
199216
return self
200217

201218
def stop(self, force: bool = True, delete_volume: bool = True) -> None:
@@ -273,6 +290,43 @@ def _configure(self) -> None:
273290
# placeholder if subclasses want to define this and use the default start method
274291
pass
275292

293+
def with_copy_into_container(
294+
self, file_content: bytes | PathLike, destination_in_container: str, mode: int = 0o644
295+
):
296+
self._transferrables.append(Transferrable(file_content, destination_in_container, mode))
297+
return self
298+
299+
def copy_into_container(self, file_content: bytes, destination_in_container: str, mode: int = 0o644):
300+
return self._transfer_into_container(file_content, destination_in_container, mode)
301+
302+
def _transfer_into_container(self, source: bytes | PathLike, destination_in_container: str, mode: int):
303+
if isinstance(source, bytes):
304+
file_content = source
305+
elif isinstance(source, PathLike):
306+
p = pathlib.Path(source)
307+
file_content = p.read_bytes()
308+
else:
309+
raise TypeError("source must be bytes or PathLike")
310+
311+
fileobj = io.BytesIO()
312+
with tarfile.open(fileobj=fileobj, mode="w") as tar:
313+
tarinfo = tarfile.TarInfo(name=destination_in_container)
314+
tarinfo.size = len(file_content)
315+
tarinfo.mode = mode
316+
tar.addfile(tarinfo, io.BytesIO(file_content))
317+
fileobj.seek(0)
318+
rv = self._container.put_archive(path="/", data=fileobj.getvalue())
319+
assert rv is True
320+
321+
def copy_from_container(self, source_in_container: str, destination_on_host: PathLike):
322+
tar_stream, _ = self._container.get_archive(source_in_container)
323+
324+
for chunk in tar_stream:
325+
with tarfile.open(fileobj=io.BytesIO(chunk)) as tar:
326+
for member in tar.getmembers():
327+
with open(destination_on_host, "wb") as f:
328+
f.write(tar.extractfile(member).read())
329+
276330

277331
class Reaper:
278332
"""

core/tests/test_core.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import tempfile
22
from pathlib import Path
33

4-
from testcontainers.core.container import DockerContainer
4+
from testcontainers.core.container import DockerContainer, Transferrable
55

66

77
def test_garbage_collection_is_defensive():
@@ -46,3 +46,121 @@ def test_docker_container_with_env_file():
4646
assert "[email protected]" in output
4747
assert "ROOT_URL=example.org/app" in output
4848
print(output)
49+
50+
51+
def test_copy_file_into_container_at_runtime(tmp_path: Path):
52+
# Given
53+
my_file = tmp_path / "my_file"
54+
my_file.write_text("hello world")
55+
destination_in_container = "/tmp/my_file"
56+
57+
with DockerContainer("bash", command="sleep infinity") as container:
58+
# When
59+
container.copy_into_container(my_file, destination_in_container)
60+
result = container.exec(f"cat {destination_in_container}")
61+
62+
# Then
63+
assert result.exit_code == 0
64+
assert result.output == b"hello world"
65+
66+
67+
def test_copy_file_into_container_at_startup(tmp_path: Path):
68+
# Given
69+
my_file = tmp_path / "my_file"
70+
my_file.write_text("hello world")
71+
destination_in_container = "/tmp/my_file"
72+
73+
container = DockerContainer("bash", command="sleep infinity")
74+
container.with_copy_into_container(my_file, destination_in_container)
75+
76+
with container:
77+
# When
78+
result = container.exec(f"cat {destination_in_container}")
79+
80+
# Then
81+
assert result.exit_code == 0
82+
assert result.output == b"hello world"
83+
84+
85+
def test_copy_file_into_container_via_initializer(tmp_path: Path):
86+
# Given
87+
my_file = tmp_path / "my_file"
88+
my_file.write_text("hello world")
89+
destination_in_container = "/tmp/my_file"
90+
91+
with DockerContainer(
92+
"bash", command="sleep infinity", transferrables=(Transferrable(my_file, destination_in_container),)
93+
) as container:
94+
# When
95+
result = container.exec(f"cat {destination_in_container}")
96+
97+
# Then
98+
assert result.exit_code == 0
99+
assert result.output == b"hello world"
100+
101+
102+
def test_copy_bytes_to_container_at_runtime():
103+
# Given
104+
file_content = b"hello world"
105+
destination_in_container = "/tmp/my_file"
106+
107+
with DockerContainer("bash", command="sleep infinity") as container:
108+
# When
109+
container.copy_into_container(file_content, destination_in_container)
110+
111+
# Then
112+
result = container.exec(f"cat {destination_in_container}")
113+
114+
assert result.exit_code == 0
115+
assert result.output == b"hello world"
116+
117+
118+
def test_copy_bytes_to_container_at_startup():
119+
# Given
120+
file_content = b"hello world"
121+
destination_in_container = "/tmp/my_file"
122+
123+
container = DockerContainer("bash", command="sleep infinity")
124+
container.with_copy_into_container(file_content, destination_in_container)
125+
126+
with container:
127+
# When
128+
result = container.exec(f"cat {destination_in_container}")
129+
130+
# Then
131+
assert result.exit_code == 0
132+
assert result.output == b"hello world"
133+
134+
135+
def test_copy_bytes_to_container_via_initializer():
136+
# Given
137+
file_content = b"hello world"
138+
destination_in_container = "/tmp/my_file"
139+
140+
with DockerContainer(
141+
"bash", command="sleep infinity", transferrables=(Transferrable(file_content, destination_in_container),)
142+
) as container:
143+
# When
144+
result = container.exec(f"cat {destination_in_container}")
145+
146+
# Then
147+
assert result.exit_code == 0
148+
assert result.output == b"hello world"
149+
150+
151+
def test_copy_file_from_container(tmp_path: Path):
152+
# Given
153+
file_in_container = "/tmp/foo.txt"
154+
destination_on_host = tmp_path / "foo.txt"
155+
assert not destination_on_host.is_file()
156+
157+
with DockerContainer("bash", command="sleep infinity") as container:
158+
result = container.exec(f'bash -c "echo -n hello world > {file_in_container}"')
159+
assert result.exit_code == 0
160+
161+
# When
162+
container.copy_from_container(file_in_container, destination_on_host)
163+
164+
# Then
165+
assert destination_on_host.is_file()
166+
assert destination_on_host.read_text() == "hello world"

0 commit comments

Comments
 (0)