Skip to content

Commit bc8b538

Browse files
committed
tests for groupedjob failing
1 parent 1984159 commit bc8b538

File tree

3 files changed

+243
-7
lines changed

3 files changed

+243
-7
lines changed

poetry.lock

Lines changed: 25 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ slack-sdk = "^3.21.3"
3535
pandas = "2.2.2"
3636
requests = ">2.32.4"
3737
pyyaml = "6.0.1"
38-
typing-extensions = "4.6.0"
38+
typing-extensions = "^4.12.0"
3939
idna = "3.7"
4040
urllib3 = "^1.26.20"
4141
setuptools = "^80.9.0"
@@ -54,11 +54,15 @@ types-cachetools = "^5.3.0.4"
5454
types-requests = "^2.28.11.15"
5555
pyinstaller = "^5.9.0"
5656
pytest = "^7.2.2"
57+
pytest-asyncio = "^0.23.0"
5758

5859
[build-system]
5960
requires = ["poetry-core"]
6061
build-backend = "poetry.core.masonry.api"
6162

6263

64+
[tool.pytest.ini_options]
65+
asyncio_mode = "auto"
66+
6367
[project]
6468
name = "robusta_krr"

tests/test_grouped_jobs.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import pytest
2+
from unittest.mock import AsyncMock, MagicMock, patch
3+
from collections import defaultdict
4+
5+
from robusta_krr.core.integrations.kubernetes import KubernetesLoader
6+
from robusta_krr.core.models.config import Config
7+
from robusta_krr.api.models import K8sObjectData
8+
9+
10+
@pytest.fixture
11+
def mock_config():
12+
"""Mock config with job grouping settings"""
13+
config = MagicMock(spec=Config)
14+
config.job_grouping_labels = ["app", "team"]
15+
config.job_grouping_limit = 3 # Small limit for testing
16+
return config
17+
18+
19+
@pytest.fixture
20+
def mock_kubernetes_loader(mock_config):
21+
"""Create a KubernetesLoader instance with mocked dependencies"""
22+
with patch("robusta_krr.core.integrations.kubernetes.settings", mock_config):
23+
loader = KubernetesLoader()
24+
loader.batch = MagicMock()
25+
loader.core = MagicMock()
26+
loader.executor = MagicMock()
27+
return loader
28+
29+
30+
def create_mock_job(name: str, namespace: str, labels: dict):
31+
"""Create a mock V1Job object"""
32+
job = MagicMock()
33+
job.metadata.name = name
34+
job.metadata.namespace = namespace
35+
job.metadata.labels = labels
36+
job.metadata.owner_references = []
37+
job.spec.template.spec.containers = [MagicMock()]
38+
return job
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_list_all_groupedjobs_with_limit(mock_kubernetes_loader, mock_config):
43+
"""Test that _list_all_groupedjobs respects the job_grouping_limit"""
44+
45+
# Create mock jobs - more than the limit (3)
46+
mock_jobs = [
47+
create_mock_job("job-1", "default", {"app": "frontend"}),
48+
create_mock_job("job-2", "default", {"app": "frontend"}),
49+
create_mock_job("job-3", "default", {"app": "frontend"}),
50+
create_mock_job("job-4", "default", {"app": "frontend"}), # This should be excluded
51+
create_mock_job("job-5", "default", {"app": "frontend"}), # This should be excluded
52+
create_mock_job("job-6", "default", {"app": "backend"}),
53+
create_mock_job("job-7", "default", {"app": "backend"}),
54+
create_mock_job("job-8", "default", {"app": "backend"}),
55+
create_mock_job("job-9", "default", {"app": "backend"}), # This should be excluded
56+
]
57+
58+
# Mock the _list_namespaced_or_global_objects method
59+
mock_kubernetes_loader._list_namespaced_or_global_objects = AsyncMock(return_value=mock_jobs)
60+
61+
# Mock the __build_scannable_object method
62+
def mock_build_scannable_object(item, container, kind):
63+
obj = MagicMock()
64+
obj._api_resource = MagicMock()
65+
return obj
66+
67+
mock_kubernetes_loader._KubernetesLoader__build_scannable_object = mock_build_scannable_object
68+
69+
# Call the method
70+
result = await mock_kubernetes_loader._list_all_groupedjobs()
71+
72+
# Verify we got 2 groups (frontend and backend)
73+
assert len(result) == 2
74+
75+
# Find the frontend group
76+
frontend_group = next((g for g in result if g.name == "app=frontend"), None)
77+
assert frontend_group is not None
78+
assert frontend_group.namespace == "default"
79+
80+
# Verify the frontend group is limited to 3 jobs (the limit)
81+
assert len(frontend_group._api_resource._grouped_jobs) == 3
82+
assert frontend_group._api_resource._grouped_jobs[0].metadata.name == "job-1"
83+
assert frontend_group._api_resource._grouped_jobs[1].metadata.name == "job-2"
84+
assert frontend_group._api_resource._grouped_jobs[2].metadata.name == "job-3"
85+
86+
# Find the backend group
87+
backend_group = next((g for g in result if g.name == "app=backend"), None)
88+
assert backend_group is not None
89+
assert backend_group.namespace == "default"
90+
91+
# Verify the backend group is also limited to 3 jobs
92+
assert len(backend_group._api_resource._grouped_jobs) == 3
93+
assert backend_group._api_resource._grouped_jobs[0].metadata.name == "job-6"
94+
assert backend_group._api_resource._grouped_jobs[1].metadata.name == "job-7"
95+
assert backend_group._api_resource._grouped_jobs[2].metadata.name == "job-8"
96+
97+
98+
@pytest.mark.asyncio
99+
async def test_list_all_groupedjobs_with_different_namespaces(mock_kubernetes_loader, mock_config):
100+
"""Test that GroupedJob objects are created separately for different namespaces"""
101+
102+
# Create mock jobs in different namespaces
103+
mock_jobs = [
104+
create_mock_job("job-1", "namespace-1", {"app": "frontend"}),
105+
create_mock_job("job-2", "namespace-1", {"app": "frontend"}),
106+
create_mock_job("job-3", "namespace-2", {"app": "frontend"}),
107+
create_mock_job("job-4", "namespace-2", {"app": "frontend"}),
108+
]
109+
110+
mock_kubernetes_loader._list_namespaced_or_global_objects = AsyncMock(return_value=mock_jobs)
111+
112+
def mock_build_scannable_object(item, container, kind):
113+
obj = MagicMock()
114+
obj._api_resource = MagicMock()
115+
return obj
116+
117+
mock_kubernetes_loader._KubernetesLoader__build_scannable_object = mock_build_scannable_object
118+
119+
# Call the method
120+
result = await mock_kubernetes_loader._list_all_groupedjobs()
121+
122+
# Verify we got 2 groups (one per namespace)
123+
assert len(result) == 2
124+
125+
# Check namespace-1 group
126+
ns1_group = next((g for g in result if g.namespace == "namespace-1"), None)
127+
assert ns1_group is not None
128+
assert ns1_group.name == "app=frontend"
129+
assert len(ns1_group._api_resource._grouped_jobs) == 2
130+
131+
# Check namespace-2 group
132+
ns2_group = next((g for g in result if g.namespace == "namespace-2"), None)
133+
assert ns2_group is not None
134+
assert ns2_group.name == "app=frontend"
135+
assert len(ns2_group._api_resource._grouped_jobs) == 2
136+
137+
138+
@pytest.mark.asyncio
139+
async def test_list_all_groupedjobs_with_cronjob_owner_reference(mock_kubernetes_loader, mock_config):
140+
"""Test that jobs with CronJob owner references are excluded"""
141+
142+
# Create mock jobs - one with CronJob owner, one without
143+
mock_jobs = [
144+
create_mock_job("job-1", "default", {"app": "frontend"}),
145+
create_mock_job("job-2", "default", {"app": "frontend"}),
146+
]
147+
148+
# Add CronJob owner reference to the second job
149+
mock_jobs[1].metadata.owner_references = [MagicMock(kind="CronJob")]
150+
151+
mock_kubernetes_loader._list_namespaced_or_global_objects = AsyncMock(return_value=mock_jobs)
152+
153+
def mock_build_scannable_object(item, container, kind):
154+
obj = MagicMock()
155+
obj._api_resource = MagicMock()
156+
return obj
157+
158+
mock_kubernetes_loader._KubernetesLoader__build_scannable_object = mock_build_scannable_object
159+
160+
# Call the method
161+
result = await mock_kubernetes_loader._list_all_groupedjobs()
162+
163+
# Verify we got 1 group with only 1 job (the one without CronJob owner)
164+
assert len(result) == 1
165+
group = result[0]
166+
assert group.name == "app=frontend"
167+
assert len(group._api_resource._grouped_jobs) == 1
168+
assert group._api_resource._grouped_jobs[0].metadata.name == "job-1"
169+
170+
171+
@pytest.mark.asyncio
172+
async def test_list_all_groupedjobs_no_grouping_labels(mock_kubernetes_loader):
173+
"""Test that no GroupedJob objects are created when no grouping labels are configured"""
174+
175+
# Mock config with no grouping labels
176+
mock_config_no_labels = MagicMock(spec=Config)
177+
mock_config_no_labels.job_grouping_labels = None
178+
179+
with patch("robusta_krr.core.integrations.kubernetes.settings", mock_config_no_labels):
180+
result = await mock_kubernetes_loader._list_all_groupedjobs()
181+
assert len(result) == 0
182+
183+
184+
@pytest.mark.asyncio
185+
async def test_list_all_groupedjobs_multiple_labels(mock_kubernetes_loader, mock_config):
186+
"""Test that jobs with different grouping labels create separate groups"""
187+
188+
# Create mock jobs with different labels
189+
mock_jobs = [
190+
create_mock_job("job-1", "default", {"app": "frontend"}),
191+
create_mock_job("job-2", "default", {"team": "backend"}),
192+
create_mock_job("job-3", "default", {"app": "api"}),
193+
]
194+
195+
mock_kubernetes_loader._list_namespaced_or_global_objects = AsyncMock(return_value=mock_jobs)
196+
197+
def mock_build_scannable_object(item, container, kind):
198+
obj = MagicMock()
199+
obj._api_resource = MagicMock()
200+
return obj
201+
202+
mock_kubernetes_loader._KubernetesLoader__build_scannable_object = mock_build_scannable_object
203+
204+
# Call the method
205+
result = await mock_kubernetes_loader._list_all_groupedjobs()
206+
207+
# Verify we got 3 groups (one for each label+value combination)
208+
assert len(result) == 3
209+
210+
group_names = {g.name for g in result}
211+
assert "app=frontend" in group_names
212+
assert "team=backend" in group_names
213+
assert "app=api" in group_names

0 commit comments

Comments
 (0)