1010from typing import Final
1111from typing import Optional
1212
13+ from apscheduler .job import Job
14+ from apscheduler .jobstores .base import JobLookupError
15+ from apscheduler .schedulers .background import BackgroundScheduler
16+ from apscheduler .triggers .interval import IntervalTrigger
1317from packaging import version
18+ from tzlocal import get_localzone
1419
1520from tests .integration .test_utils import RunSubprocessMixin
1621from tests .integration .test_utils import get_podman_version
22+ from tests .integration .test_utils import is_systemd_available
1723from tests .integration .test_utils import podman_compose_path
1824from tests .integration .test_utils import test_path
1925
@@ -24,6 +30,71 @@ def compose_yaml_path() -> str:
2430 return os .path .join (test_path (), "wait" , "docker-compose.yml" )
2531
2632
33+ # WORKAROUND for https://github.com/containers/podman/issues/28192
34+ # the healthchecks of Podman rely on systemd timers
35+ # in the test environment (GH actions -> container -> Podman) no systemd is available
36+ # therefore, this class ensures that a periodical healthcheck is run by calling
37+ # podman healthcheck run <container_name>
38+ # with the help of APScheduler
39+ #
40+ # if systemd is available, this class does nothing
41+ class EnsureHealthcheckRun :
42+ def __init__ (self , runner : RunSubprocessMixin , test_case : unittest .TestCase ) -> None :
43+ self .__runner : RunSubprocessMixin = runner
44+
45+ self .__scheduler : Optional [BackgroundScheduler ] = None
46+ self .__healthcheck_job : Optional [Job ] = None
47+
48+ # check if systemd is not available
49+ if not is_systemd_available ():
50+ # initialize APScheduler
51+ self .__scheduler = BackgroundScheduler ()
52+ self .__scheduler .start ()
53+
54+ # add a clean up to the test case (in case the test fails)
55+ test_case .addCleanup (self ._cleanup_scheduler )
56+
57+ def _cleanup_scheduler (self ) -> None :
58+ # stop and remove the healtcheck job
59+ if self .__scheduler is not None :
60+ try :
61+ if self .__healthcheck_job is not None :
62+ self .__scheduler .remove_job (self .__healthcheck_job .id )
63+ else :
64+ self .__scheduler .remove_all_jobs ()
65+ except JobLookupError :
66+ # failover
67+ self .__scheduler .remove_all_jobs ()
68+
69+ # shutdown the scheduler
70+ self .__scheduler .shutdown (wait = True )
71+
72+ # prevent this method from being executed twice:
73+ # after exiting the context manager and during clean up of the test case
74+ self .__scheduler = None
75+
76+ def __enter__ (self ) -> None :
77+ # start the healthcheck job
78+ if self .__scheduler is not None :
79+ self .__healthcheck_job = self .__scheduler .add_job (
80+ func = self .__runner .run_subprocess ,
81+ # run health checking only for the wait_app_health_1 container
82+ kwargs = {"args" : ["podman" , "healthcheck" , "run" , "wait_app_health_1" ]},
83+ trigger = IntervalTrigger (
84+ # trigger the healthcheck every 3 seconds (like defined in docker-compose.yml)
85+ seconds = 3 ,
86+ # run first healthcheck after 3 seconds (initial interval)
87+ start_date = datetime .now (get_localzone ()) + timedelta (seconds = 3 ),
88+ # stop after 21 seconds = 3s (interval) * 6 (retries) + 3s (initial interval)
89+ end_date = datetime .now (get_localzone ()) + timedelta (seconds = 21 ),
90+ ),
91+ )
92+
93+ def __exit__ (self , * _ : Any ) -> None :
94+ # clean up after exiting the context manager
95+ self ._cleanup_scheduler ()
96+
97+
2798class ExecutionTime :
2899 def __init__ (
29100 self ,
@@ -104,153 +175,158 @@ def _compose_down(self) -> None:
104175
105176 def test_without_wait (self ) -> None :
106177 try :
107- # the execution time of this command must be not more then 10 seconds
108- # otherwise this test case makes no sense
109- with ExecutionTime (max_execution_time = timedelta (seconds = 10 )):
110- self .run_subprocess_assert_returncode ([
178+ with EnsureHealthcheckRun (runner = self , test_case = self ):
179+ # the execution time of this command must be not more then 10 seconds
180+ # otherwise this test case makes no sense
181+ with ExecutionTime (max_execution_time = timedelta (seconds = 10 )):
182+ self .run_subprocess_assert_returncode ([
183+ podman_compose_path (),
184+ "-f" ,
185+ compose_yaml_path (),
186+ "up" ,
187+ "-d" ,
188+ ])
189+
190+ output , _ = self .run_subprocess_assert_returncode ([
111191 podman_compose_path (),
112192 "-f" ,
113193 compose_yaml_path (),
114- "up" ,
115- "-d" ,
194+ "ps" ,
116195 ])
196+ self .assertIn (b"wait_app_health_1" , output )
197+ self .assertIn (b"wait_app_1" , output )
117198
118- output , _ = self .run_subprocess_assert_returncode ([
119- podman_compose_path (),
120- "-f" ,
121- compose_yaml_path (),
122- "ps" ,
123- ])
124- self .assertIn (b"wait_app_health_1" , output )
125- self .assertIn (b"wait_app_1" , output )
126-
127- self .assertTrue (self ._is_running ("wait_app_1" ))
199+ self .assertTrue (self ._is_running ("wait_app_1" ))
128200
129- health = self ._get_health_status ("wait_app_health_1" )
130- self .assertEqual (health , "starting" )
201+ health = self ._get_health_status ("wait_app_health_1" )
202+ self .assertEqual (health , "starting" )
131203 finally :
132204 self ._compose_down ()
133205
134206 def test_wait (self ) -> None :
135207 try :
136- # the execution time of this command must be at least 10 seconds,
137- # because of the sleep command in entrypoint.sh
138- with ExecutionTime (min_execution_time = timedelta (seconds = 10 )):
139- self .run_subprocess_assert_returncode (
140- [
141- podman_compose_path (),
142- "-f" ,
143- compose_yaml_path (),
144- "up" ,
145- "-d" ,
146- "--wait" ,
147- ],
148- timeout = EXECUTION_TIMEOUT ,
149- )
150-
151- output , _ = self .run_subprocess_assert_returncode ([
152- podman_compose_path (),
153- "-f" ,
154- compose_yaml_path (),
155- "ps" ,
156- ])
157- self .assertIn (b"wait_app_health_1" , output )
158- self .assertIn (b"wait_app_1" , output )
208+ with EnsureHealthcheckRun (runner = self , test_case = self ):
209+ # the execution time of this command must be at least 10 seconds,
210+ # because of the sleep command in entrypoint.sh
211+ with ExecutionTime (min_execution_time = timedelta (seconds = 10 )):
212+ self .run_subprocess_assert_returncode (
213+ [
214+ podman_compose_path (),
215+ "-f" ,
216+ compose_yaml_path (),
217+ "up" ,
218+ "-d" ,
219+ "--wait" ,
220+ ],
221+ timeout = EXECUTION_TIMEOUT ,
222+ )
223+
224+ output , _ = self .run_subprocess_assert_returncode ([
225+ podman_compose_path (),
226+ "-f" ,
227+ compose_yaml_path (),
228+ "ps" ,
229+ ])
230+ self .assertIn (b"wait_app_health_1" , output )
231+ self .assertIn (b"wait_app_1" , output )
159232
160- self .assertTrue (self ._is_running ("wait_app_1" ))
233+ self .assertTrue (self ._is_running ("wait_app_1" ))
161234
162- health = self ._get_health_status ("wait_app_health_1" )
163- self .assertEqual (health , "healthy" )
235+ health = self ._get_health_status ("wait_app_health_1" )
236+ self .assertEqual (health , "healthy" )
164237 finally :
165238 self ._compose_down ()
166239
167240 def test_wait_with_timeout (self ) -> None :
168241 try :
169- # the execution time of this command must be between 5 and 10 seconds
170- with ExecutionTime (
171- min_execution_time = timedelta (seconds = 5 ), max_execution_time = timedelta (seconds = 10 )
172- ):
173- self .run_subprocess_assert_returncode (
174- [
175- podman_compose_path (),
176- "-f" ,
177- compose_yaml_path (),
178- "up" ,
179- "-d" ,
180- "--wait" ,
181- "--wait-timeout" ,
182- "5" ,
183- ],
184- timeout = EXECUTION_TIMEOUT ,
185- )
186-
187- output , _ = self .run_subprocess_assert_returncode ([
188- podman_compose_path (),
189- "-f" ,
190- compose_yaml_path (),
191- "ps" ,
192- ])
193- self .assertIn (b"wait_app_health_1" , output )
194- self .assertIn (b"wait_app_1" , output )
242+ with EnsureHealthcheckRun (runner = self , test_case = self ):
243+ # the execution time of this command must be between 5 and 10 seconds
244+ with ExecutionTime (
245+ min_execution_time = timedelta (seconds = 5 ),
246+ max_execution_time = timedelta (seconds = 10 ),
247+ ):
248+ self .run_subprocess_assert_returncode (
249+ [
250+ podman_compose_path (),
251+ "-f" ,
252+ compose_yaml_path (),
253+ "up" ,
254+ "-d" ,
255+ "--wait" ,
256+ "--wait-timeout" ,
257+ "5" ,
258+ ],
259+ timeout = EXECUTION_TIMEOUT ,
260+ )
261+
262+ output , _ = self .run_subprocess_assert_returncode ([
263+ podman_compose_path (),
264+ "-f" ,
265+ compose_yaml_path (),
266+ "ps" ,
267+ ])
268+ self .assertIn (b"wait_app_health_1" , output )
269+ self .assertIn (b"wait_app_1" , output )
195270
196- self .assertTrue (self ._is_running ("wait_app_1" ))
271+ self .assertTrue (self ._is_running ("wait_app_1" ))
197272
198- health = self ._get_health_status ("wait_app_health_1" )
199- self .assertEqual (health , "starting" )
273+ health = self ._get_health_status ("wait_app_health_1" )
274+ self .assertEqual (health , "starting" )
200275 finally :
201276 self ._compose_down ()
202277
203278 def test_wait_with_start (self ) -> None :
204279 try :
205- # the execution time of this command must be not more then 10 seconds
206- # otherwise this test case makes no sense
207- with ExecutionTime (max_execution_time = timedelta (seconds = 10 )):
208- # podman-compose create does not exist
209- # therefore bring the containers up and kill them immediately again
210- self .run_subprocess_assert_returncode ([
211- podman_compose_path (),
212- "-f" ,
213- compose_yaml_path (),
214- "up" ,
215- "-d" ,
216- ])
217- self .run_subprocess_assert_returncode ([
218- podman_compose_path (),
219- "-f" ,
220- compose_yaml_path (),
221- "kill" ,
222- "--all" ,
223- ])
224-
225- output , _ = self .run_subprocess_assert_returncode ([
226- podman_compose_path (),
227- "-f" ,
228- compose_yaml_path (),
229- "ps" ,
230- ])
231- self .assertIn (b"wait_app_health_1" , output )
232- self .assertIn (b"wait_app_1" , output )
233-
234- self .assertFalse (self ._is_running ("wait_app_health_1" ))
235- self .assertFalse (self ._is_running ("wait_app_1" ))
236-
237- # the execution time of this command must be at least 10 seconds,
238- # because of the sleep command in entrypoint.sh
239- with ExecutionTime (min_execution_time = timedelta (seconds = 10 )):
240- self .run_subprocess_assert_returncode (
241- [
280+ with EnsureHealthcheckRun (runner = self , test_case = self ):
281+ # the execution time of this command must be not more then 10 seconds
282+ # otherwise this test case makes no sense
283+ with ExecutionTime (max_execution_time = timedelta (seconds = 10 )):
284+ # podman-compose create does not exist
285+ # therefore bring the containers up and kill them immediately again
286+ self .run_subprocess_assert_returncode ([
242287 podman_compose_path (),
243288 "-f" ,
244289 compose_yaml_path (),
245- "start" ,
246- "--wait" ,
247- ],
248- timeout = EXECUTION_TIMEOUT ,
249- )
250-
251- self .assertTrue (self ._is_running ("wait_app_1" ))
290+ "up" ,
291+ "-d" ,
292+ ])
293+ self .run_subprocess_assert_returncode ([
294+ podman_compose_path (),
295+ "-f" ,
296+ compose_yaml_path (),
297+ "kill" ,
298+ "--all" ,
299+ ])
252300
253- health = self ._get_health_status ("wait_app_health_1" )
254- self .assertEqual (health , "healthy" )
301+ output , _ = self .run_subprocess_assert_returncode ([
302+ podman_compose_path (),
303+ "-f" ,
304+ compose_yaml_path (),
305+ "ps" ,
306+ ])
307+ self .assertIn (b"wait_app_health_1" , output )
308+ self .assertIn (b"wait_app_1" , output )
309+
310+ self .assertFalse (self ._is_running ("wait_app_health_1" ))
311+ self .assertFalse (self ._is_running ("wait_app_1" ))
312+
313+ # the execution time of this command must be at least 10 seconds,
314+ # because of the sleep command in entrypoint.sh
315+ with ExecutionTime (min_execution_time = timedelta (seconds = 10 )):
316+ self .run_subprocess_assert_returncode (
317+ [
318+ podman_compose_path (),
319+ "-f" ,
320+ compose_yaml_path (),
321+ "start" ,
322+ "--wait" ,
323+ ],
324+ timeout = EXECUTION_TIMEOUT ,
325+ )
326+
327+ self .assertTrue (self ._is_running ("wait_app_1" ))
328+
329+ health = self ._get_health_status ("wait_app_health_1" )
330+ self .assertEqual (health , "healthy" )
255331 finally :
256332 self ._compose_down ()
0 commit comments