Files
thechart/tests/conftest.py

258 lines
8.6 KiB
Python

"""
Fixtures and configuration for pytest tests.
"""
import os
import tempfile
import pytest
import pandas as pd
from unittest.mock import Mock
import logging
import warnings
import os as _os
# Force a headless-friendly Matplotlib backend in tests
_os.environ.setdefault("MPLBACKEND", "Agg")
@pytest.fixture(autouse=True, scope="session")
def _matplotlib_headless_backend():
"""Force Matplotlib to use the Agg backend for all tests.
Doing this at session scope ensures any pyplot usage in code under test
doesn't try to initialize interactive Tk backends.
"""
try:
import matplotlib as _mpl
_mpl.use("Agg", force=True)
except Exception:
# If Matplotlib isn't available or already configured, ignore.
pass
@pytest.fixture(autouse=True)
def _stub_pyplot_ui_calls(monkeypatch):
"""No-op pyplot UI calls that can be noisy or slow in CI.
This reduces flicker and avoids timing issues without changing behavior.
"""
try:
import matplotlib.pyplot as _plt
monkeypatch.setattr(_plt, "pause", lambda *args, **kwargs: None, raising=False)
monkeypatch.setattr(_plt, "draw", lambda *args, **kwargs: None, raising=False)
except Exception:
pass
@pytest.fixture(autouse=True, scope="session")
def _tune_reportlab_for_tests():
"""Apply small ReportLab tweaks for stable tests without heavy font checks."""
try:
from reportlab import rl_config
# Disable glyph warnings which are irrelevant for our tests
rl_config.warnOnMissingFontGlyphs = 0 # type: ignore[attr-defined]
except Exception:
pass
# Test-only warning hygiene to keep output clean while preserving behavior
# - Silence legacy deprecation shims that originate inside package internals
warnings.filterwarnings(
"ignore",
message=r".*search_filter is deprecated.*",
category=DeprecationWarning,
)
# - Silence a Pillow deprecation surfaced via Matplotlib's Tk backend used by tests
warnings.filterwarnings(
"ignore",
message=r".*'mode' parameter is deprecated and will be removed in Pillow 13.*",
category=DeprecationWarning,
)
# - Silence pandas parse fallback warning triggered intentionally by invalid test data
warnings.filterwarnings(
"ignore",
message=r"Could not infer format, so each element will be parsed individually.*",
category=UserWarning,
)
from thechart.managers import MedicineManager, Medicine
@pytest.fixture
def temp_csv_file():
"""Create a temporary CSV file for testing."""
fd, path = tempfile.mkstemp(suffix='.csv')
os.close(fd)
yield path
# Cleanup
if os.path.exists(path):
os.unlink(path)
@pytest.fixture
def mock_medicine_manager():
"""Create a mock medicine manager with default medicines for testing."""
mock_manager = Mock(spec=MedicineManager)
# Default medicines matching the original system
default_medicines = {
"bupropion": Medicine(
key="bupropion",
display_name="Bupropion",
dosage_info="150/300 mg",
quick_doses=["150", "300"],
color="#FF6B6B",
default_enabled=True
),
"hydroxyzine": Medicine(
key="hydroxyzine",
display_name="Hydroxyzine",
dosage_info="25 mg",
quick_doses=["25", "50"],
color="#4ECDC4",
default_enabled=False
),
"gabapentin": Medicine(
key="gabapentin",
display_name="Gabapentin",
dosage_info="100 mg",
quick_doses=["100", "300", "600"],
color="#45B7D1",
default_enabled=False
),
"propranolol": Medicine(
key="propranolol",
display_name="Propranolol",
dosage_info="10 mg",
quick_doses=["10", "20", "40"],
color="#96CEB4",
default_enabled=True
),
"quetiapine": Medicine(
key="quetiapine",
display_name="Quetiapine",
dosage_info="25 mg",
quick_doses=["25", "50", "100"],
color="#FFEAA7",
default_enabled=False
)
}
mock_manager.get_medicine_keys.return_value = list(default_medicines.keys())
mock_manager.get_all_medicines.return_value = default_medicines
mock_manager.get_medicine.side_effect = lambda key: default_medicines.get(key)
mock_manager.get_graph_colors.return_value = {k: v.color for k, v in default_medicines.items()}
mock_manager.get_quick_doses.side_effect = lambda key: default_medicines.get(key, Medicine("", "", "", [], "", False)).quick_doses
return mock_manager
@pytest.fixture
def mock_pathology_manager():
"""Create a mock pathology manager with default pathologies for testing."""
mock_manager = Mock()
# Default pathologies matching the original system
mock_manager.get_pathology_keys.return_value = ["depression", "anxiety", "sleep", "appetite"]
return mock_manager
@pytest.fixture
def sample_data():
"""Sample data for testing."""
return [
["2024-01-01", 3, 2, 4, 3, 1, "", 0, "", 2, "", 1, "", 0, "", "Test note 1"],
["2024-01-02", 2, 3, 3, 4, 1, "", 1, "", 2, "", 0, "", 1, "", "Test note 2"],
["2024-01-03", 4, 1, 5, 2, 0, "", 0, "", 1, "", 1, "", 0, "", ""],
]
@pytest.fixture
def sample_dataframe():
"""Sample DataFrame for testing."""
return pd.DataFrame({
'date': ['2024-01-01', '2024-01-02', '2024-01-03'],
'depression': [3, 2, 4],
'anxiety': [2, 3, 1],
'sleep': [4, 3, 5],
'appetite': [3, 4, 2],
'bupropion': [1, 1, 0],
'bupropion_doses': ['2024-01-01 08:00:00:150mg', '2024-01-02 08:00:00:300mg', ''],
'hydroxyzine': [0, 1, 0],
'hydroxyzine_doses': ['', '2024-01-02 20:00:00:25mg', ''],
'gabapentin': [2, 2, 1],
'gabapentin_doses': ['2024-01-01 12:00:00:100mg|2024-01-01 20:00:00:100mg',
'2024-01-02 12:00:00:100mg|2024-01-02 20:00:00:100mg',
'2024-01-03 12:00:00:100mg'],
'propranolol': [1, 0, 1],
'propranolol_doses': ['2024-01-01 12:00:00:10mg', '', '2024-01-03 12:00:00:20mg'],
'quetiapine': [0, 1, 0],
'quetiapine_doses': ['', '2024-01-02 22:00:00:50mg', ''],
'note': ['Test note 1', 'Test note 2', '']
})
@pytest.fixture
def mock_logger():
"""Mock logger for testing."""
return Mock(spec=logging.Logger)
@pytest.fixture
def temp_log_dir():
"""Create a temporary directory for log files."""
with tempfile.TemporaryDirectory() as temp_dir:
yield temp_dir
@pytest.fixture
def mock_env_vars(monkeypatch):
"""Mock environment variables."""
monkeypatch.setenv("LOG_LEVEL", "DEBUG")
monkeypatch.setenv("LOG_PATH", "/tmp/test_logs")
monkeypatch.setenv("LOG_CLEAR", "False")
@pytest.fixture
def sample_dose_data():
"""Sample dose data for testing dose calculation."""
return {
'standard_format': '2025-07-28 18:59:45:150mg|2025-07-28 19:34:19:75mg', # Should sum to 225
'with_bullets': '• • • • 2025-07-30 07:50:00:300', # Should be 300
'decimal_doses': '2025-07-28 18:59:45:12.5mg|2025-07-28 19:34:19:7.5mg', # Should sum to 20
'no_timestamp': '100mg|50mg', # Should sum to 150
'mixed_format': '• 2025-07-30 22:50:00:10|75mg', # Should sum to 85
'empty_string': '', # Should be 0
'nan_value': 'nan', # Should be 0
'no_units': '2025-07-28 18:59:45:10|2025-07-28 19:34:19:5', # Should sum to 15
}
@pytest.fixture
def legend_test_dataframe():
"""DataFrame specifically designed for testing legend functionality."""
return pd.DataFrame({
'date': ['2024-01-01', '2024-01-02', '2024-01-03'],
'depression': [3, 2, 4],
'anxiety': [2, 3, 1],
'sleep': [4, 3, 5],
'appetite': [3, 4, 2],
# Medicine with consistent doses for average testing
'bupropion': [1, 1, 1],
'bupropion_doses': ['2024-01-01 08:00:00:100mg',
'2024-01-02 08:00:00:200mg',
'2024-01-03 08:00:00:150mg'], # Average: 150mg
# Medicine with varying doses
'propranolol': [1, 1, 0],
'propranolol_doses': ['2024-01-01 12:00:00:10mg',
'2024-01-02 12:00:00:20mg',
''], # Average: 15mg (10+20)/2
# Medicines without dose data
'hydroxyzine': [0, 0, 0],
'hydroxyzine_doses': ['', '', ''],
'gabapentin': [0, 0, 0],
'gabapentin_doses': ['', '', ''],
'quetiapine': [0, 0, 0],
'quetiapine_doses': ['', '', ''],
'note': ['Test note 1', 'Test note 2', 'Test note 3']
})