Files
voculinux/tests/test_logging_manager.py
T
Jatin K MalikandGitHub 9e5b632710 test: Improve test coverage to 80%+ (#122)
* test: Improve test coverage to 80%

Increased test coverage from ~33% to 80.15% statement coverage.

New test files added:
- test_action_handler.py (100% coverage)
- test_version.py (100% coverage)
- test_logging_manager.py (100% coverage)
- test_common_types.py
- test_speech_recognition_init.py (100% coverage)
- test_resource_manager.py (93% coverage)

Significantly expanded existing tests:
- test_tray_indicator.py: 76% -> 98%
- test_main.py: 50% -> 92%
- test_text_injector.py: 60% -> 94%
- test_recognition_manager.py: 30% -> 48%
- test_command_processor.py: 85% -> 98%
- test_keyboard_shortcuts.py: 88% -> 95%
- test_config_manager.py: improved to 98%
- test_audio_feedback.py: improved to 92%

Configuration changes:
- Updated pyproject.toml to exclude untestable GTK dialog files
  (settings_dialog.py, logging_dialog.py) from coverage metrics

Key testing improvements:
- Added comprehensive GTK mocking for UI components
- Added edge case tests for text injection on X11/Wayland
- Added keyboard shortcut injection tests
- Added error handling and callback tests
- Added configuration management tests

* fix: Use runtime CI mode detection to fix Python 3.8/3.10 tests

Change CI_MODE from module-level constant to _is_ci_mode() function
that is evaluated at runtime. This ensures pytest detection works
correctly on all Python versions, since pytest modules may not be
loaded at import time.

* fix: Add PYTEST_RUNNING env var for reliable pytest detection in CI

The previous approach to detect pytest at runtime wasn't working
reliably on Python 3.8/3.10 in CI environments. This adds a
PYTEST_RUNNING=1 environment variable set early in conftest.py
that _is_ci_mode() can check to reliably determine if tests are
running.

* fix: Use patch.object for reliable mocking in audio_feedback tests

The previous approach using patch() with module paths didn't work
reliably because the module reimport caused the patched objects
to not be the same as the ones the functions referenced.

Using patch.object() on the imported module's attributes ensures
the patches are applied to the actual objects the functions use.

* fix: Use patch.object for reliable mocking in test_settings_callback

The test was failing on Python 3.8/3.10 because the module-level patch
for SettingsDialog wasn't being applied reliably across all Python
versions due to import timing differences.

Using patch.object() on the imported module ensures the mock is applied
to the actual reference that _on_settings_clicked() uses, regardless of
Python version or module caching behavior.
2026-02-04 21:01:34 -08:00

683 lines
21 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_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="vocalinux.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 "VocaLinux 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="vocalinux.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 "vocalinux" 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)