wrolpi/controller/test/test_status_worker.py

143 lines
5.6 KiB
Python
Raw Normal View History

"""
Tests for the status worker background task and aggregated stats endpoint.
"""
from datetime import datetime
from unittest import mock
import pytest
class TestCollectAllStatus:
"""Tests for collect_all_status function."""
@pytest.mark.asyncio
async def test_collects_all_stats_and_iso_timestamp(self):
"""Should collect every status type and include a parseable ISO timestamp."""
from controller.lib.status_worker import collect_all_status
status = await collect_all_status()
for key in (
"cpu_stats", "memory_stats", "load_stats", "drives_stats",
"nic_bandwidth_stats", "power_stats", "last_status",
):
assert key in status
datetime.fromisoformat(status["last_status"])
@pytest.mark.asyncio
@pytest.mark.parametrize("collector,result_key,failure_value", [
("get_cpu_status", "cpu_stats", None),
("get_memory_status", "memory_stats", None),
("get_drive_status", "drives_stats", []),
("get_network_status", "nic_bandwidth_stats", {}),
])
async def test_individual_collector_failure(self, collector, result_key, failure_value):
"""A single collector raising should leave the failed key at its fallback value
and let the other collectors still populate the result."""
from controller.lib.status_worker import collect_all_status
with mock.patch(
f"controller.lib.status_worker.{collector}",
side_effect=Exception(f"{collector} error"),
):
status = await collect_all_status()
assert status[result_key] == failure_value
# At least one other collector should still have produced real data.
other_keys = [k for k in ("cpu_stats", "memory_stats") if k != result_key]
assert any(status[k] is not None for k in other_keys)
class TestGetAdaptiveSleep:
"""Tests for adaptive sleep calculation."""
def test_returns_base_sleep_when_load_is_low(self):
"""Should return base sleep when load is below CPU count."""
from controller.lib.status_worker import get_adaptive_sleep
with mock.patch("multiprocessing.cpu_count", return_value=4):
load_stats = {"minute_1": "0.5"}
sleep = get_adaptive_sleep(load_stats, base_sleep=5.0)
assert sleep == 5.0
def test_returns_base_sleep_when_load_equals_cpu_count(self):
"""Should return base sleep when load equals CPU count."""
from controller.lib.status_worker import get_adaptive_sleep
with mock.patch("multiprocessing.cpu_count", return_value=4):
load_stats = {"minute_1": "4.0"}
sleep = get_adaptive_sleep(load_stats, base_sleep=5.0)
assert sleep == 5.0
def test_increases_sleep_when_load_exceeds_cpu_count(self):
"""Should increase sleep when load exceeds CPU count."""
from controller.lib.status_worker import get_adaptive_sleep
with mock.patch("multiprocessing.cpu_count", return_value=4):
load_stats = {"minute_1": "8.0"} # 2x CPU count
sleep = get_adaptive_sleep(load_stats, base_sleep=5.0)
# Expected: (5.0 * 8.0) / 4 = 10.0
assert sleep == 10.0
def test_handles_none_load_stats(self):
"""Should return base sleep when load_stats is None."""
from controller.lib.status_worker import get_adaptive_sleep
sleep = get_adaptive_sleep(None, base_sleep=5.0)
assert sleep == 5.0
def test_handles_empty_load_stats(self):
"""Should return base sleep when load_stats is empty dict."""
from controller.lib.status_worker import get_adaptive_sleep
sleep = get_adaptive_sleep({}, base_sleep=5.0)
assert sleep == 5.0
def test_handles_invalid_load_value(self):
"""Should return base sleep when load value is invalid."""
from controller.lib.status_worker import get_adaptive_sleep
load_stats = {"minute_1": "invalid"}
sleep = get_adaptive_sleep(load_stats, base_sleep=5.0)
assert sleep == 5.0
def test_handles_missing_minute_1_key(self):
"""Should return base sleep when minute_1 key is missing."""
from controller.lib.status_worker import get_adaptive_sleep
load_stats = {"minute_5": "2.0"}
sleep = get_adaptive_sleep(load_stats, base_sleep=5.0)
assert sleep == 5.0
class TestAggregatedStatsEndpoint:
"""Tests for the /api/stats aggregated endpoint."""
def test_aggregated_stats_payload(self, test_client):
"""Endpoint should return 200 with every status key and the expected sub-shapes."""
response = test_client.get("/api/stats")
assert response.status_code == 200
data = response.json()
for key in (
"cpu_stats", "memory_stats", "load_stats", "drives_stats",
"nic_bandwidth_stats", "power_stats", "last_status",
):
assert key in data
# last_status is an ISO timestamp.
datetime.fromisoformat(data["last_status"])
# Sub-field shape spot-checks.
for field in ("percent", "cores"):
assert field in data["cpu_stats"]
for field in ("total", "used", "free"):
assert field in data["memory_stats"]
for field in ("minute_1", "minute_5", "minute_15"):
assert field in data["load_stats"]
assert isinstance(data["drives_stats"], list)
assert isinstance(data["nic_bandwidth_stats"], dict)
def test_aggregated_stats_trailing_slash(self, test_client):
"""Aggregated stats should work with trailing slash."""
response = test_client.get("/api/stats/")
assert response.status_code == 200