Skip to content

Commit f083a11

Browse files
committed
workaround for testing without systemd
Signed-off-by: mammo0 <marc.ammon@hotmail.de>
1 parent cfc2928 commit f083a11

File tree

3 files changed

+198
-119
lines changed

3 files changed

+198
-119
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,6 @@ exclude = "build"
125125
[[tool.mypy.overrides]]
126126
module = [
127127
"parameterized.*",
128+
"apscheduler.*",
128129
]
129130
ignore_missing_imports = true

test-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pylint==3.1.0
1010
types-PyYAML==6.0.12.20250402
1111
types-requests==2.32.0.20250328
1212
types-setuptools==80.7.0.20250516
13+
APScheduler==3.11.2
1314

1415
# NOTE: when upgrading the above packages, also upgrade .pre-commit-config.yaml
1516

@@ -45,5 +46,6 @@ PyYAML==6.0.1
4546
requests==2.32.5
4647
tomlkit==0.12.4
4748
typing_extensions==4.13.2
49+
tzlocal==5.3.1
4850
urllib3==2.6.3
4951
virtualenv==20.26.6

tests/integration/wait/test_podman_compose_wait.py

Lines changed: 195 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
from typing import Final
1111
from 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
1317
from packaging import version
18+
from tzlocal import get_localzone
1419

1520
from tests.integration.test_utils import RunSubprocessMixin
1621
from tests.integration.test_utils import get_podman_version
22+
from tests.integration.test_utils import is_systemd_available
1723
from tests.integration.test_utils import podman_compose_path
1824
from 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+
2798
class 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

Comments
 (0)