14
14
15
15
16
16
class CeleryTestNode :
17
+ """CeleryTestNode is the logical representation of a container instance. It
18
+ is used to provide a common interface for interacting with the container
19
+ regardless of the underlying implementation.
20
+
21
+ Responsibility Scope:
22
+ The node's responsibility is to wrap the container and provide
23
+ useful methods for interacting with it.
24
+ """
25
+
17
26
def __init__ (self , container : CeleryTestContainer , app : Celery = None ) -> None :
27
+ """Setup the base components of a CeleryTestNode.
28
+
29
+ Args:
30
+ container (CeleryTestContainer): Container to use for the node.
31
+ app (Celery, optional): Celery app. Defaults to None.
32
+ """
18
33
self ._container = container
19
34
self ._app = app
20
35
21
36
@property
22
37
def container (self ) -> CeleryTestContainer :
38
+ """Underlying container for the node."""
23
39
return self ._container
24
40
25
41
@property
26
42
def app (self ) -> Celery :
43
+ """Celery app for the node if available."""
27
44
return self ._app
28
45
29
- def __eq__ (self , __value : object ) -> bool :
30
- if isinstance (__value , CeleryTestNode ):
46
+ def __eq__ (self , other : object ) -> bool :
47
+ if isinstance (other , CeleryTestNode ):
31
48
return all (
32
49
(
33
- self .container == __value .container ,
34
- self .app == __value .app ,
50
+ self .container == other .container ,
51
+ self .app == other .app ,
35
52
)
36
53
)
37
54
return False
38
55
39
56
@classmethod
40
57
def default_config (cls ) -> dict :
58
+ """Default node configurations if not overridden by the user."""
41
59
return {}
42
60
43
61
def ready (self ) -> bool :
62
+ """Waits until the node is ready or raise an exception if it fails to
63
+ boot up."""
44
64
return self .container .ready ()
45
65
46
66
def config (self , * args : tuple , ** kwargs : dict ) -> dict :
67
+ """Compile the configurations required for Celery from this node."""
47
68
return self .container .celeryconfig
48
69
49
70
def logs (self ) -> str :
71
+ """Get the logs of the underlying container."""
50
72
return self .container .logs ()
51
73
52
74
def name (self ) -> str :
75
+ """Get the name of this node."""
53
76
return self .container .name
54
77
55
78
def hostname (self ) -> str :
79
+ """Get the hostname of this node."""
56
80
return self .container .id [:12 ]
57
81
58
82
def kill (self , signal : str | int = "SIGKILL" , reload_container : bool = True ) -> None :
83
+ """Kill the underlying container.
84
+
85
+ Args:
86
+ signal (str | int, optional): Signal to send to the container. Defaults to "SIGKILL".
87
+ reload_container (bool, optional): Reload the container object after killing it. Defaults to True.
88
+ """
59
89
if self .container .status == "running" :
60
90
self .container .kill (signal = signal )
61
91
if reload_container :
62
92
self .container .reload ()
63
93
64
94
def restart (self , reload_container : bool = True , force : bool = False ) -> None :
95
+ """Restart the underlying container.
96
+
97
+ Args:
98
+ reload_container (bool, optional): Reload the container object after restarting it. Defaults to True.
99
+ force (bool, optional): Kill the container before restarting it. Defaults to False.
100
+ """
65
101
if force :
102
+ # Use SIGTERM to allow the container to gracefully shutdown
66
103
self .kill (signal = "SIGTERM" , reload_container = reload_container )
67
104
self .container .restart (timeout = CONTAINER_TIMEOUT )
68
105
if reload_container :
@@ -71,19 +108,41 @@ def restart(self, reload_container: bool = True, force: bool = False) -> None:
71
108
self .app .set_current ()
72
109
73
110
def teardown (self ) -> None :
111
+ """Teardown the node."""
74
112
self .container .teardown ()
75
113
76
114
def wait_for_log (self , log : str , message : str = "" , timeout : int = RESULT_TIMEOUT ) -> None :
115
+ """Wait for a log to appear in the container.
116
+
117
+ Args:
118
+ log (str): Log to wait for.
119
+ message (str, optional): Message to display while waiting. Defaults to "".
120
+ timeout (int, optional): Timeout in seconds. Defaults to RESULT_TIMEOUT.
121
+ """
77
122
message = message or f"Waiting for worker container '{ self .name ()} ' to log -> { log } "
78
123
wait_for_callable (message = message , func = lambda : log in self .logs (), timeout = timeout )
79
124
80
125
def assert_log_exists (self , log : str , message : str = "" , timeout : int = RESULT_TIMEOUT ) -> None :
126
+ """Assert that a log exists in the container.
127
+
128
+ Args:
129
+ log (str): Log to assert.
130
+ message (str, optional): Message to display while waiting. Defaults to "".
131
+ timeout (int, optional): Timeout in seconds. Defaults to RESULT_TIMEOUT.
132
+ """
81
133
try :
82
134
self .wait_for_log (log , message , timeout )
83
135
except pytest_docker_tools .exceptions .TimeoutError :
84
136
assert False , f"Worker container '{ self .name ()} ' did not log -> { log } within { timeout } seconds"
85
137
86
138
def assert_log_does_not_exist (self , log : str , message : str = "" , timeout : int = 1 ) -> None :
139
+ """Assert that a log does not exist in the container.
140
+
141
+ Args:
142
+ log (str): Log to assert.
143
+ message (str, optional): Message to display while waiting. Defaults to "".
144
+ timeout (int, optional): Timeout in seconds. Defaults to 1.
145
+ """
87
146
message = message or f"Waiting for worker container '{ self .name ()} ' to not log -> { log } "
88
147
try :
89
148
self .wait_for_log (log , message , timeout )
@@ -93,7 +152,24 @@ def assert_log_does_not_exist(self, log: str, message: str = "", timeout: int =
93
152
94
153
95
154
class CeleryTestCluster :
155
+ """CeleryTestCluster is a collection of CeleryTestNodes. It is used to
156
+ collect the test nodes into a single object for easier management.
157
+
158
+ Responsibility Scope:
159
+ The cluster's responsibility is to define which nodes will be used for
160
+ the test.
161
+ """
162
+
96
163
def __init__ (self , * nodes : tuple [CeleryTestNode | CeleryTestContainer ]) -> None :
164
+ """Setup the base components of a CeleryTestCluster.
165
+
166
+ Args:
167
+ *nodes (tuple[CeleryTestNode | CeleryTestContainer]): Nodes to use for the cluster.
168
+
169
+ Raises:
170
+ ValueError: At least one node is required.
171
+ TypeError: All nodes must be CeleryTestNode or CeleryTestContainer
172
+ """
97
173
if not nodes :
98
174
raise ValueError ("At least one node is required" )
99
175
if len (nodes ) == 1 and isinstance (nodes [0 ], list ):
@@ -105,10 +181,16 @@ def __init__(self, *nodes: tuple[CeleryTestNode | CeleryTestContainer]) -> None:
105
181
106
182
@property
107
183
def nodes (self ) -> tuple [CeleryTestNode ]:
184
+ """Get the nodes of the cluster."""
108
185
return self ._nodes
109
186
110
187
@nodes .setter
111
188
def nodes (self , nodes : tuple [CeleryTestNode | CeleryTestContainer ]) -> None :
189
+ """Set the nodes of the cluster.
190
+
191
+ Args:
192
+ nodes (tuple[CeleryTestNode | CeleryTestContainer]): Nodes to use for the cluster.
193
+ """
112
194
self ._nodes = self ._set_nodes (* nodes ) # type: ignore
113
195
114
196
def __iter__ (self ) -> Iterator [CeleryTestNode ]:
@@ -120,15 +202,16 @@ def __getitem__(self, index: Any) -> CeleryTestNode:
120
202
def __len__ (self ) -> int :
121
203
return len (self .nodes )
122
204
123
- def __eq__ (self , __value : object ) -> bool :
124
- if isinstance (__value , CeleryTestCluster ):
205
+ def __eq__ (self , other : object ) -> bool :
206
+ if isinstance (other , CeleryTestCluster ):
125
207
for node in self :
126
- if node not in __value :
208
+ if node not in other :
127
209
return False
128
210
return False
129
211
130
212
@classmethod
131
213
def default_config (cls ) -> dict :
214
+ """Default cluster configurations if not overridden by the user."""
132
215
return {}
133
216
134
217
@abstractmethod
@@ -137,6 +220,15 @@ def _set_nodes(
137
220
* nodes : tuple [CeleryTestNode | CeleryTestContainer ],
138
221
node_cls : type [CeleryTestNode ] = CeleryTestNode ,
139
222
) -> tuple [CeleryTestNode ]:
223
+ """Set the nodes of the cluster.
224
+
225
+ Args:
226
+ *nodes (tuple[CeleryTestNode | CeleryTestContainer]): Nodes to use for the cluster.
227
+ node_cls (type[CeleryTestNode], optional): Node class to use. Defaults to CeleryTestNode.
228
+
229
+ Returns:
230
+ tuple[CeleryTestNode]: Nodes to use for the cluster.
231
+ """
140
232
return tuple (
141
233
node_cls (node )
142
234
if isinstance (
@@ -148,16 +240,19 @@ def _set_nodes(
148
240
) # type: ignore
149
241
150
242
def ready (self ) -> bool :
243
+ """Waits until the cluster is ready or raise an exception if any of the
244
+ nodes fail to boot up."""
151
245
return all (node .ready () for node in self )
152
246
153
247
def config (self , * args : tuple , ** kwargs : dict ) -> dict :
248
+ """Compile the configurations required for Celery from this cluster."""
154
249
config = [node .container .celeryconfig for node in self ]
155
250
return {
156
251
"urls" : [c ["url" ] for c in config ],
157
252
"local_urls" : [c ["local_url" ] for c in config ],
158
253
}
159
254
160
255
def teardown (self ) -> None :
161
- # Do not need to call teardown on the nodes
162
- # but only tear down self
163
- pass
256
+ """Teardown the cluster."""
257
+ # Nodes teardown themselves, so we just need to clear the cluster
258
+ # if there is any cleanup to do
0 commit comments