1414
1515
1616class 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+
1726 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+ """
1833 self ._container = container
1934 self ._app = app
2035
2136 @property
2237 def container (self ) -> CeleryTestContainer :
38+ """Underlying container for the node."""
2339 return self ._container
2440
2541 @property
2642 def app (self ) -> Celery :
43+ """Celery app for the node if available."""
2744 return self ._app
2845
29- def __eq__ (self , __value : object ) -> bool :
30- if isinstance (__value , CeleryTestNode ):
46+ def __eq__ (self , other : object ) -> bool :
47+ if isinstance (other , CeleryTestNode ):
3148 return all (
3249 (
33- self .container == __value .container ,
34- self .app == __value .app ,
50+ self .container == other .container ,
51+ self .app == other .app ,
3552 )
3653 )
3754 return False
3855
3956 @classmethod
4057 def default_config (cls ) -> dict :
58+ """Default node configurations if not overridden by the user."""
4159 return {}
4260
4361 def ready (self ) -> bool :
62+ """Waits until the node is ready or raise an exception if it fails to
63+ boot up."""
4464 return self .container .ready ()
4565
4666 def config (self , * args : tuple , ** kwargs : dict ) -> dict :
67+ """Compile the configurations required for Celery from this node."""
4768 return self .container .celeryconfig
4869
4970 def logs (self ) -> str :
71+ """Get the logs of the underlying container."""
5072 return self .container .logs ()
5173
5274 def name (self ) -> str :
75+ """Get the name of this node."""
5376 return self .container .name
5477
5578 def hostname (self ) -> str :
79+ """Get the hostname of this node."""
5680 return self .container .id [:12 ]
5781
5882 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+ """
5989 if self .container .status == "running" :
6090 self .container .kill (signal = signal )
6191 if reload_container :
6292 self .container .reload ()
6393
6494 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+ """
65101 if force :
102+ # Use SIGTERM to allow the container to gracefully shutdown
66103 self .kill (signal = "SIGTERM" , reload_container = reload_container )
67104 self .container .restart (timeout = CONTAINER_TIMEOUT )
68105 if reload_container :
@@ -71,19 +108,41 @@ def restart(self, reload_container: bool = True, force: bool = False) -> None:
71108 self .app .set_current ()
72109
73110 def teardown (self ) -> None :
111+ """Teardown the node."""
74112 self .container .teardown ()
75113
76114 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+ """
77122 message = message or f"Waiting for worker container '{ self .name ()} ' to log -> { log } "
78123 wait_for_callable (message = message , func = lambda : log in self .logs (), timeout = timeout )
79124
80125 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+ """
81133 try :
82134 self .wait_for_log (log , message , timeout )
83135 except pytest_docker_tools .exceptions .TimeoutError :
84136 assert False , f"Worker container '{ self .name ()} ' did not log -> { log } within { timeout } seconds"
85137
86138 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+ """
87146 message = message or f"Waiting for worker container '{ self .name ()} ' to not log -> { log } "
88147 try :
89148 self .wait_for_log (log , message , timeout )
@@ -93,7 +152,24 @@ def assert_log_does_not_exist(self, log: str, message: str = "", timeout: int =
93152
94153
95154class 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+
96163 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+ """
97173 if not nodes :
98174 raise ValueError ("At least one node is required" )
99175 if len (nodes ) == 1 and isinstance (nodes [0 ], list ):
@@ -105,10 +181,16 @@ def __init__(self, *nodes: tuple[CeleryTestNode | CeleryTestContainer]) -> None:
105181
106182 @property
107183 def nodes (self ) -> tuple [CeleryTestNode ]:
184+ """Get the nodes of the cluster."""
108185 return self ._nodes
109186
110187 @nodes .setter
111188 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+ """
112194 self ._nodes = self ._set_nodes (* nodes ) # type: ignore
113195
114196 def __iter__ (self ) -> Iterator [CeleryTestNode ]:
@@ -120,15 +202,16 @@ def __getitem__(self, index: Any) -> CeleryTestNode:
120202 def __len__ (self ) -> int :
121203 return len (self .nodes )
122204
123- def __eq__ (self , __value : object ) -> bool :
124- if isinstance (__value , CeleryTestCluster ):
205+ def __eq__ (self , other : object ) -> bool :
206+ if isinstance (other , CeleryTestCluster ):
125207 for node in self :
126- if node not in __value :
208+ if node not in other :
127209 return False
128210 return False
129211
130212 @classmethod
131213 def default_config (cls ) -> dict :
214+ """Default cluster configurations if not overridden by the user."""
132215 return {}
133216
134217 @abstractmethod
@@ -137,6 +220,15 @@ def _set_nodes(
137220 * nodes : tuple [CeleryTestNode | CeleryTestContainer ],
138221 node_cls : type [CeleryTestNode ] = CeleryTestNode ,
139222 ) -> 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+ """
140232 return tuple (
141233 node_cls (node )
142234 if isinstance (
@@ -148,16 +240,19 @@ def _set_nodes(
148240 ) # type: ignore
149241
150242 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."""
151245 return all (node .ready () for node in self )
152246
153247 def config (self , * args : tuple , ** kwargs : dict ) -> dict :
248+ """Compile the configurations required for Celery from this cluster."""
154249 config = [node .container .celeryconfig for node in self ]
155250 return {
156251 "urls" : [c ["url" ] for c in config ],
157252 "local_urls" : [c ["local_url" ] for c in config ],
158253 }
159254
160255 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