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

435 lines
14 KiB
Python

"""
Tests for uncovered code paths in log_utils.py.
Targets:
- _get_research_id: record with extra, record without extra, no app context
- _process_log_queue: no app context (requeue), queue full, empty queue
- database_sink: background thread queueing, main thread direct write
- frontend_progress_sink: no research_id skips
- flush_log_queue: flushes entries, handles exceptions
- config_logger: MILESTONE level already exists, file logging, debug mode
- InterceptHandler: level not found fallback
"""
import queue
from unittest.mock import Mock, patch
MODULE = "local_deep_research.utilities.log_utils"
class TestGetResearchId:
"""Tests for _get_research_id."""
def test_from_record_extra(self):
"""Extracts research_id from record extra."""
from local_deep_research.utilities.log_utils import _get_research_id
record = {"extra": {"research_id": "test-123"}}
with patch(f"{MODULE}.has_app_context", return_value=False):
result = _get_research_id(record)
assert result == "test-123"
def test_record_without_extra_uses_flask_g(self, app):
"""Falls back to Flask g when record has no extra."""
from local_deep_research.utilities.log_utils import _get_research_id
from flask import g
record = {"level": "INFO"}
with app.test_request_context():
g.research_id = "flask-456"
result = _get_research_id(record)
assert result == "flask-456"
def test_none_record_uses_flask_g(self, app):
"""None record falls back to Flask g."""
from local_deep_research.utilities.log_utils import _get_research_id
from flask import g
with app.test_request_context():
g.research_id = "g-789"
result = _get_research_id(None)
assert result == "g-789"
def test_no_app_context_no_record(self):
"""Returns None when no app context and no record."""
from local_deep_research.utilities.log_utils import _get_research_id
with patch(f"{MODULE}.has_app_context", return_value=False):
result = _get_research_id(None)
assert result is None
def test_record_with_extra_but_no_research_id(self):
"""Record with extra but no research_id falls back to Flask g."""
from local_deep_research.utilities.log_utils import _get_research_id
record = {"extra": {"username": "bob"}}
with patch(f"{MODULE}.has_app_context", return_value=False):
result = _get_research_id(record)
assert result is None
class TestProcessLogQueue:
"""Tests for _process_log_queue."""
def test_processes_entry_with_app_context(self):
"""Processes log entry when app context is available."""
from local_deep_research.utilities.log_utils import (
_process_log_queue,
_log_queue,
_stop_queue,
)
entry = {"message": "test", "research_id": "123"}
_log_queue.put(entry)
# Set stop flag after one iteration
def stop_after_one(*args, **kwargs):
_stop_queue.set()
with patch(f"{MODULE}.has_app_context", return_value=True):
with patch(
f"{MODULE}._write_log_to_database", side_effect=stop_after_one
) as mock_write:
_stop_queue.clear()
_process_log_queue()
mock_write.assert_called_once_with(entry)
_stop_queue.clear()
def test_requeues_without_app_context(self):
"""Puts entry back when no app context."""
from local_deep_research.utilities.log_utils import (
_process_log_queue,
_log_queue,
_stop_queue,
)
# Clear queue
while not _log_queue.empty():
try:
_log_queue.get_nowait()
except queue.Empty:
break
entry = {"message": "test", "research_id": "123"}
_log_queue.put(entry)
call_count = [0]
def count_and_stop(*args, **kwargs):
call_count[0] += 1
if call_count[0] >= 2:
_stop_queue.set()
return False
with patch(f"{MODULE}.has_app_context", side_effect=count_and_stop):
_stop_queue.clear()
_process_log_queue()
_stop_queue.clear()
# Entry should still be in queue (requeued)
assert not _log_queue.empty()
# Clean up
while not _log_queue.empty():
try:
_log_queue.get_nowait()
except queue.Empty:
break
def test_skips_none_entry(self):
"""Skips None entries."""
from local_deep_research.utilities.log_utils import (
_process_log_queue,
_log_queue,
_stop_queue,
)
_log_queue.put(None)
_stop_queue.clear()
call_count = [0]
def mock_get(timeout=None):
call_count[0] += 1
if call_count[0] == 1:
return
_stop_queue.set()
raise queue.Empty()
with patch.object(_log_queue, "get", side_effect=mock_get):
_process_log_queue()
_stop_queue.clear()
class TestDatabaseSink:
"""Tests for database_sink."""
def test_background_thread_queues(self):
"""Queues log entry from background thread.
Logs need research context to be queued — ResearchLog is
research-scoped, system logs are skipped at the boundary."""
from local_deep_research.utilities.log_utils import (
database_sink,
_log_queue,
)
# Clear queue
while not _log_queue.empty():
try:
_log_queue.get_nowait()
except queue.Empty:
break
msg = Mock()
msg.record = {
"time": "2024-01-01",
"message": "test",
"name": "module",
"function": "func",
"line": 42,
"level": Mock(name="INFO"),
"extra": {"research_id": "rid-1"},
}
with patch(f"{MODULE}.has_app_context", return_value=False):
database_sink(msg)
# Entry should be in queue
assert not _log_queue.empty()
entry = _log_queue.get_nowait()
assert entry["message"] == "test"
assert entry["line_no"] == 42
def test_main_thread_writes_directly(self, app):
"""Writes directly from main thread with app context."""
from local_deep_research.utilities.log_utils import database_sink
msg = Mock()
msg.record = {
"time": "2024-01-01",
"message": "test",
"name": "module",
"function": "func",
"line": 42,
"level": Mock(name="INFO"),
"extra": {"username": "bob"},
}
with app.test_request_context():
with patch(f"{MODULE}.threading.current_thread") as mock_thread:
mock_thread.return_value.name = "MainThread"
with patch(f"{MODULE}._write_log_to_database") as mock_write:
database_sink(msg)
mock_write.assert_called_once()
def test_queue_full_drops_silently(self):
"""Drops log when queue is full."""
from local_deep_research.utilities.log_utils import database_sink
msg = Mock()
msg.record = {
"time": "2024-01-01",
"message": "test",
"name": "module",
"function": "func",
"line": 1,
"level": Mock(name="DEBUG"),
"extra": {},
}
with patch(f"{MODULE}.has_app_context", return_value=False):
with patch(f"{MODULE}._log_queue") as mock_queue:
mock_queue.put_nowait.side_effect = queue.Full()
# Should not raise
database_sink(msg)
class TestFrontendProgressSink:
"""Tests for frontend_progress_sink."""
def test_no_research_id_returns_early(self):
"""Returns early when no research_id."""
from local_deep_research.utilities.log_utils import (
frontend_progress_sink,
)
msg = Mock()
msg.record = {
"message": "test",
"level": Mock(name="INFO"),
"time": Mock(isoformat=Mock(return_value="2024-01-01T00:00:00")),
"extra": {},
}
with patch(f"{MODULE}._get_research_id", return_value=None):
with patch(f"{MODULE}.SocketIOService") as mock_sio:
frontend_progress_sink(msg)
mock_sio.assert_not_called()
def test_with_research_id_emits(self):
"""Emits to subscribers when research_id exists."""
from local_deep_research.utilities.log_utils import (
frontend_progress_sink,
)
msg = Mock()
msg.record = {
"message": "Progress update",
"level": Mock(name="INFO"),
"time": Mock(isoformat=Mock(return_value="2024-01-01T00:00:00")),
"extra": {"research_id": "r-123"},
}
with patch(f"{MODULE}._get_research_id", return_value="r-123"):
with patch(f"{MODULE}.SocketIOService") as mock_sio_cls:
mock_sio = Mock()
mock_sio_cls.return_value = mock_sio
frontend_progress_sink(msg)
mock_sio.emit_to_subscribers.assert_called_once()
call_args = mock_sio.emit_to_subscribers.call_args
assert call_args[0][0] == "progress"
assert call_args[0][1] == "r-123"
class TestFlushLogQueue:
"""Tests for flush_log_queue."""
def test_flushes_entries(self):
"""Flushes all entries from queue to database."""
from local_deep_research.utilities.log_utils import (
flush_log_queue,
_log_queue,
)
# Clear queue
while not _log_queue.empty():
try:
_log_queue.get_nowait()
except queue.Empty:
break
_log_queue.put({"message": "entry1"})
_log_queue.put({"message": "entry2"})
with patch(f"{MODULE}._write_log_to_database") as mock_write:
flush_log_queue()
assert mock_write.call_count == 2
def test_empty_queue(self):
"""Handles empty queue gracefully."""
from local_deep_research.utilities.log_utils import (
flush_log_queue,
_log_queue,
)
# Clear queue
while not _log_queue.empty():
try:
_log_queue.get_nowait()
except queue.Empty:
break
with patch(f"{MODULE}._write_log_to_database") as mock_write:
flush_log_queue()
mock_write.assert_not_called()
class TestInterceptHandler:
"""Tests for InterceptHandler."""
def _make_log_record(self, levelname="WARNING", levelno=30, msg="test"):
"""Create a mock log record without importing logging directly."""
record = Mock()
record.levelname = levelname
record.levelno = levelno
record.getMessage.return_value = msg
record.exc_info = None
return record
def test_emit_with_known_level(self):
"""Emits log with matching loguru level."""
from local_deep_research.utilities.log_utils import InterceptHandler
handler = InterceptHandler()
record = self._make_log_record("WARNING", 30, "test warning")
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level.return_value = Mock(name="WARNING")
mock_logger.opt.return_value = mock_logger
handler.emit(record)
mock_logger.opt.assert_called()
def test_emit_with_unknown_level(self):
"""Falls back to numeric level for unknown level names."""
from local_deep_research.utilities.log_utils import InterceptHandler
handler = InterceptHandler()
record = self._make_log_record("CUSTOM", 42, "custom level msg")
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level.side_effect = ValueError("Unknown level")
mock_logger.opt.return_value = mock_logger
handler.emit(record)
mock_logger.opt.assert_called()
class TestConfigLogger:
"""Tests for config_logger."""
def test_milestone_level_already_exists(self):
"""Handles MILESTONE level already existing."""
from local_deep_research.utilities.log_utils import config_logger
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level.side_effect = ValueError("Level already exists")
mock_logger.configure = Mock()
mock_logger.enable = Mock()
mock_logger.remove = Mock()
mock_logger.add = Mock()
mock_logger.warning = Mock()
# Should not raise
config_logger("test", debug=False)
def test_file_logging_enabled(self):
"""File logging adds file handler when env var set."""
from local_deep_research.utilities.log_utils import config_logger
with patch.dict("os.environ", {"LDR_ENABLE_FILE_LOGGING": "true"}):
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level = Mock(return_value=Mock())
mock_logger.configure = Mock()
mock_logger.enable = Mock()
mock_logger.remove = Mock()
mock_logger.add = Mock()
mock_logger.warning = Mock()
config_logger("test", debug=False)
# Should have 4 adds: stderr, database_sink, frontend_progress, file
assert mock_logger.add.call_count == 4
def test_debug_mode_logs_warning(self):
"""Debug mode logs security warning."""
from local_deep_research.utilities.log_utils import config_logger
with patch(f"{MODULE}.logger") as mock_logger:
mock_logger.level = Mock(return_value=Mock())
mock_logger.configure = Mock()
mock_logger.enable = Mock()
mock_logger.remove = Mock()
mock_logger.add = Mock()
mock_logger.warning = Mock()
config_logger("test", debug=True)
# Should warn about debug logging
mock_logger.warning.assert_called()
warning_msg = mock_logger.warning.call_args_list[0][0][0]
assert "DEBUG logging is enabled" in warning_msg