robotframework-testcontainers 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robotframework_testcontainers-0.1.0 → robotframework_testcontainers-0.2.0}/PKG-INFO +12 -2
- {robotframework_testcontainers-0.1.0 → robotframework_testcontainers-0.2.0}/README.md +2 -0
- {robotframework_testcontainers-0.1.0 → robotframework_testcontainers-0.2.0}/pyproject.toml +23 -1
- robotframework_testcontainers-0.2.0/src/TestcontainersLibrary/library.py +232 -0
- robotframework_testcontainers-0.1.0/src/TestcontainersLibrary/library.py +0 -111
- {robotframework_testcontainers-0.1.0 → robotframework_testcontainers-0.2.0}/src/TestcontainersLibrary/__init__.py +0 -0
- {robotframework_testcontainers-0.1.0 → robotframework_testcontainers-0.2.0}/src/TestcontainersLibrary/py.typed +0 -0
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-testcontainers
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Robot Framework keywords for Testcontainers.
|
|
5
|
+
Keywords: robotframework,testing,test,automation,container,testcontainers,docker
|
|
5
6
|
Author: Andreas Finkler
|
|
6
7
|
Author-email: Andreas Finkler <andi.finkler@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Framework :: Robot Framework
|
|
10
|
+
Classifier: Framework :: Robot Framework :: Library
|
|
11
|
+
Classifier: Topic :: Software Development :: Testing
|
|
12
|
+
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
7
13
|
Requires-Dist: robotframework>=7.3.2
|
|
8
14
|
Requires-Dist: testcontainers[generic]>=4.13.2
|
|
9
15
|
Requires-Python: >=3.10
|
|
16
|
+
Project-URL: Documentation, https://dudenr33.github.io/robotframework-testcontainers/
|
|
17
|
+
Project-URL: Repository, https://github.com/DudeNr33/robotframework-testcontainers
|
|
10
18
|
Description-Content-Type: text/markdown
|
|
11
19
|
|
|
12
20
|
# robotframework-testcontainers
|
|
13
21
|
|
|
14
22
|
Robot Framework keywords for [testcontainers](https://github.com/testcontainers/testcontainers-python).
|
|
15
23
|
|
|
24
|
+
[Keyword Documentation](https://dudenr33.github.io/robotframework-testcontainers/)
|
|
25
|
+
|
|
16
26
|
## Installation
|
|
17
27
|
|
|
18
28
|
- using `pip`:
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "robotframework-testcontainers"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Robot Framework keywords for Testcontainers."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Andreas Finkler", email = "andi.finkler@gmail.com" }]
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-file = "LICENSE"
|
|
9
|
+
keywords = [
|
|
10
|
+
"robotframework",
|
|
11
|
+
"testing",
|
|
12
|
+
"test",
|
|
13
|
+
"automation",
|
|
14
|
+
"container",
|
|
15
|
+
"testcontainers",
|
|
16
|
+
"docker",
|
|
17
|
+
]
|
|
7
18
|
requires-python = ">=3.10"
|
|
8
19
|
dependencies = ["robotframework>=7.3.2", "testcontainers[generic]>=4.13.2"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Framework :: Robot Framework",
|
|
22
|
+
"Framework :: Robot Framework :: Library",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
"Topic :: Software Development :: Testing :: Mocking",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Repository = "https://github.com/DudeNr33/robotframework-testcontainers"
|
|
29
|
+
Documentation = "https://dudenr33.github.io/robotframework-testcontainers/"
|
|
9
30
|
|
|
10
31
|
[build-system]
|
|
11
32
|
requires = ["uv_build>=0.9.5,<0.10.0"]
|
|
@@ -27,3 +48,4 @@ dev = [
|
|
|
27
48
|
|
|
28
49
|
[tool.mypy]
|
|
29
50
|
ignore_missing_imports = true
|
|
51
|
+
strict = true
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from robot.api import logger
|
|
6
|
+
from robot.api.deco import keyword, library
|
|
7
|
+
from testcontainers.core.container import DockerContainer
|
|
8
|
+
from testcontainers.core.network import Network
|
|
9
|
+
from testcontainers.core.wait_strategies import HttpWaitStrategy, LogMessageWaitStrategy
|
|
10
|
+
from testcontainers.generic.server import ServerContainer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@library(listener="SELF")
|
|
14
|
+
class TestcontainersLibrary:
|
|
15
|
+
"""
|
|
16
|
+
Keywords for [https://testcontainers.com/|Testcontainers].
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._containers: list[DockerContainer] = []
|
|
21
|
+
self._networks: list[Network] = []
|
|
22
|
+
|
|
23
|
+
@keyword
|
|
24
|
+
def create_docker_container(
|
|
25
|
+
self,
|
|
26
|
+
image: str,
|
|
27
|
+
command: str | None = None,
|
|
28
|
+
env: dict[str, str] | None = None,
|
|
29
|
+
name: str | None = None,
|
|
30
|
+
ports: list[int] | None = None,
|
|
31
|
+
volumes: list[tuple[Path, str, str]] | None = None,
|
|
32
|
+
network: Network | None = None,
|
|
33
|
+
network_aliases: list[str] | None = None,
|
|
34
|
+
start: bool = True,
|
|
35
|
+
) -> DockerContainer:
|
|
36
|
+
"""
|
|
37
|
+
Create a basic [https://testcontainers-python.readthedocs.io/en/latest/core/README.html#testcontainers.core.container.DockerContainer|DockerContainer].
|
|
38
|
+
Returns the container instance.
|
|
39
|
+
|
|
40
|
+
By default, the created container is started directly.
|
|
41
|
+
|
|
42
|
+
This can be prevented by setting ``start=False``, e.g. if
|
|
43
|
+
you need to customize the container before starting, like
|
|
44
|
+
binding specific ports etc.
|
|
45
|
+
In this case, use the ``Start Container`` keyword afterwards.
|
|
46
|
+
"""
|
|
47
|
+
container = DockerContainer(
|
|
48
|
+
image=image,
|
|
49
|
+
command=command,
|
|
50
|
+
env=env,
|
|
51
|
+
name=name,
|
|
52
|
+
ports=ports,
|
|
53
|
+
volumes=self._resolve_volume_paths(volumes) if volumes else None,
|
|
54
|
+
network=network,
|
|
55
|
+
network_aliases=network_aliases,
|
|
56
|
+
)
|
|
57
|
+
if start:
|
|
58
|
+
self.start_container(container)
|
|
59
|
+
return container
|
|
60
|
+
|
|
61
|
+
def _resolve_volume_paths(
|
|
62
|
+
self, volumes: list[tuple[Path, str, str]]
|
|
63
|
+
) -> list[tuple[str, str, str]]:
|
|
64
|
+
return [(v[0].resolve().as_posix(), v[1], v[2]) for v in volumes]
|
|
65
|
+
|
|
66
|
+
@keyword
|
|
67
|
+
def create_server_container(
|
|
68
|
+
self, port: int, image: str, start: bool = True
|
|
69
|
+
) -> ServerContainer:
|
|
70
|
+
"""
|
|
71
|
+
Create a [https://testcontainers-python.readthedocs.io/en/latest/modules/generic/README.html#testcontainers.generic.ServerContainer|ServerContainer]
|
|
72
|
+
|
|
73
|
+
By default, the created container is started directly.
|
|
74
|
+
|
|
75
|
+
This can be prevented by setting ``start=False``, e.g. if
|
|
76
|
+
you need to customize the container before starting, like
|
|
77
|
+
binding specific ports etc.
|
|
78
|
+
In this case, use the ``Start Container`` keyword afterwards.
|
|
79
|
+
"""
|
|
80
|
+
container = ServerContainer(port=port, image=image)
|
|
81
|
+
if start:
|
|
82
|
+
self.start_container(container)
|
|
83
|
+
return container
|
|
84
|
+
|
|
85
|
+
@keyword
|
|
86
|
+
def create_community_container(
|
|
87
|
+
self, module: str, container_class: str, start: bool = True, **kwargs: Any
|
|
88
|
+
) -> DockerContainer:
|
|
89
|
+
"""
|
|
90
|
+
Create a [https://testcontainers-python.readthedocs.io/en/latest/modules/index.html|community maintained container].
|
|
91
|
+
|
|
92
|
+
Specify the ``module`` and ``container_class`` the same way you would import
|
|
93
|
+
it in Python.\nFor example, the ``RedisContainer`` that you would import via\n
|
|
94
|
+
``from testcontainers.redis import RedisContainer``
|
|
95
|
+
\nin Python code can be created with\n
|
|
96
|
+
``| Create Community Container | module=testcontainers.redis | container_class=RedisContainer |``.\n
|
|
97
|
+
Extra arguments required by the container can be passed as keyword arguments.
|
|
98
|
+
|
|
99
|
+
By default, the created container is started directly.
|
|
100
|
+
|
|
101
|
+
This can be prevented by setting ``start=False``, e.g. if
|
|
102
|
+
you need to customize the container before starting, like
|
|
103
|
+
binding specific ports etc.
|
|
104
|
+
In this case, use the ``Start Container`` keyword afterwards.
|
|
105
|
+
"""
|
|
106
|
+
_module = importlib.import_module(module)
|
|
107
|
+
clazz = getattr(_module, container_class)
|
|
108
|
+
container = clazz(**kwargs)
|
|
109
|
+
if start:
|
|
110
|
+
self.start_container(container)
|
|
111
|
+
return container
|
|
112
|
+
|
|
113
|
+
@keyword
|
|
114
|
+
def bind_ports(
|
|
115
|
+
self, container: DockerContainer, container_port: int, host_port: int
|
|
116
|
+
) -> DockerContainer:
|
|
117
|
+
"""
|
|
118
|
+
Bind a container port to a host port.
|
|
119
|
+
"""
|
|
120
|
+
container.with_bind_ports(container_port, host_port)
|
|
121
|
+
return container
|
|
122
|
+
|
|
123
|
+
@keyword
|
|
124
|
+
def start_container(self, container: DockerContainer) -> DockerContainer:
|
|
125
|
+
"""
|
|
126
|
+
Start the given container.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
container.start()
|
|
130
|
+
self._containers.append(container)
|
|
131
|
+
return container
|
|
132
|
+
except Exception:
|
|
133
|
+
container.stop()
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
@keyword
|
|
137
|
+
def wait_for_log_message(
|
|
138
|
+
self, container: DockerContainer, message: str, times: int = 1
|
|
139
|
+
) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Wait until the given container has emitted the specified log message.
|
|
142
|
+
The message can be a simple string or a regular expression.
|
|
143
|
+
|
|
144
|
+
The container must already be started when calling this keyword.
|
|
145
|
+
"""
|
|
146
|
+
LogMessageWaitStrategy(message, times).wait_until_ready(container)
|
|
147
|
+
|
|
148
|
+
@keyword
|
|
149
|
+
def wait_for_http_endpoint(
|
|
150
|
+
self, container: DockerContainer, port: int, path: str, status_code: int = 200
|
|
151
|
+
) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Wait until the specified endpoint is reachable on the given container.
|
|
154
|
+
|
|
155
|
+
The container must already be started when calling this keyword.
|
|
156
|
+
"""
|
|
157
|
+
HttpWaitStrategy(port, path).for_status_code(status_code).wait_until_ready(
|
|
158
|
+
container
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@keyword
|
|
162
|
+
def stop_container(self, container: DockerContainer) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Stop the given container.
|
|
165
|
+
|
|
166
|
+
It is not required to call this keyword manually just to clean up after tests.
|
|
167
|
+
The library instance will take care of stopping all containers it has
|
|
168
|
+
started during it's lifecycle via listener methods.
|
|
169
|
+
"""
|
|
170
|
+
self._containers.remove(container)
|
|
171
|
+
container.stop()
|
|
172
|
+
|
|
173
|
+
@keyword
|
|
174
|
+
def create_network(self) -> Network:
|
|
175
|
+
"""
|
|
176
|
+
Create a network to connect different containers with each other.
|
|
177
|
+
|
|
178
|
+
Created networks are cleaned up automatically at the end of the test/suite.
|
|
179
|
+
"""
|
|
180
|
+
network = Network()
|
|
181
|
+
network.create()
|
|
182
|
+
self._networks.append(network)
|
|
183
|
+
return network
|
|
184
|
+
|
|
185
|
+
@keyword
|
|
186
|
+
def remove_network(self, network: Network) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Delete the given network.
|
|
189
|
+
"""
|
|
190
|
+
self._networks.remove(network)
|
|
191
|
+
network.remove()
|
|
192
|
+
|
|
193
|
+
@keyword
|
|
194
|
+
def connect_container_to_network(
|
|
195
|
+
self,
|
|
196
|
+
container: DockerContainer,
|
|
197
|
+
network: Network,
|
|
198
|
+
aliases: list[str] | None = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Connect a container to a network. It will be reachable from other containers by its alias, if set.
|
|
202
|
+
"""
|
|
203
|
+
container_id = container.get_wrapped_container().id
|
|
204
|
+
if container_id is None:
|
|
205
|
+
raise ValueError("Failed to obtain ID of wrapped container")
|
|
206
|
+
network.connect(container_id=container_id, network_aliases=aliases)
|
|
207
|
+
|
|
208
|
+
def _end_test(self, name: Any, attrs: Any) -> None:
|
|
209
|
+
for container in self._containers.copy():
|
|
210
|
+
cname = container.get_wrapped_container().name
|
|
211
|
+
logger.console(
|
|
212
|
+
f"\n\tTestcontainersLibrary: stopping container {cname} in end_test hook."
|
|
213
|
+
)
|
|
214
|
+
self.stop_container(container)
|
|
215
|
+
for network in self._networks.copy():
|
|
216
|
+
logger.console(
|
|
217
|
+
f"\n\tTestcontainersLibrary: removing network {network.id} in end_test hook."
|
|
218
|
+
)
|
|
219
|
+
self.remove_network(network)
|
|
220
|
+
|
|
221
|
+
def _end_suite(self, name: Any, attrs: Any) -> None:
|
|
222
|
+
for container in self._containers.copy():
|
|
223
|
+
cname = container.get_wrapped_container().name
|
|
224
|
+
logger.console(
|
|
225
|
+
f"\n\tTestcontainersLibrary: stopping container {cname} in end_suite hook."
|
|
226
|
+
)
|
|
227
|
+
self.stop_container(container)
|
|
228
|
+
for network in self._networks.copy():
|
|
229
|
+
logger.console(
|
|
230
|
+
f"\n\tTestcontainersLibrary: removing network {network.id} in end_suite hook."
|
|
231
|
+
)
|
|
232
|
+
self.remove_network(network)
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from robot.api.deco import library, keyword
|
|
4
|
-
from robot.api import logger
|
|
5
|
-
from testcontainers.core.container import DockerContainer
|
|
6
|
-
from testcontainers.core.wait_strategies import LogMessageWaitStrategy, HttpWaitStrategy
|
|
7
|
-
from testcontainers.generic.server import ServerContainer
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@library(listener="SELF")
|
|
11
|
-
class TestcontainersLibrary:
|
|
12
|
-
def __init__(self):
|
|
13
|
-
self._containers: list[DockerContainer] = []
|
|
14
|
-
|
|
15
|
-
@keyword
|
|
16
|
-
def create_docker_container(
|
|
17
|
-
self,
|
|
18
|
-
image: str,
|
|
19
|
-
command: str | None = None,
|
|
20
|
-
env: dict[str, str] | None = None,
|
|
21
|
-
name: str | None = None,
|
|
22
|
-
ports: list[int] | None = None,
|
|
23
|
-
volumes: list[tuple[Path, str, str]] | None = None,
|
|
24
|
-
start: bool = True,
|
|
25
|
-
) -> DockerContainer:
|
|
26
|
-
container = DockerContainer(
|
|
27
|
-
image=image,
|
|
28
|
-
command=command,
|
|
29
|
-
env=env,
|
|
30
|
-
name=name,
|
|
31
|
-
ports=ports,
|
|
32
|
-
volumes=self._resolve_volume_paths(volumes) if volumes else None,
|
|
33
|
-
)
|
|
34
|
-
if start:
|
|
35
|
-
self.start_container(container)
|
|
36
|
-
return container
|
|
37
|
-
|
|
38
|
-
def _resolve_volume_paths(
|
|
39
|
-
self, volumes: list[tuple[Path, str, str]]
|
|
40
|
-
) -> list[tuple[str, str, str]]:
|
|
41
|
-
return [(v[0].resolve().as_posix(), v[1], v[2]) for v in volumes]
|
|
42
|
-
|
|
43
|
-
@keyword
|
|
44
|
-
def create_server_container(
|
|
45
|
-
self, port: int, image: str, start: bool = True
|
|
46
|
-
) -> ServerContainer:
|
|
47
|
-
container = ServerContainer(port=port, image=image)
|
|
48
|
-
if start:
|
|
49
|
-
self.start_container(container)
|
|
50
|
-
return container
|
|
51
|
-
|
|
52
|
-
@keyword
|
|
53
|
-
def create_community_container(
|
|
54
|
-
self, module: str, container_class: str, start: bool = True, **kwargs
|
|
55
|
-
) -> DockerContainer:
|
|
56
|
-
_module = importlib.import_module(module)
|
|
57
|
-
clazz = getattr(_module, container_class)
|
|
58
|
-
container = clazz(**kwargs)
|
|
59
|
-
if start:
|
|
60
|
-
self.start_container(container)
|
|
61
|
-
return container
|
|
62
|
-
|
|
63
|
-
@keyword
|
|
64
|
-
def bind_ports(
|
|
65
|
-
self, container: DockerContainer, container_port: int, host_port: int
|
|
66
|
-
) -> DockerContainer:
|
|
67
|
-
container.with_bind_ports(container_port, host_port)
|
|
68
|
-
return container
|
|
69
|
-
|
|
70
|
-
@keyword
|
|
71
|
-
def start_container(self, container: DockerContainer) -> DockerContainer:
|
|
72
|
-
try:
|
|
73
|
-
container.start()
|
|
74
|
-
self._containers.append(container)
|
|
75
|
-
return container
|
|
76
|
-
except Exception:
|
|
77
|
-
container.stop()
|
|
78
|
-
raise
|
|
79
|
-
|
|
80
|
-
@keyword
|
|
81
|
-
def wait_for_log_message(
|
|
82
|
-
self, container: DockerContainer, message: str, times: int = 1
|
|
83
|
-
) -> None:
|
|
84
|
-
LogMessageWaitStrategy(message, times).wait_until_ready(container)
|
|
85
|
-
|
|
86
|
-
@keyword
|
|
87
|
-
def wait_for_http_endpoint(
|
|
88
|
-
self, container: DockerContainer, port: int, path: str
|
|
89
|
-
) -> None:
|
|
90
|
-
HttpWaitStrategy(port, path).wait_until_ready(container)
|
|
91
|
-
|
|
92
|
-
@keyword
|
|
93
|
-
def stop_container(self, container: DockerContainer) -> None:
|
|
94
|
-
self._containers.remove(container)
|
|
95
|
-
container.stop()
|
|
96
|
-
|
|
97
|
-
def _end_test(self, name, attrs) -> None:
|
|
98
|
-
for container in self._containers.copy():
|
|
99
|
-
cname = container.get_wrapped_container().name
|
|
100
|
-
logger.console(
|
|
101
|
-
f"\n\tTestcontainersLibrary: stopping container {cname} in end_test hook."
|
|
102
|
-
)
|
|
103
|
-
self.stop_container(container)
|
|
104
|
-
|
|
105
|
-
def _end_suite(self, name, attrs) -> None:
|
|
106
|
-
for container in self._containers.copy():
|
|
107
|
-
cname = container.get_wrapped_container().name
|
|
108
|
-
logger.console(
|
|
109
|
-
f"\n\tTestcontainersLibrary: stopping container {cname} in end_suite hook."
|
|
110
|
-
)
|
|
111
|
-
self.stop_container(container)
|
|
File without changes
|
|
File without changes
|