|
12 | 12 | get_gpu_info_nvidia_detailed, |
13 | 13 | read_gpu_topology, |
14 | 14 | ) |
| 15 | +from models import GPUInfo |
15 | 16 |
|
16 | 17 |
|
17 | 18 | # ============================================================================ |
@@ -337,3 +338,80 @@ def test_history_maxlen_rolls_over(self): |
337 | 338 | gpu_mod._GPU_HISTORY.clear() |
338 | 339 | for item in saved: |
339 | 340 | gpu_mod._GPU_HISTORY.append(item) |
| 341 | + |
| 342 | + |
| 343 | +# ============================================================================ |
| 344 | +# _get_raw_gpus — Apple Silicon dispatch (routers/gpu.py) |
| 345 | +# ============================================================================ |
| 346 | + |
| 347 | + |
| 348 | +def _sample_apple_gpu_info() -> GPUInfo: |
| 349 | + return GPUInfo( |
| 350 | + name="Apple M3 Max", |
| 351 | + memory_used_mb=24000, |
| 352 | + memory_total_mb=65536, |
| 353 | + memory_percent=36.6, |
| 354 | + utilization_percent=0, |
| 355 | + temperature_c=0, |
| 356 | + power_w=None, |
| 357 | + memory_type="unified", |
| 358 | + gpu_backend="apple", |
| 359 | + ) |
| 360 | + |
| 361 | + |
| 362 | +class TestGetRawGpusApple: |
| 363 | + def test_apple_returns_single_entry(self, monkeypatch): |
| 364 | + """Apple backend wraps the single GPUInfo into a one-element IndividualGPU list.""" |
| 365 | + import routers.gpu as gpu_mod |
| 366 | + monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: _sample_apple_gpu_info()) |
| 367 | + |
| 368 | + result = gpu_mod._get_raw_gpus("apple") |
| 369 | + assert result is not None |
| 370 | + assert len(result) == 1 |
| 371 | + g = result[0] |
| 372 | + assert g.index == 0 |
| 373 | + assert len(g.uuid) >= 8 # GPUCard.jsx calls uuid.slice(-8) |
| 374 | + assert g.name == "Apple M3 Max" |
| 375 | + assert g.memory_used_mb == 24000 |
| 376 | + assert g.memory_total_mb == 65536 |
| 377 | + assert g.memory_percent == 36.6 |
| 378 | + assert g.utilization_percent == 0 |
| 379 | + assert g.temperature_c == 0 |
| 380 | + assert g.power_w is None |
| 381 | + assert g.assigned_services == [] |
| 382 | + |
| 383 | + def test_apple_returns_none_when_detection_fails(self, monkeypatch): |
| 384 | + """Detection returning None propagates as None — endpoint will raise 503.""" |
| 385 | + import routers.gpu as gpu_mod |
| 386 | + monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: None) |
| 387 | + |
| 388 | + assert gpu_mod._get_raw_gpus("apple") is None |
| 389 | + |
| 390 | + |
| 391 | +class TestGpuDetailedEndpointApple: |
| 392 | + def test_endpoint_returns_apple_aggregate(self, monkeypatch, test_client): |
| 393 | + """/api/gpu/detailed with GPU_BACKEND=apple returns 200 with single-GPU aggregate.""" |
| 394 | + import routers.gpu as gpu_mod |
| 395 | + # Bypass the 3 s TTL cache so this test sees fresh data. |
| 396 | + gpu_mod._detailed_cache["expires"] = 0.0 |
| 397 | + gpu_mod._detailed_cache["value"] = None |
| 398 | + |
| 399 | + monkeypatch.setenv("GPU_BACKEND", "apple") |
| 400 | + monkeypatch.setattr(gpu_mod, "get_gpu_info_apple", lambda: _sample_apple_gpu_info()) |
| 401 | + monkeypatch.setattr(gpu_mod, "decode_gpu_assignment", lambda: None) |
| 402 | + |
| 403 | + try: |
| 404 | + response = test_client.get("/api/gpu/detailed", headers=test_client.auth_headers) |
| 405 | + assert response.status_code == 200 |
| 406 | + body = response.json() |
| 407 | + assert body["backend"] == "apple" |
| 408 | + assert body["gpu_count"] == 1 |
| 409 | + assert len(body["gpus"]) == 1 |
| 410 | + assert body["gpus"][0]["name"] == "Apple M3 Max" |
| 411 | + assert body["gpus"][0]["index"] == 0 |
| 412 | + assert len(body["gpus"][0]["uuid"]) >= 8 |
| 413 | + assert body["aggregate"]["name"] == "Apple M3 Max" |
| 414 | + assert body["aggregate"]["gpu_backend"] == "apple" |
| 415 | + finally: |
| 416 | + gpu_mod._detailed_cache["expires"] = 0.0 |
| 417 | + gpu_mod._detailed_cache["value"] = None |
0 commit comments