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