Skip to content

Commit 3e3c77e

Browse files
committed
feat(core): Added SrvContainer
Added doctest to SrcContainer and update readme Fix issue with SrcContainer image handle Add test for SrcContainer Fixed doctest Improve SrvContainer Fix test_srv_container logs check Improve SrvContainer and update tests Updates for SrvContainer
1 parent ec76df2 commit 3e3c77e

File tree

4 files changed

+99
-0
lines changed

4 files changed

+99
-0
lines changed

core/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ testcontainers-core
77

88
.. autoclass:: testcontainers.core.image.DockerImage
99

10+
.. autoclass:: testcontainers.core.generic.SrvContainer
11+
1012
Using `DockerContainer` and `DockerImage` directly:
1113

1214
.. doctest::

core/testcontainers/core/generic.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
1313
from typing import Optional
14+
from urllib.error import HTTPError
1415
from urllib.parse import quote
16+
from urllib.request import urlopen
1517

1618
from testcontainers.core.container import DockerContainer
1719
from testcontainers.core.exceptions import ContainerStartException
20+
from testcontainers.core.image import DockerImage
1821
from testcontainers.core.utils import raise_for_deprecated_parameter
1922
from testcontainers.core.waiting_utils import wait_container_is_ready
2023

@@ -79,3 +82,67 @@ def _configure(self) -> None:
7982

8083
def _transfer_seed(self) -> None:
8184
pass
85+
86+
87+
class SrvContainer(DockerContainer):
88+
"""
89+
Container for a generic server that is based on a custom image.
90+
91+
Example:
92+
93+
.. doctest::
94+
95+
>>> import httpx
96+
>>> from testcontainers.core.generic import SrvContainer
97+
>>> from testcontainers.core.waiting_utils import wait_for_logs
98+
99+
>>> with SrvContainer(path="./core/tests/image_fixtures/python_server", port=9000, tag="test-srv:latest") as srv:
100+
... url = srv._create_connection_url()
101+
... response = httpx.get(f"{url}", timeout=5)
102+
... assert response.status_code == 200, "Response status code is not 200"
103+
... delay = wait_for_logs(srv, "GET / HTTP/1.1")
104+
105+
106+
:param path: Path to the Dockerfile to build the image
107+
:param tag: Tag for the image to be built (default: None)
108+
"""
109+
110+
def __init__(self, path: str, port: int, tag: Optional[str], image_cleanup: bool = True) -> None:
111+
self.docker_image = DockerImage(path=path, tag=tag, clean_up=image_cleanup).build()
112+
super().__init__(str(self.docker_image))
113+
self.internal_port = port
114+
self.with_exposed_ports(self.internal_port)
115+
116+
@wait_container_is_ready(HTTPError)
117+
def _connect(self) -> None:
118+
# noinspection HttpUrlsUsage
119+
url = self._create_connection_url()
120+
try:
121+
with urlopen(url) as r:
122+
assert b"" in r.read()
123+
except HTTPError as e:
124+
# 404 is expected, as the server may not have the specific endpoint we are looking for
125+
if e.code == 404:
126+
pass
127+
else:
128+
raise
129+
130+
def get_api_url(self) -> str:
131+
raise NotImplementedError
132+
133+
def _create_connection_url(self) -> str:
134+
if self._container is None:
135+
raise ContainerStartException("container has not been started")
136+
host = self.get_container_host_ip()
137+
exposed_port = self.get_exposed_port(self.internal_port)
138+
url = f"http://{host}:{exposed_port}"
139+
return url
140+
141+
def start(self) -> "SrvContainer":
142+
super().start()
143+
self._connect()
144+
return self
145+
146+
def stop(self, force=True, delete_volume=True) -> None:
147+
super().stop(force, delete_volume)
148+
self.docker_image.remove()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM python:3
2+
EXPOSE 9000
3+
CMD ["python", "-m", "http.server", "9000"]

core/tests/test_generics.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
from typing import Optional
3+
from testcontainers.core.generic import SrvContainer
4+
5+
import re
6+
7+
8+
@pytest.mark.parametrize("test_image_cleanup", [True, False])
9+
@pytest.mark.parametrize("test_image_tag", [None, "custom-image:test"])
10+
def test_srv_container(test_image_tag: Optional[str], test_image_cleanup: bool, check_for_image, port=9000):
11+
with SrvContainer(
12+
path="./core/tests/image_fixtures/python_server",
13+
port=port,
14+
tag=test_image_tag,
15+
image_cleanup=test_image_cleanup,
16+
) as srv:
17+
image_short_id = srv.docker_image.short_id
18+
image_build_logs = srv.docker_image.get_logs()
19+
# check if dict is in any of the logs
20+
assert {"stream": f"Step 2/3 : EXPOSE {port}"} in image_build_logs, "Image logs mismatch"
21+
assert (port, None) in srv.ports.items(), "Port mismatch"
22+
with pytest.raises(NotImplementedError):
23+
srv.get_api_url()
24+
test_url = srv._create_connection_url()
25+
assert re.match(r"http://localhost:\d+", test_url), "Connection URL mismatch"
26+
27+
check_for_image(image_short_id, test_image_cleanup)

0 commit comments

Comments
 (0)