This commit is contained in:
Justin Zhang 2025-04-15 14:30:09 +00:00 committed by GitHub
commit 44102dc79f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 196 additions and 34 deletions

View File

@ -23,6 +23,20 @@ repos:
"-sn", # Don't display the score "-sn", # Don't display the score
"--rcfile=.pylintrc", # Link to your config file "--rcfile=.pylintrc", # Link to your config file
] ]
files: ^[^/]+\.py$
# Hook for test code with different config
- id: pylint
name: pylint-tests
entry: pylint
language: system
types: [python]
args:
[
"-rn", # Only display messages
"-sn", # Don't display the score
"--rcfile=tests/.pylintrc" # Relax the lint rules for test code
]
files: ^tests/
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.2.5 rev: v2.2.5
hooks: hooks:

View File

@ -2707,6 +2707,18 @@ async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None:
deps_cd = [] deps_cd = []
for d in deps: for d in deps:
if d.condition == condition: if d.condition == condition:
if (
d.condition
in (ServiceDependencyCondition.HEALTHY, ServiceDependencyCondition.UNHEALTHY)
) and strverscmp_lt(compose.podman_version, "4.6.0"):
log.warning(
"Ignored %s condition check due to podman %s doesn't support %s!",
d.name,
compose.podman_version,
condition.value,
)
continue
deps_cd.extend(compose.container_names_by_service[d.name]) deps_cd.extend(compose.container_names_by_service[d.name])
if deps_cd: if deps_cd:
@ -2783,28 +2795,25 @@ async def compose_up(compose: PodmanCompose, args):
.splitlines() .splitlines()
) )
diff_hashes = [i for i in hashes if i and i != compose.yaml_hash] diff_hashes = [i for i in hashes if i and i != compose.yaml_hash]
if args.force_recreate or len(diff_hashes): if (args.force_recreate and len(hashes) > 0) or len(diff_hashes):
log.info("recreating: ...") log.info("recreating: ...")
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None)) down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
await compose.commands["down"](compose, down_args) await compose.commands["down"](compose, down_args)
log.info("recreating: done\n\n") log.info("recreating: done\n\n")
# args.no_recreate disables check for changes (which is not implemented) # args.no_recreate disables check for changes (which is not implemented)
podman_command = "run" if args.detach and not args.no_start else "create"
await create_pods(compose, args) await create_pods(compose, args)
for cnt in compose.containers: for cnt in compose.containers:
if cnt["_service"] in excluded: if cnt["_service"] in excluded:
log.debug("** skipping: %s", cnt["name"]) log.debug("** skipping: %s", cnt["name"])
continue continue
podman_args = await container_to_args( podman_args = await container_to_args(compose, cnt, detached=False, no_deps=args.no_deps)
compose, cnt, detached=args.detach, no_deps=args.no_deps subproc = await compose.podman.run([], "create", podman_args)
) if not args.no_start and args.detach and subproc is not None:
subproc = await compose.podman.run([], podman_command, podman_args)
if podman_command == "run" and subproc is not None:
await run_container( await run_container(
compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]]) compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]])
) )
if args.no_start or args.detach or args.dry_run: if args.no_start or args.detach or args.dry_run:
return return
# TODO: handle already existing # TODO: handle already existing

18
tests/.pylintrc Normal file
View File

@ -0,0 +1,18 @@
[MESSAGES CONTROL]
# C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin
# consider-using-with: we need it for color formatter pipe
disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment,broad-exception-caught,duplicate-code,missing-timeout
# allow _ for ignored variables
# allow generic names like a,b,c and i,j,k,l,m,n and x,y,z
# allow k,v for key/value
# allow e for exceptions, it for iterator, ix for index
# allow ip for ip address
# allow w,h for width, height
# allow op for operation/operator/opcode
# allow t, t0, t1, t2, and t3 for time
# allow dt for delta time
# allow db for database
# allow ls for list
# allow p for pipe
# allow ex for examples, exists ..etc
good-names=_,a,b,c,dt,db,e,f,fn,fd,i,j,k,v,kv,kw,l,m,n,ls,t,t0,t1,t2,t3,w,h,x,y,z,it,ix,ip,op,p,ex

View File

@ -0,0 +1,23 @@
version: "3.7"
services:
web:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
tmpfs:
- /run
- /tmp
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"]
start_period: 10s # initialization time for containers that need time to bootstrap
interval: 10s # Time between health checks
timeout: 5s # Time to wait for a response
retries: 3 # Number of consecutive failures before marking as unhealthy
sleep:
image: nopush/podman-compose-test
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"]
depends_on:
web:
condition: service_healthy
tmpfs:
- /run
- /tmp

View File

@ -1,8 +1,10 @@
# SPDX-License-Identifier: GPL-2.0 # SPDX-License-Identifier: GPL-2.0
import os import os
import time
import unittest import unittest
from tests.integration.test_utils import RunSubprocessMixin from tests.integration.test_utils import RunSubprocessMixin
from tests.integration.test_utils import is_systemd_available
from tests.integration.test_utils import podman_compose_path from tests.integration.test_utils import podman_compose_path
from tests.integration.test_utils import test_path from tests.integration.test_utils import test_path
@ -14,7 +16,7 @@ def compose_yaml_path(suffix=""):
class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin): class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
def test_deps(self): def test_deps(self):
try: try:
output, error = self.run_subprocess_assert_returncode([ output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(), podman_compose_path(),
"-f", "-f",
compose_yaml_path(), compose_yaml_path(),
@ -37,7 +39,7 @@ class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
def test_run_nodeps(self): def test_run_nodeps(self):
try: try:
output, error = self.run_subprocess_assert_returncode([ output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(), podman_compose_path(),
"-f", "-f",
compose_yaml_path(), compose_yaml_path(),
@ -71,7 +73,7 @@ class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
"--detach", "--detach",
"sleep", "sleep",
]) ])
output, error = self.run_subprocess_assert_returncode([ output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(), podman_compose_path(),
"-f", "-f",
compose_yaml_path(), compose_yaml_path(),
@ -144,7 +146,7 @@ class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
def test_deps_succeeds(self): def test_deps_succeeds(self):
suffix = "-conditional-succeeds" suffix = "-conditional-succeeds"
try: try:
output, error = self.run_subprocess_assert_returncode([ output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(), podman_compose_path(),
"-f", "-f",
compose_yaml_path(suffix), compose_yaml_path(suffix),
@ -168,7 +170,7 @@ class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
def test_deps_fails(self): def test_deps_fails(self):
suffix = "-conditional-fails" suffix = "-conditional-fails"
try: try:
output, error = self.run_subprocess_assert_returncode([ output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(), podman_compose_path(),
"-f", "-f",
compose_yaml_path(suffix), compose_yaml_path(suffix),
@ -183,3 +185,74 @@ class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
compose_yaml_path(suffix), compose_yaml_path(suffix),
"down", "down",
]) ])
class TestComposeConditionalDepsHealthy(unittest.TestCase, RunSubprocessMixin):
def test_up_deps_healthy(self):
suffix = "-conditional-healthy"
try:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(suffix),
"up",
"sleep",
"--detach",
])
# The `podman wait --condition=healthy` is invalid prior to 4.6.0.
# Since the podman-compose project uses podman 4.3.1 in github actions, we
# use sleep as workaround to wait until the `sleep` container becomes running.
time.sleep(3)
# self.run_subprocess_assert_returncode([
# "podman",
# "wait",
# "--condition=running",
# "deps_web_1",
# "deps_sleep_1",
# ])
# check both web and sleep are running
output, _ = self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"ps",
"--format",
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.StartedAt}}",
])
# extract container id of web
decoded_out = output.decode('utf-8')
web_cnt_id = ""
web_cnt_name = ""
web_cnt_status = ""
web_cnt_started = ""
sleep_cnt_id = ""
sleep_cnt_name = ""
sleep_cnt_started = ""
for line in decoded_out.split("\n"):
if "web" in line:
web_cnt_id, web_cnt_name, web_cnt_status, web_cnt_started = line.split("\t")
if "sleep" in line:
sleep_cnt_id, sleep_cnt_name, _, sleep_cnt_started = line.split("\t")
self.assertNotEqual("", web_cnt_id)
self.assertEqual("deps_web_1", web_cnt_name)
self.assertNotEqual("", sleep_cnt_id)
self.assertEqual("deps_sleep_1", sleep_cnt_name)
# When test case is executed inside container like github actions, the absence of
# systemd prevents health check from working properly, resulting in failure to
# transit to healthy state. As a result, we only assert the `healthy` state where
# systemd is functioning.
if is_systemd_available():
self.assertIn("healthy", web_cnt_status)
self.assertGreaterEqual(int(sleep_cnt_started), int(web_cnt_started))
finally:
self.run_subprocess_assert_returncode([
podman_compose_path(),
"-f",
compose_yaml_path(),
"down",
])

View File

@ -80,18 +80,25 @@ class TestComposeExteds(unittest.TestCase, RunSubprocessMixin):
"env1", "env1",
]) ])
lines = output.decode('utf-8').split('\n') lines = output.decode('utf-8').split('\n')
# HOSTNAME name is random string so is ignored in asserting # Test selected env variables to improve robustness
lines = sorted([line for line in lines if not line.startswith("HOSTNAME")]) lines = sorted([
line
for line in lines
if line.startswith("BAR")
or line.startswith("BAZ")
or line.startswith("FOO")
or line.startswith("HOME")
or line.startswith("PATH")
or line.startswith("container")
])
self.assertEqual( self.assertEqual(
lines, lines,
[ [
'',
'BAR=local', 'BAR=local',
'BAZ=local', 'BAZ=local',
'FOO=original', 'FOO=original',
'HOME=/root', 'HOME=/root',
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'TERM=xterm',
'container=podman', 'container=podman',
], ],
) )

View File

@ -35,8 +35,7 @@ class TestFilesystem(unittest.TestCase, RunSubprocessMixin):
"container1", "container1",
]) ])
# BUG: figure out why cat is called twice self.assertEqual(out, b'data_compose_symlink\n')
self.assertEqual(out, b'data_compose_symlink\ndata_compose_symlink\n')
finally: finally:
out, _ = self.run_subprocess_assert_returncode([ out, _ = self.run_subprocess_assert_returncode([

View File

@ -59,9 +59,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
) )
# check if Host port is the same as provided by the service port # check if Host port is the same as provided by the service port
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual( self.assertEqual(
container_info['NetworkSettings']["Ports"], container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
) )
self.assertEqual(container_info["Config"]["Hostname"], "web1") self.assertEqual(container_info["Config"]["Hostname"], "web1")
@ -77,9 +81,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default" list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default"
) )
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual( self.assertEqual(
container_info['NetworkSettings']["Ports"], container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
) )
self.assertEqual(container_info["Config"]["Hostname"], "web2") self.assertEqual(container_info["Config"]["Hostname"], "web2")

View File

@ -59,9 +59,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
) )
# check if Host port is the same as prodvided by the service port # check if Host port is the same as prodvided by the service port
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual( self.assertEqual(
container_info['NetworkSettings']["Ports"], container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
) )
self.assertEqual(container_info["Config"]["Hostname"], "web1") self.assertEqual(container_info["Config"]["Hostname"], "web1")
@ -78,9 +82,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack" list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack"
) )
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
self.assertIsNotNone(
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
)
self.assertEqual( self.assertEqual(
container_info['NetworkSettings']["Ports"], container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
) )
self.assertEqual(container_info["Config"]["Hostname"], "web2") self.assertEqual(container_info["Config"]["Hostname"], "web2")

View File

@ -21,6 +21,14 @@ def podman_compose_path():
return os.path.join(base_path(), "podman_compose.py") return os.path.join(base_path(), "podman_compose.py")
def is_systemd_available():
try:
with open("/proc/1/comm", "r", encoding="utf-8") as fh:
return fh.read().strip() == "systemd"
except FileNotFoundError:
return False
class RunSubprocessMixin: class RunSubprocessMixin:
def is_debug_enabled(self): def is_debug_enabled(self):
return "TESTS_DEBUG" in os.environ return "TESTS_DEBUG" in os.environ

View File

@ -34,14 +34,11 @@ class TestUlimit(unittest.TestCase, RunSubprocessMixin):
for el in split_output for el in split_output
if not el.startswith(b"soft process") and not el.startswith(b"hard process") if not el.startswith(b"soft process") and not el.startswith(b"hard process")
] ]
# BUG: figure out why echo is called twice
self.assertEqual( self.assertEqual(
output_part, output_part,
[ [
b"soft nofile limit 1001", b"soft nofile limit 1001",
b"hard nofile limit 1001", b"hard nofile limit 1001",
b"soft nofile limit 1001",
b"hard nofile limit 1001",
], ],
) )
@ -53,8 +50,7 @@ class TestUlimit(unittest.TestCase, RunSubprocessMixin):
self.assertEqual( self.assertEqual(
out, out,
b"soft process limit 1002\nhard process limit 2002\nsoft nofile limit 1002\n" b"soft process limit 1002\nhard process limit 2002\nsoft nofile limit 1002\n"
b"hard nofile limit 1002\nsoft process limit 1002\nhard process limit 2002\n" b"hard nofile limit 1002\n",
b"soft nofile limit 1002\nhard nofile limit 1002\n",
) )
out, _ = self.run_subprocess_assert_returncode([ out, _ = self.run_subprocess_assert_returncode([
@ -65,8 +61,7 @@ class TestUlimit(unittest.TestCase, RunSubprocessMixin):
self.assertEqual( self.assertEqual(
out, out,
b"soft process limit 1003\nhard process limit 2003\nsoft nofile limit 1003\n" b"soft process limit 1003\nhard process limit 2003\nsoft nofile limit 1003\n"
b"hard nofile limit 1003\nsoft process limit 1003\nhard process limit 2003\n" b"hard nofile limit 1003\n",
b"soft nofile limit 1003\nhard nofile limit 1003\n",
) )
finally: finally:
self.run_subprocess_assert_returncode([ self.run_subprocess_assert_returncode([