Files
voculinux/tests/test_logging_manager.py
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

701 lines
22 KiB
Python

"""
Tests for the logging_manager module.
"""
import logging
import os
import tempfile
import threading
import time
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
class TestLogRecord:
"""Tests for LogRecord class."""
def test_init(self):
"""Test LogRecord initialization."""
from vocalinux.ui.logging_manager import LogRecord
timestamp = datetime.now()
record = LogRecord(
timestamp=timestamp,
level="INFO",
logger_name="test.module",
message="Test message",
module="test",
)
assert record.timestamp == timestamp
assert record.level == "INFO"
assert record.logger_name == "test.module"
assert record.message == "Test message"
assert record.module == "test"
def test_init_default_module(self):
"""Test LogRecord initialization with default module."""
from vocalinux.ui.logging_manager import LogRecord
timestamp = datetime.now()
record = LogRecord(
timestamp=timestamp, level="DEBUG", logger_name="mylogger", message="Test"
)
assert record.module == ""
def test_to_dict(self):
"""Test LogRecord to_dict method."""
from vocalinux.ui.logging_manager import LogRecord
timestamp = datetime(2024, 1, 15, 10, 30, 45, 123456)
record = LogRecord(
timestamp=timestamp,
level="WARNING",
logger_name="test.module",
message="Warning message",
module="test",
)
result = record.to_dict()
assert result["timestamp"] == "2024-01-15T10:30:45.123456"
assert result["level"] == "WARNING"
assert result["logger_name"] == "test.module"
assert result["message"] == "Warning message"
assert result["module"] == "test"
def test_str(self):
"""Test LogRecord string representation."""
from vocalinux.ui.logging_manager import LogRecord
timestamp = datetime(2024, 1, 15, 10, 30, 45, 123456)
record = LogRecord(
timestamp=timestamp,
level="ERROR",
logger_name="test.module",
message="Error occurred",
module="test",
)
result = str(record)
assert "10:30:45.123" in result
assert "ERROR" in result
assert "test.module" in result
assert "Error occurred" in result
class TestLoggingManager:
"""Tests for LoggingManager class."""
@pytest.fixture
def logging_manager(self):
"""Create a fresh LoggingManager for each test."""
# Reset the global instance
import vocalinux.ui.logging_manager as lm
lm._logging_manager = None
# Remove any existing handlers from root logger to avoid interference
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
manager = lm.LoggingManager(max_records=100)
yield manager
# Cleanup: remove handler after test
root_logger.removeHandler(manager.handler)
def test_init(self, logging_manager):
"""Test LoggingManager initialization."""
assert logging_manager.max_records == 100
assert logging_manager.log_callbacks == []
assert logging_manager.handler is not None
def test_logs_dir_honours_xdg_data_home(self, tmp_path, monkeypatch):
"""Logs must follow XDG_DATA_HOME so Flatpak sandbox paths work."""
import vocalinux.ui.logging_manager as lm
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
lm._logging_manager = None
root_logger = logging.getLogger()
for h in [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]:
root_logger.removeHandler(h)
manager = lm.LoggingManager(max_records=10)
try:
assert manager.logs_dir == tmp_path / "voculinux" / "logs"
assert manager.logs_dir.is_dir()
finally:
root_logger.removeHandler(manager.handler)
lm._logging_manager = None
def test_add_log_record(self, logging_manager):
"""Test adding a log record."""
from vocalinux.ui.logging_manager import LogRecord
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message="Test message",
)
logging_manager.add_log_record(record)
# Account for the init log message and any others
assert any(r.message == "Test message" for r in logging_manager.log_records)
def test_add_log_record_trims_old_records(self, logging_manager):
"""Test that old records are trimmed when max_records is exceeded."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.max_records = 5
for i in range(10):
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message=f"Message {i}",
)
logging_manager.add_log_record(record)
assert len(logging_manager.log_records) == 5
# Should have the last 5 messages
messages = [r.message for r in logging_manager.log_records]
assert "Message 9" in messages
assert "Message 5" in messages
def test_add_log_record_notifies_callbacks(self, logging_manager):
"""Test that callbacks are notified when records are added."""
from vocalinux.ui.logging_manager import LogRecord
callback_records = []
def callback(record):
callback_records.append(record)
logging_manager.register_callback(callback)
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message="Callback test",
)
logging_manager.add_log_record(record)
assert len(callback_records) >= 1
assert any(r.message == "Callback test" for r in callback_records)
def test_callback_error_does_not_break_logging(self, logging_manager):
"""Test that callback errors don't break logging."""
from vocalinux.ui.logging_manager import LogRecord
def bad_callback(record):
raise ValueError("Callback error")
logging_manager.register_callback(bad_callback)
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message="Should still work",
)
# Should not raise
logging_manager.add_log_record(record)
assert any(r.message == "Should still work" for r in logging_manager.log_records)
def test_register_callback(self, logging_manager):
"""Test callback registration."""
def callback(record):
pass
logging_manager.register_callback(callback)
assert callback in logging_manager.log_callbacks
def test_unregister_callback(self, logging_manager):
"""Test callback unregistration."""
def callback(record):
pass
logging_manager.register_callback(callback)
logging_manager.unregister_callback(callback)
assert callback not in logging_manager.log_callbacks
def test_unregister_nonexistent_callback(self, logging_manager):
"""Test unregistering a callback that was never registered."""
def callback(record):
pass
# Should not raise
logging_manager.unregister_callback(callback)
def test_get_logs_no_filter(self, logging_manager):
"""Test getting logs without filters."""
from vocalinux.ui.logging_manager import LogRecord
for i in range(5):
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message=f"Message {i}",
)
logging_manager.add_log_record(record)
logs = logging_manager.get_logs()
assert len(logs) >= 5
def test_get_logs_level_filter(self, logging_manager):
"""Test getting logs with level filter."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
for level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
record = LogRecord(
timestamp=datetime.now(),
level=level,
logger_name="test",
message=f"{level} message",
)
logging_manager.add_log_record(record)
error_logs = logging_manager.get_logs(level_filter="ERROR")
assert len(error_logs) == 1
assert error_logs[0].level == "ERROR"
def test_get_logs_module_filter(self, logging_manager):
"""Test getting logs with module filter."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
record1 = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="vocalinux.ui.test",
message="UI message",
)
record2 = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="voculinux.speech.test",
message="Speech message",
)
logging_manager.add_log_record(record1)
logging_manager.add_log_record(record2)
ui_logs = logging_manager.get_logs(module_filter="ui")
assert len(ui_logs) == 1
assert "ui" in ui_logs[0].logger_name.lower()
def test_get_logs_last_n(self, logging_manager):
"""Test getting last N logs."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
for i in range(10):
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message=f"Message {i}",
)
logging_manager.add_log_record(record)
logs = logging_manager.get_logs(last_n=3)
assert len(logs) == 3
assert logs[-1].message == "Message 9"
def test_export_logs(self, logging_manager):
"""Test exporting logs to a file."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message="Export test message",
)
logging_manager.add_log_record(record)
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
filepath = f.name
try:
result = logging_manager.export_logs(filepath)
assert result is True
assert os.path.exists(filepath)
with open(filepath, "r") as f:
content = f.read()
assert "Voculinux Log Export" in content
assert "Export test message" in content
finally:
if os.path.exists(filepath):
os.unlink(filepath)
def test_export_logs_with_filters(self, logging_manager):
"""Test exporting logs with filters."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
record = LogRecord(
timestamp=datetime.now(),
level="ERROR",
logger_name="test.module",
message="Error message",
)
logging_manager.add_log_record(record)
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
filepath = f.name
try:
result = logging_manager.export_logs(
filepath, level_filter="ERROR", module_filter="test"
)
assert result is True
with open(filepath, "r") as f:
content = f.read()
assert "Level Filter: ERROR" in content
assert "Module Filter: test" in content
finally:
if os.path.exists(filepath):
os.unlink(filepath)
def test_export_logs_failure(self, logging_manager):
"""Test export logs handles errors gracefully."""
# Try to write to an invalid path
result = logging_manager.export_logs("/nonexistent/directory/file.txt")
assert result is False
def test_clear_logs(self, logging_manager):
"""Test clearing logs."""
from vocalinux.ui.logging_manager import LogRecord
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="test",
message="Test message",
)
logging_manager.add_log_record(record)
# Verify we have at least one record
assert len(logging_manager.log_records) >= 1
logging_manager.clear_logs()
# clear_logs clears the list; the log message goes to root logger
# which may or may not be captured depending on handler setup
# Just verify clear() was called
assert len(logging_manager.log_records) <= 1
def test_get_log_stats_empty(self, logging_manager):
"""Test get_log_stats with no logs."""
logging_manager.log_records.clear()
stats = logging_manager.get_log_stats()
assert stats["total"] == 0
assert stats["by_level"] == {}
assert stats["by_module"] == {}
assert stats["oldest"] is None
assert stats["newest"] is None
def test_get_log_stats_with_records(self, logging_manager):
"""Test get_log_stats with records."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
timestamp1 = datetime(2024, 1, 15, 10, 0, 0)
timestamp2 = datetime(2024, 1, 15, 11, 0, 0)
record1 = LogRecord(
timestamp=timestamp1,
level="INFO",
logger_name="vocalinux.ui.test",
message="Message 1",
)
record2 = LogRecord(
timestamp=timestamp2,
level="ERROR",
logger_name="voculinux.speech.test",
message="Message 2",
)
logging_manager.add_log_record(record1)
logging_manager.add_log_record(record2)
stats = logging_manager.get_log_stats()
assert stats["total"] == 2
assert stats["by_level"]["INFO"] == 1
assert stats["by_level"]["ERROR"] == 1
assert "voculinux" in stats["by_module"]
assert stats["oldest"] == timestamp1
assert stats["newest"] == timestamp2
def test_get_log_stats_module_without_dot(self, logging_manager):
"""Test get_log_stats with logger name without dots."""
from vocalinux.ui.logging_manager import LogRecord
logging_manager.log_records.clear()
record = LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name="simplelogger",
message="Message",
)
logging_manager.add_log_record(record)
stats = logging_manager.get_log_stats()
assert "simplelogger" in stats["by_module"]
class TestLoggingHandler:
"""Tests for LoggingHandler class."""
@pytest.fixture
def handler_setup(self):
"""Set up a LoggingHandler for testing."""
import vocalinux.ui.logging_manager as lm
lm._logging_manager = None
# Create a minimal manager
manager = MagicMock()
manager.add_log_record = MagicMock()
handler = lm.LoggingHandler(manager)
yield handler, manager
def test_emit(self, handler_setup):
"""Test emit method."""
handler, manager = handler_setup
log_record = logging.LogRecord(
name="test.logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None,
)
handler.emit(log_record)
manager.add_log_record.assert_called_once()
call_args = manager.add_log_record.call_args[0][0]
assert call_args.level == "INFO"
assert call_args.logger_name == "test.logger"
assert call_args.message == "Test message"
def test_emit_prevents_recursion(self, handler_setup):
"""Test that emit prevents recursive calls."""
handler, manager = handler_setup
# Simulate already emitting
handler._emitting = True
log_record = logging.LogRecord(
name="test.logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None,
)
handler.emit(log_record)
# Should not have called add_log_record due to recursion prevention
manager.add_log_record.assert_not_called()
def test_emit_handles_errors(self, handler_setup):
"""Test that emit handles errors gracefully."""
handler, manager = handler_setup
manager.add_log_record.side_effect = Exception("Test error")
log_record = logging.LogRecord(
name="test.logger",
level=logging.INFO,
pathname="test.py",
lineno=1,
msg="Test message",
args=(),
exc_info=None,
)
with patch.object(handler, "handleError") as mock_handle_error:
handler.emit(log_record)
mock_handle_error.assert_called_once()
# _emitting should be reset even after error
assert handler._emitting is False
class TestGlobalFunctions:
"""Tests for module-level functions."""
def test_get_logging_manager_creates_instance(self):
"""Test that get_logging_manager creates instance if needed."""
import vocalinux.ui.logging_manager as lm
# Reset global
lm._logging_manager = None
# Remove any existing handlers
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
manager = lm.get_logging_manager()
assert manager is not None
assert isinstance(manager, lm.LoggingManager)
# Cleanup
root_logger.removeHandler(manager.handler)
def test_get_logging_manager_returns_same_instance(self):
"""Test that get_logging_manager returns the same instance."""
import vocalinux.ui.logging_manager as lm
# Reset global
lm._logging_manager = None
# Remove any existing handlers
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
manager1 = lm.get_logging_manager()
manager2 = lm.get_logging_manager()
assert manager1 is manager2
# Cleanup
root_logger.removeHandler(manager1.handler)
def test_initialize_logging(self):
"""Test initialize_logging function."""
import vocalinux.ui.logging_manager as lm
# Reset global
lm._logging_manager = None
# Remove any existing handlers
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
lm.initialize_logging()
assert lm._logging_manager is not None
# Cleanup
root_logger.removeHandler(lm._logging_manager.handler)
def test_initialize_logging_does_not_recreate(self):
"""Test that initialize_logging doesn't recreate existing manager."""
import vocalinux.ui.logging_manager as lm
# Reset global
lm._logging_manager = None
# Remove any existing handlers
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
lm.initialize_logging()
first_manager = lm._logging_manager
lm.initialize_logging()
assert lm._logging_manager is first_manager
# Cleanup
root_logger.removeHandler(first_manager.handler)
class TestThreadSafety:
"""Tests for thread safety of LoggingManager."""
def test_concurrent_add_log_record(self):
"""Test that adding records from multiple threads is thread-safe."""
import vocalinux.ui.logging_manager as lm
# Reset global
lm._logging_manager = None
# Remove any existing handlers
root_logger = logging.getLogger()
handlers_to_remove = [h for h in root_logger.handlers if isinstance(h, lm.LoggingHandler)]
for h in handlers_to_remove:
root_logger.removeHandler(h)
manager = lm.LoggingManager(max_records=1000)
def add_records(thread_id):
for i in range(100):
record = lm.LogRecord(
timestamp=datetime.now(),
level="INFO",
logger_name=f"thread_{thread_id}",
message=f"Message {i}",
)
manager.add_log_record(record)
threads = [threading.Thread(target=add_records, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
# Should have records from all threads (plus any init messages)
assert len(manager.log_records) >= 500
# Cleanup
root_logger.removeHandler(manager.handler)