140 lines
5.1 KiB
Python
140 lines
5.1 KiB
Python
"""Tests for TelemetryCollector."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from collections import deque
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
|
||
class TestTelemetryCollector:
|
||
"""Test telemetry calculations in isolation (no Qt event loop required)."""
|
||
|
||
def _make_collector(self, cpu_count: int = 8):
|
||
"""Construct a TelemetryCollector bypassing Qt machinery."""
|
||
from app.telemetry.telemetry_collector import TelemetryCollector
|
||
|
||
with patch.object(TelemetryCollector, "__init__", return_value=None):
|
||
col = TelemetryCollector.__new__(TelemetryCollector)
|
||
|
||
col._frame_times = deque(maxlen=120)
|
||
col._last_frame_time = 0.0
|
||
col._total_frames = 0
|
||
col._dropped_frames = 0
|
||
col._fps_window = deque()
|
||
col._fps_window_size_s = 1.0
|
||
col._target_fps = None
|
||
col._cpu_count = cpu_count
|
||
|
||
col._process = MagicMock()
|
||
# Simulate Windows: wset takes priority over rss
|
||
mem_info = MagicMock()
|
||
mem_info.wset = 50 * 1024 * 1024 # 50 MB private working set
|
||
mem_info.rss = 70 * 1024 * 1024 # RSS (larger, includes shared)
|
||
col._process.memory_info.return_value = mem_info
|
||
col._process.cpu_percent.return_value = 0.0
|
||
return col
|
||
|
||
def test_initial_snapshot_has_zero_fps(self):
|
||
col = self._make_collector()
|
||
snap = col._compute_snapshot()
|
||
assert snap.fps == 0.0
|
||
|
||
def test_fps_counts_frames_in_window(self):
|
||
col = self._make_collector()
|
||
now = time.perf_counter()
|
||
for i in range(30):
|
||
col._fps_window.append(now - 0.9 + i * 0.03)
|
||
snap = col._compute_snapshot()
|
||
assert snap.fps == 30.0
|
||
|
||
def test_fps_excludes_old_frames(self):
|
||
col = self._make_collector()
|
||
now = time.perf_counter()
|
||
# 10 frames older than 1 second — should not count
|
||
for i in range(10):
|
||
col._fps_window.append(now - 2.0 + i * 0.05)
|
||
# 5 frames within the last second
|
||
for i in range(5):
|
||
col._fps_window.append(now - 0.4 + i * 0.05)
|
||
snap = col._compute_snapshot()
|
||
assert snap.fps == 5.0
|
||
|
||
def test_frame_time_average(self):
|
||
col = self._make_collector()
|
||
interval = 0.0333
|
||
for _ in range(10):
|
||
col._frame_times.append(interval)
|
||
snap = col._compute_snapshot()
|
||
assert abs(snap.frame_time_ms - interval * 1000) < 0.1
|
||
|
||
def test_drop_detection(self):
|
||
col = self._make_collector()
|
||
normal_interval = 0.016
|
||
now = time.perf_counter()
|
||
col._last_frame_time = now - normal_interval * 10
|
||
for _ in range(9):
|
||
col._last_frame_time += normal_interval
|
||
col._frame_times.append(normal_interval)
|
||
|
||
col._last_frame_time = now
|
||
with patch("app.telemetry.telemetry_collector.time") as mock_time:
|
||
col._last_frame_time = now
|
||
big_delta = normal_interval * 5 # 5× average → drop
|
||
mock_time.perf_counter.return_value = now + big_delta
|
||
delta = big_delta
|
||
col._frame_times.append(delta)
|
||
avg = sum(col._frame_times) / len(col._frame_times)
|
||
if delta > avg * 2.5:
|
||
col._dropped_frames += 1
|
||
|
||
assert col._dropped_frames == 1
|
||
|
||
def test_reset_counters(self):
|
||
col = self._make_collector()
|
||
col._total_frames = 100
|
||
col._dropped_frames = 5
|
||
col._frame_times.append(0.016)
|
||
col._fps_window.append(time.perf_counter())
|
||
col.reset_counters()
|
||
assert col._total_frames == 0
|
||
assert col._dropped_frames == 0
|
||
assert len(col._frame_times) == 0
|
||
assert len(col._fps_window) == 0
|
||
|
||
def test_snapshot_memory_mb(self):
|
||
col = self._make_collector()
|
||
snap = col._compute_snapshot()
|
||
assert snap.memory_mb == 50.0
|
||
|
||
def test_cpu_sys_is_core_divided_by_cpu_count(self):
|
||
col = self._make_collector(cpu_count=8)
|
||
col._process.cpu_percent.return_value = 80.0 # 80% of one core
|
||
snap = col._compute_snapshot()
|
||
assert snap.cpu_percent_core == 80.0
|
||
assert snap.cpu_percent_sys == round(80.0 / 8, 1)
|
||
|
||
def test_cpu_sys_never_exceeds_100_on_single_core_machine(self):
|
||
col = self._make_collector(cpu_count=1)
|
||
col._process.cpu_percent.return_value = 95.0
|
||
snap = col._compute_snapshot()
|
||
assert snap.cpu_percent_sys == snap.cpu_percent_core == 95.0
|
||
|
||
def test_cpu_sys_le_cpu_core(self):
|
||
"""cpu_percent_sys must always be <= cpu_percent_core."""
|
||
col = self._make_collector(cpu_count=4)
|
||
col._process.cpu_percent.return_value = 150.0
|
||
snap = col._compute_snapshot()
|
||
assert snap.cpu_percent_sys <= snap.cpu_percent_core
|
||
|
||
def test_target_fps_none_by_default(self):
|
||
col = self._make_collector()
|
||
snap = col._compute_snapshot()
|
||
assert snap.target_fps is None
|
||
|
||
def test_set_target_fps_reflected_in_snapshot(self):
|
||
col = self._make_collector()
|
||
col.set_target_fps(60.0)
|
||
snap = col._compute_snapshot()
|
||
assert snap.target_fps == 60.0
|