Skip to content

Commit 844c284

Browse files
committed
unit tests for bps
1 parent 2e6c1fa commit 844c284

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

tests/test_bps.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import argparse
2+
import pytest
3+
from unittest import mock
4+
from contextlib import contextmanager
5+
from typing import Any
6+
7+
from bps import ListPodsCommand, summarize_gpu_pods
8+
9+
10+
def create_pod(name: str, namespace: str, node: str, phase: str, gpu_count: int) -> mock.Mock:
11+
"""Helper to create a properly structured mock pod object."""
12+
pod = mock.Mock()
13+
14+
pod.model = mock.Mock()
15+
pod.model.metadata = mock.Mock()
16+
pod.model.metadata.name = name
17+
pod.model.metadata.namespace = namespace
18+
19+
pod.model.status = mock.Mock()
20+
pod.model.status.phase = phase
21+
22+
pod.model.spec = mock.Mock()
23+
pod.model.spec.nodeName = node
24+
25+
container = mock.Mock()
26+
container.resources = mock.Mock()
27+
container.resources.requests = {"nvidia.com/gpu": gpu_count} if gpu_count > 0 else {}
28+
29+
pod.model.spec.containers = [container]
30+
31+
return pod
32+
33+
34+
@pytest.fixture
35+
def args() -> argparse.Namespace:
36+
args = argparse.Namespace()
37+
args.verbose = 0
38+
args.node_names = []
39+
return args
40+
41+
42+
@contextmanager
43+
def patch_pods_selector(pods: list[Any]):
44+
with mock.patch("openshift_client.selector") as mock_selector:
45+
mock_result = mock.Mock(name="result")
46+
mock_result.objects.return_value = pods
47+
mock_selector.return_value = mock_result
48+
with mock.patch("openshift_client.timeout"):
49+
yield mock_selector
50+
51+
52+
def test_no_pods(args: argparse.Namespace, capsys):
53+
with patch_pods_selector([]):
54+
ListPodsCommand.run(args)
55+
captured = capsys.readouterr()
56+
assert captured.out == ""
57+
58+
59+
def test_list_gpu_pods_all_nodes(args: argparse.Namespace, capsys):
60+
pods = [
61+
create_pod("gpu-pod-1", "default", "node-a", "Running", 2),
62+
create_pod("gpu-pod-2", "ml-team", "node-b", "Running", 1),
63+
]
64+
with patch_pods_selector(pods):
65+
ListPodsCommand.run(args)
66+
captured = capsys.readouterr()
67+
assert "node-a: BUSY 2 default/gpu-pod-1" in captured.out
68+
assert "node-b: BUSY 1 ml-team/gpu-pod-2" in captured.out
69+
70+
71+
def test_list_gpu_pods_specific_node(args: argparse.Namespace, capsys):
72+
pods = [
73+
create_pod("gpu-pod-1", "default", "node-a", "Running", 2),
74+
create_pod("gpu-pod-2", "ml-team", "node-b", "Running", 1),
75+
]
76+
args.node_names = ["node-a"]
77+
with patch_pods_selector(pods):
78+
ListPodsCommand.run(args)
79+
captured = capsys.readouterr()
80+
assert "node-a: BUSY 2 default/gpu-pod-1" in captured.out
81+
assert "node-b" not in captured.out
82+
83+
84+
def test_list_gpu_pods_multiple_specific_nodes(args: argparse.Namespace, capsys):
85+
pods = [
86+
create_pod("gpu-pod-1", "default", "node-a", "Running", 2),
87+
create_pod("gpu-pod-2", "ml-team", "node-b", "Running", 1),
88+
]
89+
args.node_names = ["node-a", "node-b"]
90+
with patch_pods_selector(pods):
91+
ListPodsCommand.run(args)
92+
captured = capsys.readouterr()
93+
assert "node-a: BUSY 2 default/gpu-pod-1" in captured.out
94+
assert "node-b: BUSY 1 ml-team/gpu-pod-2" in captured.out
95+
96+
97+
def test_non_gpu_pods_not_shown(args: argparse.Namespace, capsys):
98+
pods = [create_pod("cpu-pod-1", "default", "node-c", "Running", 0)]
99+
with patch_pods_selector(pods):
100+
ListPodsCommand.run(args)
101+
captured = capsys.readouterr()
102+
assert captured.out == ""
103+
104+
105+
def test_non_gpu_pods_shown_with_verbose(args: argparse.Namespace, capsys):
106+
pods = [create_pod("cpu-pod-1", "default", "node-c", "Running", 0)]
107+
args.verbose = 1
108+
with patch_pods_selector(pods):
109+
ListPodsCommand.run(args)
110+
captured = capsys.readouterr()
111+
assert "node-c: FREE" in captured.out
112+
113+
114+
def test_pending_pods_ignored(args: argparse.Namespace, capsys):
115+
pods = [
116+
create_pod("pending-pod", "default", "node-a", "Pending", 1),
117+
create_pod("gpu-pod-1", "default", "node-a", "Running", 2),
118+
]
119+
with patch_pods_selector(pods):
120+
ListPodsCommand.run(args)
121+
captured = capsys.readouterr()
122+
assert "pending-pod" not in captured.out
123+
assert "node-a: BUSY 2 default/gpu-pod-1" in captured.out
124+
125+
126+
def test_multiple_pods_same_node(args: argparse.Namespace, capsys):
127+
pods = [
128+
create_pod("pod-1", "default", "node-a", "Running", 1),
129+
create_pod("pod-2", "default", "node-a", "Running", 2),
130+
]
131+
with patch_pods_selector(pods):
132+
ListPodsCommand.run(args)
133+
captured = capsys.readouterr()
134+
assert "node-a: BUSY 3" in captured.out
135+
assert "default/pod-1" in captured.out
136+
assert "default/pod-2" in captured.out
137+
138+
def test_summarize_gpu_pods_empty():
139+
result = summarize_gpu_pods([], verbose=False)
140+
assert result == []
141+
142+
def test_summarize_gpu_pods_with_gpu():
143+
pods = [create_pod("test-pod", "default", "node-a", "Running", 2)]
144+
result = summarize_gpu_pods(pods, verbose=False)
145+
assert len(result) == 1
146+
assert "node-a: BUSY 2 default/test-pod" in result[0]
147+
148+
def test_summarize_gpu_pods_multiple_containers():
149+
# base pod with one GPU container
150+
pod = create_pod("multi-gpu-pod", "default", "node-a", "Running", 1)
151+
152+
# add a second container requesting 2 GPUs
153+
container2 = mock.Mock()
154+
container2.resources = mock.Mock()
155+
container2.resources.requests = {"nvidia.com/gpu": 2}
156+
pod.model.spec.containers.append(container2)
157+
158+
result = summarize_gpu_pods([pod], verbose=False)
159+
160+
assert len(result) == 1
161+
assert "node-a: BUSY 3 default/multi-gpu-pod" in result[0]
162+
163+
164+
def test_list_pods_openshift_exception(args: argparse.Namespace, capsys):
165+
"""Test handling of OpenShift exceptions."""
166+
with mock.patch("openshift_client.selector") as mock_selector:
167+
with mock.patch("openshift_client.timeout"):
168+
import openshift_client as oc
169+
mock_selector.side_effect = oc.OpenShiftPythonException("Connection failed")
170+
171+
with pytest.raises(SystemExit):
172+
ListPodsCommand.run(args)

0 commit comments

Comments
 (0)