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.
@@ -1,18 +1,28 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: robotframework-testcontainers
3
- Version: 0.1.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`:
@@ -2,6 +2,8 @@
2
2
 
3
3
  Robot Framework keywords for [testcontainers](https://github.com/testcontainers/testcontainers-python).
4
4
 
5
+ [Keyword Documentation](https://dudenr33.github.io/robotframework-testcontainers/)
6
+
5
7
  ## Installation
6
8
 
7
9
  - using `pip`:
@@ -1,11 +1,32 @@
1
1
  [project]
2
2
  name = "robotframework-testcontainers"
3
- version = "0.1.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)