semantic update
This commit is contained in:
397
backend/tests/test_log_persistence.py
Normal file
397
backend/tests/test_log_persistence.py
Normal file
@@ -0,0 +1,397 @@
|
||||
# [DEF:test_log_persistence:Module]
|
||||
# @SEMANTICS: test, log, persistence, unit_test
|
||||
# @PURPOSE: Unit tests for TaskLogPersistenceService.
|
||||
# @LAYER: Test
|
||||
# @RELATION: TESTS -> TaskLogPersistenceService
|
||||
# @TIER: STANDARD
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.core.task_manager.persistence import TaskLogPersistenceService
|
||||
from src.core.task_manager.models import LogEntry
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:TestLogPersistence:Class]
|
||||
# @PURPOSE: Test suite for TaskLogPersistenceService.
|
||||
# @TIER: STANDARD
|
||||
class TestLogPersistence:
|
||||
|
||||
# [DEF:setup_class:Function]
|
||||
# @PURPOSE: Setup test database and service instance.
|
||||
# @PRE: None.
|
||||
# @POST: In-memory database and service instance created.
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
"""Create an in-memory database for testing."""
|
||||
cls.engine = create_engine("sqlite:///:memory:")
|
||||
cls.SessionLocal = sessionmaker(bind=cls.engine)
|
||||
cls.service = TaskLogPersistenceService(cls.engine)
|
||||
# [/DEF:setup_class:Function]
|
||||
|
||||
# [DEF:teardown_class:Function]
|
||||
# @PURPOSE: Clean up test database.
|
||||
# @PRE: None.
|
||||
# @POST: Database disposed.
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
"""Dispose of the database engine."""
|
||||
cls.engine.dispose()
|
||||
# [/DEF:teardown_class:Function]
|
||||
|
||||
# [DEF:setup_method:Function]
|
||||
# @PURPOSE: Setup for each test method.
|
||||
# @PRE: None.
|
||||
# @POST: Fresh database session created.
|
||||
def setup_method(self):
|
||||
"""Create a new session for each test."""
|
||||
self.session = self.SessionLocal()
|
||||
# [/DEF:setup_method:Function]
|
||||
|
||||
# [DEF:teardown_method:Function]
|
||||
# @PURPOSE: Cleanup after each test method.
|
||||
# @PRE: None.
|
||||
# @POST: Session closed and rolled back.
|
||||
def teardown_method(self):
|
||||
"""Close the session after each test."""
|
||||
self.session.close()
|
||||
# [/DEF:teardown_method:Function]
|
||||
|
||||
# [DEF:test_add_log_single:Function]
|
||||
# @PURPOSE: Test adding a single log entry.
|
||||
# @PRE: Service and session initialized.
|
||||
# @POST: Log entry persisted to database.
|
||||
def test_add_log_single(self):
|
||||
"""Test adding a single log entry."""
|
||||
entry = LogEntry(
|
||||
task_id="test-task-1",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="test_source",
|
||||
message="Test message"
|
||||
)
|
||||
|
||||
self.service.add_log(entry)
|
||||
|
||||
# Query the database
|
||||
result = self.session.query(LogEntry).filter_by(task_id="test-task-1").first()
|
||||
|
||||
assert result is not None
|
||||
assert result.level == "INFO"
|
||||
assert result.source == "test_source"
|
||||
assert result.message == "Test message"
|
||||
# [/DEF:test_add_log_single:Function]
|
||||
|
||||
# [DEF:test_add_log_batch:Function]
|
||||
# @PURPOSE: Test adding multiple log entries in batch.
|
||||
# @PRE: Service and session initialized.
|
||||
# @POST: All log entries persisted to database.
|
||||
def test_add_log_batch(self):
|
||||
"""Test adding multiple log entries in batch."""
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-2",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="source1",
|
||||
message="Message 1"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-2",
|
||||
timestamp=datetime.now(),
|
||||
level="WARNING",
|
||||
source="source2",
|
||||
message="Message 2"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-2",
|
||||
timestamp=datetime.now(),
|
||||
level="ERROR",
|
||||
source="source3",
|
||||
message="Message 3"
|
||||
),
|
||||
]
|
||||
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Query the database
|
||||
results = self.session.query(LogEntry).filter_by(task_id="test-task-2").all()
|
||||
|
||||
assert len(results) == 3
|
||||
assert results[0].level == "INFO"
|
||||
assert results[1].level == "WARNING"
|
||||
assert results[2].level == "ERROR"
|
||||
# [/DEF:test_add_log_batch:Function]
|
||||
|
||||
# [DEF:test_get_logs_by_task_id:Function]
|
||||
# @PURPOSE: Test retrieving logs by task ID.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns logs for the specified task.
|
||||
def test_get_logs_by_task_id(self):
|
||||
"""Test retrieving logs by task ID."""
|
||||
# Add test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-3",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="source1",
|
||||
message=f"Message {i}"
|
||||
)
|
||||
for i in range(5)
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Retrieve logs
|
||||
logs = self.service.get_logs("test-task-3")
|
||||
|
||||
assert len(logs) == 5
|
||||
assert all(log.task_id == "test-task-3" for log in logs)
|
||||
# [/DEF:test_get_logs_by_task_id:Function]
|
||||
|
||||
# [DEF:test_get_logs_with_filters:Function]
|
||||
# @PURPOSE: Test retrieving logs with level and source filters.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns filtered logs.
|
||||
def test_get_logs_with_filters(self):
|
||||
"""Test retrieving logs with level and source filters."""
|
||||
# Add test logs with different levels and sources
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-4",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="api",
|
||||
message="Info message"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-4",
|
||||
timestamp=datetime.now(),
|
||||
level="WARNING",
|
||||
source="api",
|
||||
message="Warning message"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-4",
|
||||
timestamp=datetime.now(),
|
||||
level="ERROR",
|
||||
source="storage",
|
||||
message="Error message"
|
||||
),
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Test level filter
|
||||
warning_logs = self.service.get_logs("test-task-4", level="WARNING")
|
||||
assert len(warning_logs) == 1
|
||||
assert warning_logs[0].level == "WARNING"
|
||||
|
||||
# Test source filter
|
||||
api_logs = self.service.get_logs("test-task-4", source="api")
|
||||
assert len(api_logs) == 2
|
||||
assert all(log.source == "api" for log in api_logs)
|
||||
|
||||
# Test combined filters
|
||||
api_warning_logs = self.service.get_logs("test-task-4", level="WARNING", source="api")
|
||||
assert len(api_warning_logs) == 1
|
||||
# [/DEF:test_get_logs_with_filters:Function]
|
||||
|
||||
# [DEF:test_get_logs_with_pagination:Function]
|
||||
# @PURPOSE: Test retrieving logs with pagination.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns paginated logs.
|
||||
def test_get_logs_with_pagination(self):
|
||||
"""Test retrieving logs with pagination."""
|
||||
# Add 15 test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-5",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="test",
|
||||
message=f"Message {i}"
|
||||
)
|
||||
for i in range(15)
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Test first page
|
||||
page1 = self.service.get_logs("test-task-5", limit=10, offset=0)
|
||||
assert len(page1) == 10
|
||||
|
||||
# Test second page
|
||||
page2 = self.service.get_logs("test-task-5", limit=10, offset=10)
|
||||
assert len(page2) == 5
|
||||
# [/DEF:test_get_logs_with_pagination:Function]
|
||||
|
||||
# [DEF:test_get_logs_with_search:Function]
|
||||
# @PURPOSE: Test retrieving logs with search query.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns logs matching search query.
|
||||
def test_get_logs_with_search(self):
|
||||
"""Test retrieving logs with search query."""
|
||||
# Add test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-6",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="api",
|
||||
message="User authentication successful"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-6",
|
||||
timestamp=datetime.now(),
|
||||
level="ERROR",
|
||||
source="api",
|
||||
message="Failed to connect to database"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-6",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="storage",
|
||||
message="File saved successfully"
|
||||
),
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Test search for "authentication"
|
||||
auth_logs = self.service.get_logs("test-task-6", search="authentication")
|
||||
assert len(auth_logs) == 1
|
||||
assert "authentication" in auth_logs[0].message.lower()
|
||||
|
||||
# Test search for "failed"
|
||||
failed_logs = self.service.get_logs("test-task-6", search="failed")
|
||||
assert len(failed_logs) == 1
|
||||
assert "failed" in failed_logs[0].message.lower()
|
||||
# [/DEF:test_get_logs_with_search:Function]
|
||||
|
||||
# [DEF:test_get_log_stats:Function]
|
||||
# @PURPOSE: Test retrieving log statistics.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns statistics grouped by level and source.
|
||||
def test_get_log_stats(self):
|
||||
"""Test retrieving log statistics."""
|
||||
# Add test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-7",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="api",
|
||||
message="Info 1"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-7",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="api",
|
||||
message="Info 2"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-7",
|
||||
timestamp=datetime.now(),
|
||||
level="WARNING",
|
||||
source="api",
|
||||
message="Warning 1"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-7",
|
||||
timestamp=datetime.now(),
|
||||
level="ERROR",
|
||||
source="storage",
|
||||
message="Error 1"
|
||||
),
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Get stats
|
||||
stats = self.service.get_log_stats("test-task-7")
|
||||
|
||||
assert stats is not None
|
||||
assert stats["by_level"]["INFO"] == 2
|
||||
assert stats["by_level"]["WARNING"] == 1
|
||||
assert stats["by_level"]["ERROR"] == 1
|
||||
assert stats["by_source"]["api"] == 3
|
||||
assert stats["by_source"]["storage"] == 1
|
||||
# [/DEF:test_get_log_stats:Function]
|
||||
|
||||
# [DEF:test_get_log_sources:Function]
|
||||
# @PURPOSE: Test retrieving unique log sources.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Returns list of unique sources.
|
||||
def test_get_log_sources(self):
|
||||
"""Test retrieving unique log sources."""
|
||||
# Add test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-8",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="api",
|
||||
message="Message 1"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-8",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="storage",
|
||||
message="Message 2"
|
||||
),
|
||||
LogEntry(
|
||||
task_id="test-task-8",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="git",
|
||||
message="Message 3"
|
||||
),
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Get sources
|
||||
sources = self.service.get_log_sources("test-task-8")
|
||||
|
||||
assert len(sources) == 3
|
||||
assert "api" in sources
|
||||
assert "storage" in sources
|
||||
assert "git" in sources
|
||||
# [/DEF:test_get_log_sources:Function]
|
||||
|
||||
# [DEF:test_delete_logs_by_task_id:Function]
|
||||
# @PURPOSE: Test deleting logs by task ID.
|
||||
# @PRE: Service and session initialized, logs exist.
|
||||
# @POST: Logs for the task are deleted.
|
||||
def test_delete_logs_by_task_id(self):
|
||||
"""Test deleting logs by task ID."""
|
||||
# Add test logs
|
||||
entries = [
|
||||
LogEntry(
|
||||
task_id="test-task-9",
|
||||
timestamp=datetime.now(),
|
||||
level="INFO",
|
||||
source="test",
|
||||
message=f"Message {i}"
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
self.service.add_logs(entries)
|
||||
|
||||
# Verify logs exist
|
||||
logs_before = self.service.get_logs("test-task-9")
|
||||
assert len(logs_before) == 3
|
||||
|
||||
# Delete logs
|
||||
self.service.delete_logs("test-task-9")
|
||||
|
||||
# Verify logs are deleted
|
||||
logs_after = self.service.get_logs("test-task-9")
|
||||
assert len(logs_after) == 0
|
||||
# [/DEF:test_delete_logs_by_task_id:Function]
|
||||
|
||||
# [/DEF:TestLogPersistence:Class]
|
||||
# [/DEF:test_log_persistence:Module]
|
||||
@@ -1,14 +1,30 @@
|
||||
import pytest
|
||||
from src.core.logger import belief_scope, logger
|
||||
import logging
|
||||
from src.core.logger import (
|
||||
belief_scope,
|
||||
logger,
|
||||
configure_logger,
|
||||
get_task_log_level,
|
||||
should_log_task_level
|
||||
)
|
||||
from src.core.config_models import LoggingConfig
|
||||
|
||||
|
||||
# [DEF:test_belief_scope_logs_entry_action_exit:Function]
|
||||
# @PURPOSE: Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs.
|
||||
# @PRE: belief_scope is available. caplog fixture is used.
|
||||
# @POST: Logs are verified to contain Entry, Action, and Exit tags.
|
||||
def test_belief_scope_logs_entry_action_exit(caplog):
|
||||
"""Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs."""
|
||||
caplog.set_level("INFO")
|
||||
# [DEF:test_belief_scope_logs_entry_action_exit_at_debug:Function]
|
||||
# @PURPOSE: Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs at DEBUG level.
|
||||
# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.
|
||||
# @POST: Logs are verified to contain Entry, Action, and Exit tags at DEBUG level.
|
||||
def test_belief_scope_logs_entry_action_exit_at_debug(caplog):
|
||||
"""Test that belief_scope generates [ID][Entry], [ID][Action], and [ID][Exit] logs at DEBUG level."""
|
||||
# Configure logger to DEBUG level
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with belief_scope("TestFunction"):
|
||||
logger.info("Doing something important")
|
||||
@@ -19,16 +35,28 @@ def test_belief_scope_logs_entry_action_exit(caplog):
|
||||
assert any("[TestFunction][Entry]" in msg for msg in log_messages), "Entry log not found"
|
||||
assert any("[TestFunction][Action] Doing something important" in msg for msg in log_messages), "Action log not found"
|
||||
assert any("[TestFunction][Exit]" in msg for msg in log_messages), "Exit log not found"
|
||||
# [/DEF:test_belief_scope_logs_entry_action_exit:Function]
|
||||
|
||||
# Reset to INFO
|
||||
config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True)
|
||||
configure_logger(config)
|
||||
# [/DEF:test_belief_scope_logs_entry_action_exit_at_debug:Function]
|
||||
|
||||
|
||||
# [DEF:test_belief_scope_error_handling:Function]
|
||||
# @PURPOSE: Test that belief_scope logs Coherence:Failed on exception.
|
||||
# @PRE: belief_scope is available. caplog fixture is used.
|
||||
# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.
|
||||
# @POST: Logs are verified to contain Coherence:Failed tag.
|
||||
def test_belief_scope_error_handling(caplog):
|
||||
"""Test that belief_scope logs Coherence:Failed on exception."""
|
||||
caplog.set_level("INFO")
|
||||
# Configure logger to DEBUG level
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with belief_scope("FailingFunction"):
|
||||
@@ -39,16 +67,28 @@ def test_belief_scope_error_handling(caplog):
|
||||
assert any("[FailingFunction][Entry]" in msg for msg in log_messages), "Entry log not found"
|
||||
assert any("[FailingFunction][Coherence:Failed]" in msg for msg in log_messages), "Failed coherence log not found"
|
||||
# Exit should not be logged on failure
|
||||
|
||||
# Reset to INFO
|
||||
config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True)
|
||||
configure_logger(config)
|
||||
# [/DEF:test_belief_scope_error_handling:Function]
|
||||
|
||||
|
||||
# [DEF:test_belief_scope_success_coherence:Function]
|
||||
# @PURPOSE: Test that belief_scope logs Coherence:OK on success.
|
||||
# @PRE: belief_scope is available. caplog fixture is used.
|
||||
# @PRE: belief_scope is available. caplog fixture is used. Logger configured to DEBUG.
|
||||
# @POST: Logs are verified to contain Coherence:OK tag.
|
||||
def test_belief_scope_success_coherence(caplog):
|
||||
"""Test that belief_scope logs Coherence:OK on success."""
|
||||
caplog.set_level("INFO")
|
||||
# Configure logger to DEBUG level
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with belief_scope("SuccessFunction"):
|
||||
pass
|
||||
@@ -56,4 +96,119 @@ def test_belief_scope_success_coherence(caplog):
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
|
||||
assert any("[SuccessFunction][Coherence:OK]" in msg for msg in log_messages), "Success coherence log not found"
|
||||
# [/DEF:test_belief_scope_success_coherence:Function]
|
||||
|
||||
# Reset to INFO
|
||||
config = LoggingConfig(level="INFO", task_log_level="INFO", enable_belief_state=True)
|
||||
configure_logger(config)
|
||||
# [/DEF:test_belief_scope_success_coherence:Function]
|
||||
|
||||
|
||||
# [DEF:test_belief_scope_not_visible_at_info:Function]
|
||||
# @PURPOSE: Test that belief_scope Entry/Exit/Coherence logs are NOT visible at INFO level.
|
||||
# @PRE: belief_scope is available. caplog fixture is used.
|
||||
# @POST: Entry/Exit/Coherence logs are not captured at INFO level.
|
||||
def test_belief_scope_not_visible_at_info(caplog):
|
||||
"""Test that belief_scope Entry/Exit/Coherence logs are NOT visible at INFO level."""
|
||||
caplog.set_level("INFO")
|
||||
|
||||
with belief_scope("InfoLevelFunction"):
|
||||
logger.info("Doing something important")
|
||||
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
|
||||
# Action log should be visible
|
||||
assert any("[InfoLevelFunction][Action] Doing something important" in msg for msg in log_messages), "Action log not found"
|
||||
# Entry/Exit/Coherence should NOT be visible at INFO level
|
||||
assert not any("[InfoLevelFunction][Entry]" in msg for msg in log_messages), "Entry log should not be visible at INFO"
|
||||
assert not any("[InfoLevelFunction][Exit]" in msg for msg in log_messages), "Exit log should not be visible at INFO"
|
||||
assert not any("[InfoLevelFunction][Coherence:OK]" in msg for msg in log_messages), "Coherence log should not be visible at INFO"
|
||||
# [/DEF:test_belief_scope_not_visible_at_info:Function]
|
||||
|
||||
|
||||
# [DEF:test_task_log_level_default:Function]
|
||||
# @PURPOSE: Test that default task log level is INFO.
|
||||
# @PRE: None.
|
||||
# @POST: Default level is INFO.
|
||||
def test_task_log_level_default():
|
||||
"""Test that default task log level is INFO."""
|
||||
level = get_task_log_level()
|
||||
assert level == "INFO"
|
||||
# [/DEF:test_task_log_level_default:Function]
|
||||
|
||||
|
||||
# [DEF:test_should_log_task_level:Function]
|
||||
# @PURPOSE: Test that should_log_task_level correctly filters log levels.
|
||||
# @PRE: None.
|
||||
# @POST: Filtering works correctly for all level combinations.
|
||||
def test_should_log_task_level():
|
||||
"""Test that should_log_task_level correctly filters log levels."""
|
||||
# Default level is INFO
|
||||
assert should_log_task_level("ERROR") is True, "ERROR should be logged at INFO threshold"
|
||||
assert should_log_task_level("WARNING") is True, "WARNING should be logged at INFO threshold"
|
||||
assert should_log_task_level("INFO") is True, "INFO should be logged at INFO threshold"
|
||||
assert should_log_task_level("DEBUG") is False, "DEBUG should NOT be logged at INFO threshold"
|
||||
# [/DEF:test_should_log_task_level:Function]
|
||||
|
||||
|
||||
# [DEF:test_configure_logger_task_log_level:Function]
|
||||
# @PURPOSE: Test that configure_logger updates task_log_level.
|
||||
# @PRE: LoggingConfig is available.
|
||||
# @POST: task_log_level is updated correctly.
|
||||
def test_configure_logger_task_log_level():
|
||||
"""Test that configure_logger updates task_log_level."""
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
|
||||
assert get_task_log_level() == "DEBUG", "task_log_level should be DEBUG"
|
||||
assert should_log_task_level("DEBUG") is True, "DEBUG should be logged at DEBUG threshold"
|
||||
|
||||
# Reset to INFO
|
||||
config = LoggingConfig(
|
||||
level="INFO",
|
||||
task_log_level="INFO",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
assert get_task_log_level() == "INFO", "task_log_level should be reset to INFO"
|
||||
# [/DEF:test_configure_logger_task_log_level:Function]
|
||||
|
||||
|
||||
# [DEF:test_enable_belief_state_flag:Function]
|
||||
# @PURPOSE: Test that enable_belief_state flag controls belief_scope logging.
|
||||
# @PRE: LoggingConfig is available. caplog fixture is used.
|
||||
# @POST: belief_scope logs are controlled by the flag.
|
||||
def test_enable_belief_state_flag(caplog):
|
||||
"""Test that enable_belief_state flag controls belief_scope logging."""
|
||||
# Disable belief state
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=False
|
||||
)
|
||||
configure_logger(config)
|
||||
|
||||
caplog.set_level("DEBUG")
|
||||
|
||||
with belief_scope("DisabledFunction"):
|
||||
logger.info("Doing something")
|
||||
|
||||
log_messages = [record.message for record in caplog.records]
|
||||
|
||||
# Entry and Exit should NOT be logged when disabled
|
||||
assert not any("[DisabledFunction][Entry]" in msg for msg in log_messages), "Entry should not be logged when disabled"
|
||||
assert not any("[DisabledFunction][Exit]" in msg for msg in log_messages), "Exit should not be logged when disabled"
|
||||
# Coherence:OK should still be logged (internal tracking)
|
||||
assert any("[DisabledFunction][Coherence:OK]" in msg for msg in log_messages), "Coherence should still be logged"
|
||||
|
||||
# Re-enable for other tests
|
||||
config = LoggingConfig(
|
||||
level="DEBUG",
|
||||
task_log_level="DEBUG",
|
||||
enable_belief_state=True
|
||||
)
|
||||
configure_logger(config)
|
||||
# [/DEF:test_enable_belief_state_flag:Function]
|
||||
377
backend/tests/test_task_logger.py
Normal file
377
backend/tests/test_task_logger.py
Normal file
@@ -0,0 +1,377 @@
|
||||
# [DEF:test_task_logger:Module]
|
||||
# @SEMANTICS: test, task_logger, task_context, unit_test
|
||||
# @PURPOSE: Unit tests for TaskLogger and TaskContext.
|
||||
# @LAYER: Test
|
||||
# @RELATION: TESTS -> TaskLogger, TaskContext
|
||||
# @TIER: STANDARD
|
||||
|
||||
# [SECTION: IMPORTS]
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from datetime import datetime
|
||||
|
||||
from src.core.task_manager.task_logger import TaskLogger
|
||||
from src.core.task_manager.context import TaskContext
|
||||
from src.core.task_manager.models import LogEntry
|
||||
# [/SECTION]
|
||||
|
||||
# [DEF:TestTaskLogger:Class]
|
||||
# @PURPOSE: Test suite for TaskLogger.
|
||||
# @TIER: STANDARD
|
||||
class TestTaskLogger:
|
||||
|
||||
# [DEF:setup_method:Function]
|
||||
# @PURPOSE: Setup for each test method.
|
||||
# @PRE: None.
|
||||
# @POST: Mock add_log_fn created.
|
||||
def setup_method(self):
|
||||
"""Create a mock add_log function for testing."""
|
||||
self.mock_add_log = Mock()
|
||||
self.logger = TaskLogger(
|
||||
task_id="test-task-1",
|
||||
add_log_fn=self.mock_add_log,
|
||||
source="test_source"
|
||||
)
|
||||
# [/DEF:setup_method:Function]
|
||||
|
||||
# [DEF:test_init:Function]
|
||||
# @PURPOSE: Test TaskLogger initialization.
|
||||
# @PRE: None.
|
||||
# @POST: Logger instance created with correct attributes.
|
||||
def test_init(self):
|
||||
"""Test TaskLogger initialization."""
|
||||
assert self.logger._task_id == "test-task-1"
|
||||
assert self.logger._default_source == "test_source"
|
||||
assert self.logger._add_log == self.mock_add_log
|
||||
# [/DEF:test_init:Function]
|
||||
|
||||
# [DEF:test_with_source:Function]
|
||||
# @PURPOSE: Test creating a sub-logger with different source.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: New logger created with different source but same task_id.
|
||||
def test_with_source(self):
|
||||
"""Test creating a sub-logger with different source."""
|
||||
sub_logger = self.logger.with_source("new_source")
|
||||
|
||||
assert sub_logger._task_id == "test-task-1"
|
||||
assert sub_logger._default_source == "new_source"
|
||||
assert sub_logger._add_log == self.mock_add_log
|
||||
# [/DEF:test_with_source:Function]
|
||||
|
||||
# [DEF:test_debug:Function]
|
||||
# @PURPOSE: Test debug log level.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with DEBUG level.
|
||||
def test_debug(self):
|
||||
"""Test debug logging."""
|
||||
self.logger.debug("Debug message")
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="DEBUG",
|
||||
message="Debug message",
|
||||
source="test_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_debug:Function]
|
||||
|
||||
# [DEF:test_info:Function]
|
||||
# @PURPOSE: Test info log level.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with INFO level.
|
||||
def test_info(self):
|
||||
"""Test info logging."""
|
||||
self.logger.info("Info message")
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="INFO",
|
||||
message="Info message",
|
||||
source="test_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_info:Function]
|
||||
|
||||
# [DEF:test_warning:Function]
|
||||
# @PURPOSE: Test warning log level.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with WARNING level.
|
||||
def test_warning(self):
|
||||
"""Test warning logging."""
|
||||
self.logger.warning("Warning message")
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="WARNING",
|
||||
message="Warning message",
|
||||
source="test_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_warning:Function]
|
||||
|
||||
# [DEF:test_error:Function]
|
||||
# @PURPOSE: Test error log level.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with ERROR level.
|
||||
def test_error(self):
|
||||
"""Test error logging."""
|
||||
self.logger.error("Error message")
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="ERROR",
|
||||
message="Error message",
|
||||
source="test_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_error:Function]
|
||||
|
||||
# [DEF:test_error_with_metadata:Function]
|
||||
# @PURPOSE: Test error logging with metadata.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with ERROR level and metadata.
|
||||
def test_error_with_metadata(self):
|
||||
"""Test error logging with metadata."""
|
||||
metadata = {"error_code": 500, "details": "Connection failed"}
|
||||
self.logger.error("Error message", metadata=metadata)
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="ERROR",
|
||||
message="Error message",
|
||||
source="test_source",
|
||||
metadata=metadata
|
||||
)
|
||||
# [/DEF:test_error_with_metadata:Function]
|
||||
|
||||
# [DEF:test_progress:Function]
|
||||
# @PURPOSE: Test progress logging.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with INFO level and progress metadata.
|
||||
def test_progress(self):
|
||||
"""Test progress logging."""
|
||||
self.logger.progress("Processing items", percent=50)
|
||||
|
||||
expected_metadata = {"progress": 50}
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="INFO",
|
||||
message="Processing items",
|
||||
source="test_source",
|
||||
metadata=expected_metadata
|
||||
)
|
||||
# [/DEF:test_progress:Function]
|
||||
|
||||
# [DEF:test_progress_clamping:Function]
|
||||
# @PURPOSE: Test progress value clamping (0-100).
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: Progress values clamped to 0-100 range.
|
||||
def test_progress_clamping(self):
|
||||
"""Test progress value clamping."""
|
||||
# Test below 0
|
||||
self.logger.progress("Below 0", percent=-10)
|
||||
call1 = self.mock_add_log.call_args_list[0]
|
||||
assert call1.kwargs["metadata"]["progress"] == 0
|
||||
|
||||
self.mock_add_log.reset_mock()
|
||||
|
||||
# Test above 100
|
||||
self.logger.progress("Above 100", percent=150)
|
||||
call2 = self.mock_add_log.call_args_list[0]
|
||||
assert call2.kwargs["metadata"]["progress"] == 100
|
||||
# [/DEF:test_progress_clamping:Function]
|
||||
|
||||
# [DEF:test_source_override:Function]
|
||||
# @PURPOSE: Test overriding the default source.
|
||||
# @PRE: Logger initialized.
|
||||
# @POST: add_log_fn called with overridden source.
|
||||
def test_source_override(self):
|
||||
"""Test overriding the default source."""
|
||||
self.logger.info("Message", source="override_source")
|
||||
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-1",
|
||||
level="INFO",
|
||||
message="Message",
|
||||
source="override_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_source_override:Function]
|
||||
|
||||
# [DEF:test_sub_logger_source_independence:Function]
|
||||
# @PURPOSE: Test sub-logger independence from parent.
|
||||
# @PRE: Logger and sub-logger initialized.
|
||||
# @POST: Sub-logger has different source, parent unchanged.
|
||||
def test_sub_logger_source_independence(self):
|
||||
"""Test sub-logger source independence from parent."""
|
||||
sub_logger = self.logger.with_source("sub_source")
|
||||
|
||||
# Log with parent
|
||||
self.logger.info("Parent message")
|
||||
|
||||
# Log with sub-logger
|
||||
sub_logger.info("Sub message")
|
||||
|
||||
# Verify both calls were made with correct sources
|
||||
calls = self.mock_add_log.call_args_list
|
||||
assert len(calls) == 2
|
||||
assert calls[0].kwargs["source"] == "test_source"
|
||||
assert calls[1].kwargs["source"] == "sub_source"
|
||||
# [/DEF:test_sub_logger_source_independence:Function]
|
||||
|
||||
# [/DEF:TestTaskLogger:Class]
|
||||
|
||||
# [DEF:TestTaskContext:Class]
|
||||
# @PURPOSE: Test suite for TaskContext.
|
||||
# @TIER: STANDARD
|
||||
class TestTaskContext:
|
||||
|
||||
# [DEF:setup_method:Function]
|
||||
# @PURPOSE: Setup for each test method.
|
||||
# @PRE: None.
|
||||
# @POST: Mock add_log_fn created.
|
||||
def setup_method(self):
|
||||
"""Create a mock add_log function for testing."""
|
||||
self.mock_add_log = Mock()
|
||||
self.params = {"param1": "value1", "param2": "value2"}
|
||||
self.context = TaskContext(
|
||||
task_id="test-task-2",
|
||||
add_log_fn=self.mock_add_log,
|
||||
params=self.params,
|
||||
default_source="plugin"
|
||||
)
|
||||
# [/DEF:setup_method:Function]
|
||||
|
||||
# [DEF:test_init:Function]
|
||||
# @PURPOSE: Test TaskContext initialization.
|
||||
# @PRE: None.
|
||||
# @POST: Context instance created with correct attributes.
|
||||
def test_init(self):
|
||||
"""Test TaskContext initialization."""
|
||||
assert self.context._task_id == "test-task-2"
|
||||
assert self.context._params == self.params
|
||||
assert isinstance(self.context._logger, TaskLogger)
|
||||
assert self.context._logger._default_source == "plugin"
|
||||
# [/DEF:test_init:Function]
|
||||
|
||||
# [DEF:test_task_id_property:Function]
|
||||
# @PURPOSE: Test task_id property.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Returns correct task_id.
|
||||
def test_task_id_property(self):
|
||||
"""Test task_id property."""
|
||||
assert self.context.task_id == "test-task-2"
|
||||
# [/DEF:test_task_id_property:Function]
|
||||
|
||||
# [DEF:test_logger_property:Function]
|
||||
# @PURPOSE: Test logger property.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Returns TaskLogger instance.
|
||||
def test_logger_property(self):
|
||||
"""Test logger property."""
|
||||
logger = self.context.logger
|
||||
assert isinstance(logger, TaskLogger)
|
||||
assert logger._task_id == "test-task-2"
|
||||
assert logger._default_source == "plugin"
|
||||
# [/DEF:test_logger_property:Function]
|
||||
|
||||
# [DEF:test_params_property:Function]
|
||||
# @PURPOSE: Test params property.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Returns correct params dict.
|
||||
def test_params_property(self):
|
||||
"""Test params property."""
|
||||
assert self.context.params == self.params
|
||||
# [/DEF:test_params_property:Function]
|
||||
|
||||
# [DEF:test_get_param:Function]
|
||||
# @PURPOSE: Test getting a specific parameter.
|
||||
# @PRE: Context initialized with params.
|
||||
# @POST: Returns parameter value or default.
|
||||
def test_get_param(self):
|
||||
"""Test getting a specific parameter."""
|
||||
assert self.context.get_param("param1") == "value1"
|
||||
assert self.context.get_param("param2") == "value2"
|
||||
assert self.context.get_param("nonexistent") is None
|
||||
assert self.context.get_param("nonexistent", "default") == "default"
|
||||
# [/DEF:test_get_param:Function]
|
||||
|
||||
# [DEF:test_create_sub_context:Function]
|
||||
# @PURPOSE: Test creating a sub-context with different source.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: New context created with different logger source.
|
||||
def test_create_sub_context(self):
|
||||
"""Test creating a sub-context with different source."""
|
||||
sub_context = self.context.create_sub_context("new_source")
|
||||
|
||||
assert sub_context._task_id == "test-task-2"
|
||||
assert sub_context._params == self.params
|
||||
assert sub_context._logger._default_source == "new_source"
|
||||
assert sub_context._logger._task_id == "test-task-2"
|
||||
# [/DEF:test_create_sub_context:Function]
|
||||
|
||||
# [DEF:test_context_logger_delegates_to_task_logger:Function]
|
||||
# @PURPOSE: Test context logger delegates to TaskLogger.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Logger calls are delegated to TaskLogger.
|
||||
def test_context_logger_delegates_to_task_logger(self):
|
||||
"""Test context logger delegates to TaskLogger."""
|
||||
# Call through context
|
||||
self.context.logger.info("Test message")
|
||||
|
||||
# Verify the mock was called
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-2",
|
||||
level="INFO",
|
||||
message="Test message",
|
||||
source="plugin",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_context_logger_delegates_to_task_logger:Function]
|
||||
|
||||
# [DEF:test_sub_context_with_source:Function]
|
||||
# @PURPOSE: Test sub-context logger uses new source.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Sub-context logger uses new source.
|
||||
def test_sub_context_with_source(self):
|
||||
"""Test sub-context logger uses new source."""
|
||||
sub_context = self.context.create_sub_context("api_source")
|
||||
|
||||
# Log through sub-context
|
||||
sub_context.logger.info("API message")
|
||||
|
||||
# Verify the mock was called with new source
|
||||
self.mock_add_log.assert_called_once_with(
|
||||
task_id="test-task-2",
|
||||
level="INFO",
|
||||
message="API message",
|
||||
source="api_source",
|
||||
metadata=None
|
||||
)
|
||||
# [/DEF:test_sub_context_with_source:Function]
|
||||
|
||||
# [DEF:test_multiple_sub_contexts:Function]
|
||||
# @PURPOSE: Test creating multiple sub-contexts.
|
||||
# @PRE: Context initialized.
|
||||
# @POST: Each sub-context has independent logger source.
|
||||
def test_multiple_sub_contexts(self):
|
||||
"""Test creating multiple sub-contexts."""
|
||||
sub1 = self.context.create_sub_context("source1")
|
||||
sub2 = self.context.create_sub_context("source2")
|
||||
sub3 = self.context.create_sub_context("source3")
|
||||
|
||||
assert sub1._logger._default_source == "source1"
|
||||
assert sub2._logger._default_source == "source2"
|
||||
assert sub3._logger._default_source == "source3"
|
||||
|
||||
# All should have same task_id and params
|
||||
assert sub1._task_id == "test-task-2"
|
||||
assert sub2._task_id == "test-task-2"
|
||||
assert sub3._task_id == "test-task-2"
|
||||
assert sub1._params == self.params
|
||||
assert sub2._params == self.params
|
||||
assert sub3._params == self.params
|
||||
# [/DEF:test_multiple_sub_contexts:Function]
|
||||
|
||||
# [/DEF:TestTaskContext:Class]
|
||||
# [/DEF:test_task_logger:Module]
|
||||
Reference in New Issue
Block a user