Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

458 lines
16 KiB
Python

"""
Coverage tests for benchmarks/efficiency/resource_monitor.py.
Covers paths not exercised by the existing high-value test file:
- _monitor_resources thread: collects process/system data, skips disabled tracking,
handles exceptions during collection
- Start/stop cycle: creates thread, joins thread, sets flag, resets data
- Context manager: delegates to start/stop and collects data
- print_summary: output with both sections, system-only section
- get_combined_stats: zero system memory edge case, no system stats present
"""
from unittest.mock import MagicMock, patch
from local_deep_research.benchmarks.efficiency.resource_monitor import (
ResourceMonitor,
)
# ---------------------------------------------------------------------------
# _monitor_resources thread
# ---------------------------------------------------------------------------
class TestMonitorResourcesThread:
"""Tests for the _monitor_resources background collection loop."""
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.psutil")
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_collects_process_and_system_data(self, mock_time, mock_psutil):
"""A single iteration collects both process and system samples."""
mock_time.time.return_value = 1000.0
# Stop after one sleep
def sleep_side_effect(_interval):
monitor.monitoring = False
mock_time.sleep.side_effect = sleep_side_effect
# Mock process
mock_process = MagicMock()
mock_process.cpu_percent.return_value = 12.5
mem_info = MagicMock(
rss=50 * 1024 * 1024, vms=100 * 1024 * 1024, shared=10
)
mock_process.memory_info.return_value = mem_info
mock_process.num_threads.return_value = 3
mock_process.open_files.return_value = [MagicMock()]
mock_process.status.return_value = "running"
mock_psutil.Process.return_value = mock_process
# Mock system calls
mock_psutil.cpu_percent.return_value = 40.0
mock_psutil.virtual_memory.return_value = MagicMock(
total=8 * 1024**3,
available=4 * 1024**3,
used=4 * 1024**3,
percent=50.0,
)
mock_psutil.disk_usage.return_value = MagicMock(
total=256 * 1024**3,
used=128 * 1024**3,
percent=50.0,
)
monitor = ResourceMonitor(sampling_interval=0.01)
monitor.monitoring = True
monitor._monitor_resources()
assert len(monitor.process_data) == 1
assert monitor.process_data[0]["cpu_percent"] == 12.5
assert monitor.process_data[0]["memory_rss"] == 50 * 1024 * 1024
assert monitor.process_data[0]["num_threads"] == 3
assert monitor.process_data[0]["open_files"] == 1
assert monitor.process_data[0]["status"] == "running"
assert len(monitor.system_data) == 1
assert monitor.system_data[0]["cpu_percent"] == 40.0
assert monitor.system_data[0]["memory_percent"] == 50.0
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.psutil")
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_skips_process_tracking_when_disabled(self, mock_time, mock_psutil):
"""When track_process=False no process data is collected."""
mock_time.time.return_value = 1000.0
def sleep_side_effect(_interval):
monitor.monitoring = False
mock_time.sleep.side_effect = sleep_side_effect
mock_psutil.Process.return_value = MagicMock()
mock_psutil.cpu_percent.return_value = 10.0
mock_psutil.virtual_memory.return_value = MagicMock(
total=8 * 1024**3,
available=4 * 1024**3,
used=4 * 1024**3,
percent=50.0,
)
mock_psutil.disk_usage.return_value = MagicMock(
total=256 * 1024**3,
used=128 * 1024**3,
percent=50.0,
)
monitor = ResourceMonitor(track_process=False, track_system=True)
monitor.monitoring = True
monitor._monitor_resources()
assert len(monitor.process_data) == 0
assert len(monitor.system_data) == 1
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.psutil")
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_skips_system_tracking_when_disabled(self, mock_time, mock_psutil):
"""When track_system=False no system data is collected."""
mock_time.time.return_value = 1000.0
def sleep_side_effect(_interval):
monitor.monitoring = False
mock_time.sleep.side_effect = sleep_side_effect
mock_process = MagicMock()
mock_process.cpu_percent.return_value = 5.0
mem_info = MagicMock(rss=30 * 1024 * 1024, vms=60 * 1024 * 1024)
mock_process.memory_info.return_value = mem_info
mock_process.num_threads.return_value = 2
mock_process.open_files.return_value = []
mock_process.status.return_value = "sleeping"
mock_psutil.Process.return_value = mock_process
monitor = ResourceMonitor(track_process=True, track_system=False)
monitor.monitoring = True
monitor._monitor_resources()
assert len(monitor.process_data) == 1
assert len(monitor.system_data) == 0
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.psutil")
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_handles_exception_during_collection(self, mock_time, mock_psutil):
"""An exception inside the loop is caught; monitoring continues."""
mock_time.time.return_value = 1000.0
iteration = {"count": 0}
def sleep_side_effect(_interval):
iteration["count"] += 1
if iteration["count"] >= 2:
monitor.monitoring = False
mock_time.sleep.side_effect = sleep_side_effect
mock_process = MagicMock()
# First call raises, second call succeeds
mock_process.cpu_percent.side_effect = [RuntimeError("boom"), 10.0]
mem_info = MagicMock(rss=50 * 1024 * 1024, vms=100 * 1024 * 1024)
mock_process.memory_info.return_value = mem_info
mock_process.num_threads.return_value = 2
mock_process.open_files.return_value = []
mock_process.status.return_value = "running"
mock_psutil.Process.return_value = mock_process
monitor = ResourceMonitor()
monitor.monitoring = True
monitor._monitor_resources()
# First iteration raised, second succeeded => 1 sample
assert len(monitor.process_data) == 1
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
False,
)
def test_returns_immediately_without_psutil(self):
"""_monitor_resources returns immediately when psutil unavailable."""
monitor = ResourceMonitor()
monitor.monitoring = True
monitor._monitor_resources()
assert len(monitor.process_data) == 0
assert len(monitor.system_data) == 0
# ---------------------------------------------------------------------------
# Start / stop lifecycle
# ---------------------------------------------------------------------------
class TestStartStopCycle:
"""Tests for the start/stop lifecycle with thread management."""
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.threading"
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_start_creates_and_starts_thread(self, mock_time, mock_threading):
"""start() creates a daemon thread and calls .start() on it."""
mock_time.time.return_value = 5000.0
mock_thread = MagicMock()
mock_threading.Thread.return_value = mock_thread
monitor = ResourceMonitor()
monitor.can_monitor = True
monitor.start()
mock_threading.Thread.assert_called_once()
mock_thread.start.assert_called_once()
assert monitor.monitoring is True
assert monitor.start_time == 5000.0
assert monitor.process_data == []
assert monitor.system_data == []
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.threading"
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_stop_joins_thread_and_resets(self, mock_time, mock_threading):
"""stop() joins the thread, clears it, and records end_time."""
mock_time.time.return_value = 6000.0
mock_thread = MagicMock()
mock_threading.Thread.return_value = mock_thread
monitor = ResourceMonitor()
monitor.can_monitor = True
monitor.start()
mock_time.time.return_value = 7000.0
monitor.stop()
mock_thread.join.assert_called_once_with(timeout=2.0)
assert monitor.monitoring is False
assert monitor.end_time == 7000.0
assert monitor.monitor_thread is None
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.threading"
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_start_when_already_monitoring_is_noop(
self, mock_time, mock_threading
):
"""Calling start() a second time while monitoring logs warning only."""
mock_time.time.return_value = 5000.0
mock_thread = MagicMock()
mock_threading.Thread.return_value = mock_thread
monitor = ResourceMonitor()
monitor.can_monitor = True
monitor.start()
monitor.start() # second call should not create another thread
assert mock_threading.Thread.call_count == 1
# ---------------------------------------------------------------------------
# Context manager
# ---------------------------------------------------------------------------
class TestContextManager:
"""Tests for the monitor() context manager."""
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.PSUTIL_AVAILABLE",
True,
)
@patch(
"local_deep_research.benchmarks.efficiency.resource_monitor.threading"
)
@patch("local_deep_research.benchmarks.efficiency.resource_monitor.time")
def test_context_manager_calls_start_and_stop(
self, mock_time, mock_threading
):
"""The context manager starts monitoring on enter and stops on exit."""
mock_time.time.return_value = 1000.0
mock_thread = MagicMock()
mock_threading.Thread.return_value = mock_thread
monitor = ResourceMonitor()
monitor.can_monitor = True
with monitor.monitor():
assert monitor.monitoring is True
assert monitor.monitoring is False
mock_thread.join.assert_called_once()
# ---------------------------------------------------------------------------
# print_summary
# ---------------------------------------------------------------------------
class TestPrintSummary:
"""Tests for print_summary output."""
def test_print_summary_with_both_sections(self, capsys):
"""print_summary outputs both process and system sections."""
monitor = ResourceMonitor()
monitor.start_time = 100.0
monitor.end_time = 110.0
monitor.process_data = [
{
"timestamp": 101.0,
"cpu_percent": 15.0,
"memory_rss": 64 * 1024 * 1024,
"memory_vms": 128 * 1024 * 1024,
"memory_shared": 0,
"num_threads": 5,
"open_files": 1,
"status": "running",
},
]
monitor.system_data = [
{
"timestamp": 101.0,
"cpu_percent": 25.0,
"memory_total": 16 * 1024**3,
"memory_available": 8 * 1024**3,
"memory_used": 8 * 1024**3,
"memory_percent": 50.0,
"disk_total": 500 * 1024**3,
"disk_used": 250 * 1024**3,
"disk_percent": 50.0,
},
]
monitor.print_summary()
captured = capsys.readouterr().out
assert "Process Resources" in captured
assert "System Resources" in captured
assert "15.0%" in captured
assert "64.0 MB" in captured
assert "Threads: 5" in captured
def test_print_summary_system_only(self, capsys):
"""print_summary outputs system section only when no process data."""
monitor = ResourceMonitor()
monitor.start_time = 100.0
monitor.end_time = 110.0
monitor.system_data = [
{
"timestamp": 101.0,
"cpu_percent": 30.0,
"memory_total": 8 * 1024**3,
"memory_available": 4 * 1024**3,
"memory_used": 4 * 1024**3,
"memory_percent": 50.0,
"disk_total": 256 * 1024**3,
"disk_used": 128 * 1024**3,
"disk_percent": 50.0,
},
]
monitor.print_summary()
captured = capsys.readouterr().out
assert "Process Resources" not in captured
assert "System Resources" in captured
# ---------------------------------------------------------------------------
# get_combined_stats edge cases
# ---------------------------------------------------------------------------
class TestGetCombinedStatsEdgeCases:
"""Edge-case tests for get_combined_stats."""
def test_zero_system_memory_yields_zero_percent(self):
"""When system_memory_mb is 0, process_memory_percent is 0."""
monitor = ResourceMonitor()
monitor.start_time = 100.0
monitor.end_time = 110.0
monitor.process_data = [
{
"timestamp": 101.0,
"cpu_percent": 10.0,
"memory_rss": 50 * 1024 * 1024,
"memory_vms": 100 * 1024 * 1024,
"memory_shared": 0,
"num_threads": 2,
"open_files": 0,
"status": "running",
},
]
monitor.system_data = [
{
"timestamp": 101.0,
"cpu_percent": 20.0,
"memory_total": 0, # zero total memory
"memory_available": 0,
"memory_used": 0,
"memory_percent": 0.0,
"disk_total": 100 * 1024**3,
"disk_used": 50 * 1024**3,
"disk_percent": 50.0,
},
]
stats = monitor.get_combined_stats()
assert stats["process_memory_percent"] == 0
def test_no_system_stats_omits_system_keys(self):
"""When there is no system data, combined stats have no system_ keys."""
monitor = ResourceMonitor()
monitor.start_time = 100.0
monitor.end_time = 110.0
monitor.process_data = [
{
"timestamp": 101.0,
"cpu_percent": 10.0,
"memory_rss": 50 * 1024 * 1024,
"memory_vms": 100 * 1024 * 1024,
"memory_shared": 0,
"num_threads": 2,
"open_files": 0,
"status": "running",
},
]
stats = monitor.get_combined_stats()
assert "process_cpu_avg" in stats
system_keys = [k for k in stats if k.startswith("system_")]
assert len(system_keys) == 0
assert "process_memory_percent" not in stats