Skip to content

Commit 0c4d541

Browse files
committed
Add tests for KubernetesProvider submit
1 parent 86ade32 commit 0c4d541

File tree

2 files changed

+103
-1
lines changed

2 files changed

+103
-1
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ radical_local_test:
8484

8585
.PHONY: config_local_test
8686
config_local_test: $(CCTOOLS_INSTALL)
87-
pip3 install ".[monitoring,visualization,proxystore]"
87+
pip3 install ".[monitoring,visualization,proxystore,kubernetes]"
8888
PYTHONPATH=/tmp/cctools/lib/python3.8/site-packages pytest parsl/tests/ -k "not cleannet" --config local --random-order --durations 10
8989

9090
.PHONY: site_test
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import re
2+
from unittest import mock
3+
4+
import pytest
5+
6+
from parsl.providers.kubernetes.kube import KubernetesProvider
7+
from parsl.tests.test_utils.test_sanitize_dns import DNS_SUBDOMAIN_REGEX
8+
9+
_MOCK_BASE = "parsl.providers.kubernetes.kube"
10+
11+
12+
@pytest.fixture(autouse=True)
13+
def mock_kube_config():
14+
with mock.patch(f"{_MOCK_BASE}.config") as mock_config:
15+
mock_config.load_kube_config.return_value = None
16+
yield mock_config
17+
18+
19+
@pytest.fixture
20+
def mock_kube_client():
21+
mock_client = mock.MagicMock()
22+
with mock.patch(f"{_MOCK_BASE}.client.CoreV1Api") as mock_api:
23+
mock_api.return_value = mock_client
24+
yield mock_client
25+
26+
27+
@pytest.mark.local
28+
def test_submit_happy_path(mock_kube_client: mock.MagicMock):
29+
image = "test-image"
30+
namespace = "test-namespace"
31+
cmd_string = "test-command"
32+
volumes = [("test-volume", "test-mount-path")]
33+
service_account_name = "test-service-account"
34+
annotations = {"test-annotation": "test-value"}
35+
max_cpu = 2
36+
max_mem = "2Gi"
37+
init_cpu = 1
38+
init_mem = "1Gi"
39+
provider = KubernetesProvider(
40+
image=image,
41+
persistent_volumes=volumes,
42+
namespace=namespace,
43+
service_account_name=service_account_name,
44+
annotations=annotations,
45+
max_cpu=max_cpu,
46+
max_mem=max_mem,
47+
init_cpu=init_cpu,
48+
init_mem=init_mem,
49+
)
50+
51+
job_name = "test.job.name"
52+
job_id = provider.submit(cmd_string=cmd_string, tasks_per_node=1, job_name=job_name)
53+
54+
assert job_id in provider.resources
55+
assert mock_kube_client.create_namespaced_pod.call_count == 1
56+
57+
call_args = mock_kube_client.create_namespaced_pod.call_args[1]
58+
pod = call_args["body"]
59+
container = pod.spec.containers[0]
60+
volume = container.volume_mounts[0]
61+
62+
assert image == container.image
63+
assert namespace == call_args["namespace"]
64+
assert any(cmd_string in arg for arg in container.args)
65+
assert volumes[0] == (volume.name, volume.mount_path)
66+
assert service_account_name == pod.spec.service_account_name
67+
assert annotations == pod.metadata.annotations
68+
assert str(max_cpu) == container.resources.limits["cpu"]
69+
assert max_mem == container.resources.limits["memory"]
70+
assert str(init_cpu) == container.resources.requests["cpu"]
71+
assert init_mem == container.resources.requests["memory"]
72+
assert job_id == pod.metadata.labels["parsl-job-id"]
73+
assert job_id == container.name
74+
assert f"{job_name}.{job_id}" == pod.metadata.name
75+
76+
77+
@pytest.mark.local
78+
@mock.patch(f"{_MOCK_BASE}.KubernetesProvider._create_pod")
79+
@pytest.mark.parametrize("char", (".", "-"))
80+
def test_submit_pod_name_includes_job_id(mock_create_pod: mock.MagicMock, char: str):
81+
provider = KubernetesProvider(image="test-image")
82+
83+
job_name = "a." * 121 + f"a{char}" + "a" * 9
84+
assert len(job_name) == 253 # Max length for pod name
85+
job_id = provider.submit(cmd_string="test-command", tasks_per_node=1, job_name=job_name)
86+
87+
expected_pod_name = job_name[:253 - len(job_id) - 2] + f".{job_id}"
88+
actual_pod_name = mock_create_pod.call_args[1]["pod_name"]
89+
assert re.match(DNS_SUBDOMAIN_REGEX, actual_pod_name)
90+
assert expected_pod_name == actual_pod_name
91+
92+
93+
@pytest.mark.local
94+
@mock.patch(f"{_MOCK_BASE}.KubernetesProvider._create_pod")
95+
@mock.patch(f"{_MOCK_BASE}.logger")
96+
@pytest.mark.parametrize("job_name", ("", ".", "-", "a.-.a", "$$$"))
97+
def test_submit_invalid_job_name(mock_logger: mock.MagicMock, mock_create_pod: mock.MagicMock, job_name: str):
98+
provider = KubernetesProvider(image="test-image")
99+
job_id = provider.submit(cmd_string="test-command", tasks_per_node=1, job_name=job_name)
100+
assert mock_logger.warning.call_count == 1
101+
assert f"Invalid pod name '{job_name}' for job '{job_id}'" in mock_logger.warning.call_args[0][0]
102+
assert f"parsl.kube.{job_id}" == mock_create_pod.call_args[1]["pod_name"]

0 commit comments

Comments
 (0)