Skip to content

Commit 4686e9c

Browse files
Merge pull request #835 from yasinBursali/test/resources-endpoint-coverage
test: add unit tests for resources.py endpoint
2 parents 399473c + 4a27e70 commit 4686e9c

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
"""Tests for routers/resources.py — per-service resource metrics."""
2+
3+
import json
4+
import urllib.error
5+
from unittest.mock import MagicMock, patch
6+
7+
from routers.resources import _scan_service_disk, _fetch_container_stats
8+
9+
10+
# ---------------------------------------------------------------------------
11+
# _scan_service_disk
12+
# ---------------------------------------------------------------------------
13+
14+
15+
class TestScanServiceDisk:
16+
17+
def test_nonexistent_data_dir(self, monkeypatch):
18+
"""Nonexistent DATA_DIR returns empty dict."""
19+
monkeypatch.setattr("routers.resources.DATA_DIR", "/nonexistent/path")
20+
result = _scan_service_disk()
21+
assert result == {}
22+
23+
def test_empty_data_dir(self, tmp_path, monkeypatch):
24+
"""Empty DATA_DIR returns empty dict."""
25+
monkeypatch.setattr("routers.resources.DATA_DIR", str(tmp_path))
26+
result = _scan_service_disk()
27+
assert result == {}
28+
29+
def test_known_dir_mapped_to_service(self, tmp_path, monkeypatch):
30+
"""Known dir name 'models' is mapped to 'llama-server'."""
31+
monkeypatch.setattr("routers.resources.DATA_DIR", str(tmp_path))
32+
(tmp_path / "models").mkdir()
33+
monkeypatch.setattr("routers.resources.dir_size_gb", lambda p: 5.2)
34+
35+
result = _scan_service_disk()
36+
assert "llama-server" in result
37+
assert result["llama-server"]["data_gb"] == 5.2
38+
assert result["llama-server"]["path"] == "data/models"
39+
40+
def test_unknown_dir_passes_through(self, tmp_path, monkeypatch):
41+
"""Unknown dir name passes through as-is for service_id."""
42+
monkeypatch.setattr("routers.resources.DATA_DIR", str(tmp_path))
43+
(tmp_path / "custom-service").mkdir()
44+
monkeypatch.setattr("routers.resources.dir_size_gb", lambda p: 1.5)
45+
46+
result = _scan_service_disk()
47+
assert "custom-service" in result
48+
assert result["custom-service"]["data_gb"] == 1.5
49+
assert result["custom-service"]["path"] == "data/custom-service"
50+
51+
def test_zero_size_dir_excluded(self, tmp_path, monkeypatch):
52+
"""Directories with size_gb == 0 are excluded from results."""
53+
monkeypatch.setattr("routers.resources.DATA_DIR", str(tmp_path))
54+
(tmp_path / "models").mkdir()
55+
monkeypatch.setattr("routers.resources.dir_size_gb", lambda p: 0)
56+
57+
result = _scan_service_disk()
58+
assert result == {}
59+
60+
def test_files_in_data_dir_skipped(self, tmp_path, monkeypatch):
61+
"""Regular files in DATA_DIR are skipped, only directories scanned."""
62+
monkeypatch.setattr("routers.resources.DATA_DIR", str(tmp_path))
63+
(tmp_path / "somefile.txt").write_text("not a directory")
64+
65+
result = _scan_service_disk()
66+
assert result == {}
67+
68+
69+
# ---------------------------------------------------------------------------
70+
# _fetch_container_stats
71+
# ---------------------------------------------------------------------------
72+
73+
74+
class TestFetchContainerStats:
75+
76+
def test_valid_json_response(self):
77+
"""Valid JSON with containers key returns the list."""
78+
containers = [{"container_name": "dream-llama", "cpu_percent": 45.0}]
79+
body = json.dumps({"containers": containers}).encode()
80+
81+
mock_resp = MagicMock()
82+
mock_resp.read.return_value = body
83+
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
84+
mock_resp.__exit__ = MagicMock(return_value=False)
85+
86+
with patch("routers.resources.urllib.request.urlopen", return_value=mock_resp):
87+
result = _fetch_container_stats()
88+
assert result == containers
89+
90+
def test_host_agent_unreachable(self):
91+
"""URLError (host agent unreachable) returns empty list."""
92+
with patch("routers.resources.urllib.request.urlopen",
93+
side_effect=urllib.error.URLError("Connection refused")):
94+
result = _fetch_container_stats()
95+
assert result == []
96+
97+
def test_http_500_error(self):
98+
"""HTTPError (server error) returns empty list."""
99+
with patch("routers.resources.urllib.request.urlopen",
100+
side_effect=urllib.error.HTTPError(
101+
url="http://test", code=500, msg="Internal Server Error",
102+
hdrs=None, fp=None)):
103+
result = _fetch_container_stats()
104+
assert result == []
105+
106+
def test_os_error(self):
107+
"""OSError (network unreachable) returns empty list."""
108+
with patch("routers.resources.urllib.request.urlopen",
109+
side_effect=OSError("Network is unreachable")):
110+
result = _fetch_container_stats()
111+
assert result == []
112+
113+
114+
# ---------------------------------------------------------------------------
115+
# service_resources endpoint
116+
# ---------------------------------------------------------------------------
117+
118+
119+
class TestServiceResources:
120+
121+
@staticmethod
122+
def _clear_resource_cache():
123+
"""Remove cached resource entries so each test gets fresh data."""
124+
from main import _cache
125+
_cache._store.pop("service_resources_containers", None)
126+
_cache._store.pop("service_resources_disk", None)
127+
128+
def test_requires_auth(self, test_client):
129+
"""GET /api/services/resources without auth header returns 401."""
130+
resp = test_client.get("/api/services/resources")
131+
assert resp.status_code == 401
132+
133+
def test_full_response_with_stats_and_disk(self, test_client, monkeypatch):
134+
"""Merges container stats and disk data into per-service entries."""
135+
self._clear_resource_cache()
136+
137+
fake_services = {
138+
"llama-server": {"name": "Llama Server", "container_name": "dream-llama"},
139+
"open-webui": {"name": "Open WebUI", "container_name": "dream-webui"},
140+
}
141+
monkeypatch.setattr("routers.resources.SERVICES", fake_services)
142+
monkeypatch.setattr("routers.resources.GPU_BACKEND", "nvidia")
143+
144+
fake_stats = [
145+
{"container_name": "dream-llama", "cpu_percent": 45.0, "memory_used_mb": 1024},
146+
{"container_name": "dream-webui", "cpu_percent": 10.0, "memory_used_mb": 256},
147+
]
148+
fake_disk = {
149+
"llama-server": {"data_gb": 16.5, "path": "data/models"},
150+
}
151+
152+
with patch("routers.resources._fetch_container_stats", return_value=fake_stats), \
153+
patch("routers.resources._scan_service_disk", return_value=fake_disk):
154+
resp = test_client.get(
155+
"/api/services/resources",
156+
headers=test_client.auth_headers,
157+
)
158+
159+
assert resp.status_code == 200
160+
data = resp.json()
161+
assert "services" in data
162+
assert "totals" in data
163+
assert "caveats" in data
164+
165+
ids = [s["id"] for s in data["services"]]
166+
assert "llama-server" in ids
167+
assert "open-webui" in ids
168+
169+
llama = next(s for s in data["services"] if s["id"] == "llama-server")
170+
assert llama["container"]["cpu_percent"] == 45.0
171+
assert llama["disk"]["data_gb"] == 16.5
172+
173+
webui = next(s for s in data["services"] if s["id"] == "open-webui")
174+
assert webui["container"]["cpu_percent"] == 10.0
175+
assert webui["disk"] is None
176+
177+
assert data["totals"]["cpu_percent"] == 55.0
178+
assert data["totals"]["memory_used_mb"] == 1280
179+
assert data["totals"]["disk_data_gb"] == 16.5
180+
assert data["caveats"]["docker_desktop_memory"] is False
181+
182+
def test_host_agent_down_disk_only(self, test_client, monkeypatch):
183+
"""When host agent returns no stats, response has disk data only."""
184+
self._clear_resource_cache()
185+
186+
fake_services = {
187+
"llama-server": {"name": "Llama Server", "container_name": "dream-llama"},
188+
}
189+
monkeypatch.setattr("routers.resources.SERVICES", fake_services)
190+
monkeypatch.setattr("routers.resources.GPU_BACKEND", "nvidia")
191+
192+
fake_disk = {"llama-server": {"data_gb": 8.0, "path": "data/models"}}
193+
194+
with patch("routers.resources._fetch_container_stats", return_value=[]), \
195+
patch("routers.resources._scan_service_disk", return_value=fake_disk):
196+
resp = test_client.get(
197+
"/api/services/resources",
198+
headers=test_client.auth_headers,
199+
)
200+
201+
assert resp.status_code == 200
202+
data = resp.json()
203+
llama = next(s for s in data["services"] if s["id"] == "llama-server")
204+
assert llama["container"] is None
205+
assert llama["disk"]["data_gb"] == 8.0
206+
assert data["totals"]["cpu_percent"] == 0
207+
assert data["totals"]["memory_used_mb"] == 0
208+
209+
def test_apple_backend_sets_caveat(self, test_client, monkeypatch):
210+
"""GPU_BACKEND=apple sets docker_desktop_memory caveat to True."""
211+
self._clear_resource_cache()
212+
213+
monkeypatch.setattr("routers.resources.SERVICES", {})
214+
monkeypatch.setattr("routers.resources.GPU_BACKEND", "apple")
215+
216+
with patch("routers.resources._fetch_container_stats", return_value=[]), \
217+
patch("routers.resources._scan_service_disk", return_value={}):
218+
resp = test_client.get(
219+
"/api/services/resources",
220+
headers=test_client.auth_headers,
221+
)
222+
223+
assert resp.status_code == 200
224+
assert resp.json()["caveats"]["docker_desktop_memory"] is True
225+
226+
def test_orphaned_disk_data_included(self, test_client, monkeypatch):
227+
"""Disk data for services not in SERVICES dict appears as orphaned entries."""
228+
self._clear_resource_cache()
229+
230+
monkeypatch.setattr("routers.resources.SERVICES", {})
231+
monkeypatch.setattr("routers.resources.GPU_BACKEND", "nvidia")
232+
233+
fake_disk = {"orphaned-svc": {"data_gb": 2.0, "path": "data/orphaned-svc"}}
234+
235+
with patch("routers.resources._fetch_container_stats", return_value=[]), \
236+
patch("routers.resources._scan_service_disk", return_value=fake_disk):
237+
resp = test_client.get(
238+
"/api/services/resources",
239+
headers=test_client.auth_headers,
240+
)
241+
242+
assert resp.status_code == 200
243+
data = resp.json()
244+
orphaned = next(s for s in data["services"] if s["id"] == "orphaned-svc")
245+
assert orphaned["name"] == "orphaned-svc"
246+
assert orphaned["container"] is None
247+
assert orphaned["disk"]["data_gb"] == 2.0

0 commit comments

Comments
 (0)