feat: Add coverage, iniconfig, pluggy, pygments, pytest, pytest-cov, and pytest-mock as dependencies

- Added coverage version 7.10.1 with multiple wheel distributions.
- Added iniconfig version 2.1.0 with its wheel distribution.
- Added pluggy version 1.6.0 with its wheel distribution.
- Added pygments version 2.19.2 with its wheel distribution.
- Added pytest version 8.4.1 with its wheel distribution and dependencies.
- Added pytest-cov version 6.2.1 with its wheel distribution and dependencies.
- Added pytest-mock version 3.14.1 with its wheel distribution and dependencies.
- Updated dev-dependencies to include coverage, pytest, pytest-cov, and pytest-mock.
- Updated requires-dist to specify minimum versions for coverage, pytest, pytest-cov, and pytest-mock.
This commit is contained in:
William Valentin
2025-07-28 17:53:19 -07:00
parent 9aa1188c98
commit c20c4478a6
18 changed files with 2951 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
# Tests for TheChart application
+68
View File
@@ -0,0 +1,68 @@
"""
Fixtures and configuration for pytest tests.
"""
import os
import tempfile
import pytest
import pandas as pd
from unittest.mock import Mock
import logging
@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 sample_data():
"""Sample data for testing."""
return [
["2024-01-01", 3, 2, 4, 3, 1, 0, 2, 1, "Test note 1"],
["2024-01-02", 2, 3, 3, 4, 1, 1, 2, 0, "Test note 2"],
["2024-01-03", 4, 1, 5, 2, 0, 0, 1, 1, ""],
]
@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],
'hydroxyzine': [0, 1, 0],
'gabapentin': [2, 2, 1],
'propranolol': [1, 0, 1],
'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")
+130
View File
@@ -0,0 +1,130 @@
"""
Tests for constants module.
"""
import os
import pytest
from unittest.mock import patch
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
class TestConstants:
"""Test cases for the constants module."""
def test_default_log_level(self):
"""Test default LOG_LEVEL when not set in environment."""
with patch.dict(os.environ, {}, clear=True):
# Re-import to get fresh values
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_LEVEL == "INFO"
def test_custom_log_level(self):
"""Test custom LOG_LEVEL from environment."""
with patch.dict(os.environ, {'LOG_LEVEL': 'debug'}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_LEVEL == "DEBUG"
def test_default_log_path(self):
"""Test default LOG_PATH when not set in environment."""
with patch.dict(os.environ, {}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_PATH == "/tmp/logs/thechart"
def test_custom_log_path(self):
"""Test custom LOG_PATH from environment."""
with patch.dict(os.environ, {'LOG_PATH': '/custom/log/path'}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_PATH == "/custom/log/path"
def test_default_log_clear(self):
"""Test default LOG_CLEAR when not set in environment."""
with patch.dict(os.environ, {}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_CLEAR == "False"
def test_custom_log_clear_true(self):
"""Test LOG_CLEAR when set to true in environment."""
with patch.dict(os.environ, {'LOG_CLEAR': 'true'}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_CLEAR == "True"
def test_custom_log_clear_false(self):
"""Test LOG_CLEAR when set to false in environment."""
with patch.dict(os.environ, {'LOG_CLEAR': 'false'}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_CLEAR == "False"
def test_log_level_case_insensitive(self):
"""Test that LOG_LEVEL is converted to uppercase."""
with patch.dict(os.environ, {'LOG_LEVEL': 'warning'}, clear=True):
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
assert constants.LOG_LEVEL == "WARNING"
def test_dotenv_override(self):
"""Test that dotenv override parameter is set to True."""
# This is a structural test since dotenv is loaded during import
with patch('constants.load_dotenv') as mock_load_dotenv:
import importlib
if 'constants' in sys.modules:
importlib.reload(sys.modules['constants'])
else:
import constants
mock_load_dotenv.assert_called_once_with(override=True)
def test_all_constants_are_strings(self):
"""Test that all constants are string type."""
import constants
assert isinstance(constants.LOG_LEVEL, str)
assert isinstance(constants.LOG_PATH, str)
assert isinstance(constants.LOG_CLEAR, str)
def test_constants_not_empty(self):
"""Test that constants are not empty strings."""
import constants
assert constants.LOG_LEVEL != ""
assert constants.LOG_PATH != ""
assert constants.LOG_CLEAR != ""
+285
View File
@@ -0,0 +1,285 @@
"""
Tests for the DataManager class.
"""
import os
import csv
import pytest
import pandas as pd
from unittest.mock import Mock, patch
import tempfile
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from data_manager import DataManager
class TestDataManager:
"""Test cases for the DataManager class."""
def test_init(self, temp_csv_file, mock_logger):
"""Test DataManager initialization."""
dm = DataManager(temp_csv_file, mock_logger)
assert dm.filename == temp_csv_file
assert dm.logger == mock_logger
assert os.path.exists(temp_csv_file)
def test_initialize_csv_creates_file_with_headers(self, temp_csv_file, mock_logger):
"""Test that initialize_csv creates a file with proper headers."""
# Remove the file if it exists
if os.path.exists(temp_csv_file):
os.unlink(temp_csv_file)
dm = DataManager(temp_csv_file, mock_logger)
# Check file exists and has correct headers
assert os.path.exists(temp_csv_file)
with open(temp_csv_file, 'r') as f:
reader = csv.reader(f)
headers = next(reader)
expected_headers = [
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
]
assert headers == expected_headers
def test_initialize_csv_does_not_overwrite_existing_file(self, temp_csv_file, mock_logger):
"""Test that initialize_csv does not overwrite existing file."""
# Write some data to the file first
with open(temp_csv_file, 'w') as f:
f.write("existing,data\n1,2\n")
dm = DataManager(temp_csv_file, mock_logger)
# Check that existing data is preserved
with open(temp_csv_file, 'r') as f:
content = f.read()
assert "existing,data" in content
def test_load_data_empty_file(self, temp_csv_file, mock_logger):
"""Test loading data from an empty file."""
dm = DataManager(temp_csv_file, mock_logger)
df = dm.load_data()
assert df.empty
def test_load_data_nonexistent_file(self, mock_logger):
"""Test loading data from a nonexistent file."""
dm = DataManager("nonexistent.csv", mock_logger)
df = dm.load_data()
assert df.empty
mock_logger.warning.assert_called()
def test_load_data_with_valid_data(self, temp_csv_file, mock_logger, sample_data):
"""Test loading valid data from CSV file."""
# Write sample data to file
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
# Write headers first
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
# Write sample data
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
df = dm.load_data()
assert not df.empty
assert len(df) == 3
assert list(df.columns) == [
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
]
# Check data types
assert df["depression"].dtype == int
assert df["anxiety"].dtype == int
assert df["note"].dtype == object
def test_load_data_sorted_by_date(self, temp_csv_file, mock_logger):
"""Test that loaded data is sorted by date."""
# Write data in random order
unsorted_data = [
["2024-01-03", 1, 1, 1, 1, 1, 1, 1, 1, "third"],
["2024-01-01", 2, 2, 2, 2, 2, 2, 2, 2, "first"],
["2024-01-02", 3, 3, 3, 3, 3, 3, 3, 3, "second"],
]
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(unsorted_data)
dm = DataManager(temp_csv_file, mock_logger)
df = dm.load_data()
# Check that data is sorted by date
assert df.iloc[0]["note"] == "first"
assert df.iloc[1]["note"] == "second"
assert df.iloc[2]["note"] == "third"
def test_add_entry_success(self, temp_csv_file, mock_logger):
"""Test successfully adding an entry."""
dm = DataManager(temp_csv_file, mock_logger)
entry = ["2024-01-01", 3, 2, 4, 3, 1, 0, 2, 1, "Test note"]
result = dm.add_entry(entry)
assert result is True
# Verify entry was added
df = dm.load_data()
assert len(df) == 1
assert df.iloc[0]["date"] == "2024-01-01"
assert df.iloc[0]["note"] == "Test note"
def test_add_entry_duplicate_date(self, temp_csv_file, mock_logger, sample_data):
"""Test adding entry with duplicate date."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
# Try to add entry with existing date
duplicate_entry = ["2024-01-01", 5, 5, 5, 5, 1, 1, 1, 1, "Duplicate"]
result = dm.add_entry(duplicate_entry)
assert result is False
mock_logger.warning.assert_called_with("Entry with date 2024-01-01 already exists.")
def test_update_entry_success(self, temp_csv_file, mock_logger, sample_data):
"""Test successfully updating an entry."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
updated_values = ["2024-01-01", 5, 5, 5, 5, 2, 2, 2, 2, "Updated note"]
result = dm.update_entry("2024-01-01", updated_values)
assert result is True
# Verify entry was updated
df = dm.load_data()
updated_row = df[df["date"] == "2024-01-01"].iloc[0]
assert updated_row["depression"] == 5
assert updated_row["note"] == "Updated note"
def test_update_entry_change_date(self, temp_csv_file, mock_logger, sample_data):
"""Test updating an entry with a date change."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
updated_values = ["2024-01-05", 5, 5, 5, 5, 2, 2, 2, 2, "Updated note"]
result = dm.update_entry("2024-01-01", updated_values)
assert result is True
# Verify old date is gone and new date exists
df = dm.load_data()
assert not any(df["date"] == "2024-01-01")
assert any(df["date"] == "2024-01-05")
def test_update_entry_duplicate_date(self, temp_csv_file, mock_logger, sample_data):
"""Test updating entry to a date that already exists."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
# Try to change date to one that already exists
updated_values = ["2024-01-02", 5, 5, 5, 5, 2, 2, 2, 2, "Updated note"]
result = dm.update_entry("2024-01-01", updated_values)
assert result is False
mock_logger.warning.assert_called_with(
"Cannot update: entry with date 2024-01-02 already exists."
)
def test_delete_entry_success(self, temp_csv_file, mock_logger, sample_data):
"""Test successfully deleting an entry."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
result = dm.delete_entry("2024-01-02")
assert result is True
# Verify entry was deleted
df = dm.load_data()
assert len(df) == 2
assert not any(df["date"] == "2024-01-02")
def test_delete_entry_nonexistent(self, temp_csv_file, mock_logger, sample_data):
"""Test deleting a nonexistent entry."""
# Add initial data
with open(temp_csv_file, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
])
writer.writerows(sample_data)
dm = DataManager(temp_csv_file, mock_logger)
result = dm.delete_entry("2024-01-10")
assert result is True # Should return True even if no matching entry
# Verify no data was lost
df = dm.load_data()
assert len(df) == 3
@patch('pandas.read_csv')
def test_load_data_exception_handling(self, mock_read_csv, temp_csv_file, mock_logger):
"""Test exception handling in load_data."""
mock_read_csv.side_effect = Exception("Test error")
dm = DataManager(temp_csv_file, mock_logger)
df = dm.load_data()
assert df.empty
mock_logger.error.assert_called_with("Error loading data: Test error")
@patch('builtins.open')
def test_add_entry_exception_handling(self, mock_open, temp_csv_file, mock_logger):
"""Test exception handling in add_entry."""
mock_open.side_effect = Exception("Test error")
dm = DataManager(temp_csv_file, mock_logger)
entry = ["2024-01-01", 3, 2, 4, 3, 1, 0, 2, 1, "Test note"]
result = dm.add_entry(entry)
assert result is False
mock_logger.error.assert_called_with("Error adding entry: Test error")
+267
View File
@@ -0,0 +1,267 @@
"""
Tests for the GraphManager class.
"""
import os
import pytest
import pandas as pd
import tkinter as tk
from tkinter import ttk
from unittest.mock import Mock, patch, MagicMock
import matplotlib.pyplot as plt
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from graph_manager import GraphManager
class TestGraphManager:
"""Test cases for the GraphManager class."""
@pytest.fixture
def root_window(self):
"""Create a root window for testing."""
root = tk.Tk()
yield root
root.destroy()
@pytest.fixture
def parent_frame(self, root_window):
"""Create a parent frame for testing."""
frame = ttk.LabelFrame(root_window, text="Test Frame")
frame.pack()
return frame
def test_init(self, parent_frame):
"""Test GraphManager initialization."""
gm = GraphManager(parent_frame)
assert gm.parent_frame == parent_frame
assert isinstance(gm.toggle_vars, dict)
assert "depression" in gm.toggle_vars
assert "anxiety" in gm.toggle_vars
assert "sleep" in gm.toggle_vars
assert "appetite" in gm.toggle_vars
# Check that all toggles are initially True
for var in gm.toggle_vars.values():
assert var.get() is True
def test_toggle_controls_creation(self, parent_frame):
"""Test that toggle controls are created properly."""
gm = GraphManager(parent_frame)
# Check that control frame exists
assert hasattr(gm, 'control_frame')
assert isinstance(gm.control_frame, ttk.Frame)
# Check that toggle variables exist
expected_toggles = ["depression", "anxiety", "sleep", "appetite"]
for toggle in expected_toggles:
assert toggle in gm.toggle_vars
assert isinstance(gm.toggle_vars[toggle], tk.BooleanVar)
def test_graph_frame_creation(self, parent_frame):
"""Test that graph frame is created properly."""
gm = GraphManager(parent_frame)
assert hasattr(gm, 'graph_frame')
assert isinstance(gm.graph_frame, ttk.Frame)
@patch('matplotlib.pyplot.subplots')
def test_matplotlib_initialization(self, mock_subplots, parent_frame):
"""Test matplotlib figure and canvas initialization."""
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
assert gm.fig == mock_fig
assert gm.ax == mock_ax
assert gm.canvas == mock_canvas
mock_canvas_class.assert_called_once_with(figure=mock_fig, master=gm.graph_frame)
def test_update_graph_empty_dataframe(self, parent_frame):
"""Test updating graph with empty DataFrame."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg'):
gm = GraphManager(parent_frame)
# Test with empty DataFrame
empty_df = pd.DataFrame()
gm.update_graph(empty_df)
# Verify ax.clear() was called
mock_ax.clear.assert_called()
def test_update_graph_with_data(self, parent_frame, sample_dataframe):
"""Test updating graph with valid data."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
gm.update_graph(sample_dataframe)
# Verify methods were called
mock_ax.clear.assert_called()
mock_canvas.draw.assert_called()
def test_toggle_functionality(self, parent_frame, sample_dataframe):
"""Test that toggle variables affect graph display."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
# Turn off depression toggle
gm.toggle_vars["depression"].set(False)
gm.update_graph(sample_dataframe)
# The graph should still update (specific plotting logic would need more detailed testing)
mock_ax.clear.assert_called()
mock_canvas.draw.assert_called()
def test_close_method(self, parent_frame):
"""Test the close method."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
with patch('matplotlib.pyplot.close') as mock_plt_close:
gm = GraphManager(parent_frame)
gm.close()
mock_plt_close.assert_called_once_with(mock_fig)
def test_date_parsing_in_update_graph(self, parent_frame):
"""Test that date parsing works correctly in update_graph."""
# Create a DataFrame with date strings
df_with_dates = 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],
'hydroxyzine': [0, 1, 0],
'gabapentin': [2, 2, 1],
'propranolol': [1, 0, 1],
'note': ['Test note 1', 'Test note 2', '']
})
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
with patch('pandas.to_datetime') as mock_to_datetime:
gm = GraphManager(parent_frame)
gm.update_graph(df_with_dates)
# Verify pandas.to_datetime was called
mock_to_datetime.assert_called()
@patch('matplotlib.pyplot.subplots')
def test_exception_handling_in_update_graph(self, mock_subplots, parent_frame, sample_dataframe):
"""Test exception handling in update_graph method."""
mock_fig = Mock()
mock_ax = Mock()
mock_ax.plot.side_effect = Exception("Plot error")
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
# This should not raise an exception, but handle it gracefully
try:
gm.update_graph(sample_dataframe)
except Exception as e:
pytest.fail(f"update_graph should handle exceptions gracefully, but raised: {e}")
def test_grid_configuration(self, parent_frame):
"""Test that grid configuration is set up correctly."""
gm = GraphManager(parent_frame)
# The parent frame should have grid configuration
# Note: In a real test, you might need to check grid_info() or similar
# This is a basic structure test
assert hasattr(gm, 'parent_frame')
assert hasattr(gm, 'control_frame')
assert hasattr(gm, 'graph_frame')
def test_canvas_widget_packing(self, parent_frame):
"""Test that canvas widget is properly packed."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas.get_tk_widget.return_value = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
# Verify get_tk_widget was called (for packing)
mock_canvas.get_tk_widget.assert_called()
def test_multiple_toggle_combinations(self, parent_frame, sample_dataframe):
"""Test various combinations of toggle states."""
with patch('matplotlib.pyplot.subplots') as mock_subplots:
mock_fig = Mock()
mock_ax = Mock()
mock_subplots.return_value = (mock_fig, mock_ax)
with patch('graph_manager.FigureCanvasTkAgg') as mock_canvas_class:
mock_canvas = Mock()
mock_canvas_class.return_value = mock_canvas
gm = GraphManager(parent_frame)
# Test all toggles off
for toggle in gm.toggle_vars.values():
toggle.set(False)
gm.update_graph(sample_dataframe)
# Test mixed toggles
gm.toggle_vars["depression"].set(True)
gm.toggle_vars["anxiety"].set(False)
gm.update_graph(sample_dataframe)
# Verify the graph was updated in each case
assert mock_ax.clear.call_count >= 2
assert mock_canvas.draw.call_count >= 2
+258
View File
@@ -0,0 +1,258 @@
"""
Tests for init module.
"""
import os
import tempfile
import pytest
from unittest.mock import patch, Mock
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
class TestInit:
"""Test cases for the init module."""
def test_log_directory_creation(self, temp_log_dir):
"""Test that log directory is created if it doesn't exist."""
with patch('init.LOG_PATH', temp_log_dir + '/new_dir'), \
patch('os.path.exists', return_value=False), \
patch('os.mkdir') as mock_mkdir:
# Re-import to trigger the directory creation logic
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_mkdir.assert_called_once()
def test_log_directory_exists(self, temp_log_dir):
"""Test behavior when log directory already exists."""
with patch('init.LOG_PATH', temp_log_dir), \
patch('os.path.exists', return_value=True), \
patch('os.mkdir') as mock_mkdir:
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_mkdir.assert_not_called()
def test_log_directory_creation_error(self, temp_log_dir):
"""Test handling of errors during log directory creation."""
with patch('init.LOG_PATH', '/invalid/path'), \
patch('os.path.exists', return_value=False), \
patch('os.mkdir', side_effect=PermissionError("Permission denied")), \
patch('builtins.print') as mock_print:
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_print.assert_called()
def test_logger_initialization(self, temp_log_dir):
"""Test that logger is initialized correctly."""
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_LEVEL', 'INFO'), \
patch('init.init_logger') as mock_init_logger:
mock_logger = Mock()
mock_init_logger.return_value = mock_logger
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_init_logger.assert_called_once_with('init', testing_mode=False)
def test_logger_initialization_debug_mode(self, temp_log_dir):
"""Test logger initialization in debug mode."""
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_LEVEL', 'DEBUG'), \
patch('init.init_logger') as mock_init_logger:
mock_logger = Mock()
mock_init_logger.return_value = mock_logger
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_init_logger.assert_called_once_with('init', testing_mode=True)
def test_log_files_definition(self, temp_log_dir):
"""Test that log files tuple is defined correctly."""
with patch('init.LOG_PATH', temp_log_dir):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
expected_files = (
f"{temp_log_dir}/thechart.log",
f"{temp_log_dir}/thechart.warning.log",
f"{temp_log_dir}/thechart.error.log",
)
assert init.log_files == expected_files
def test_testing_mode_detection(self, temp_log_dir):
"""Test that testing mode is detected correctly."""
with patch('init.LOG_PATH', temp_log_dir):
# Test with DEBUG level
with patch('init.LOG_LEVEL', 'DEBUG'):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
assert init.testing_mode is True
# Test with non-DEBUG level
with patch('init.LOG_LEVEL', 'INFO'):
importlib.reload(sys.modules['init'])
assert init.testing_mode is False
def test_log_clear_true(self, temp_log_dir):
"""Test log file clearing when LOG_CLEAR is True."""
# Create some test log files
log_files = [
os.path.join(temp_log_dir, "thechart.log"),
os.path.join(temp_log_dir, "thechart.warning.log"),
os.path.join(temp_log_dir, "thechart.error.log"),
]
for log_file in log_files:
with open(log_file, 'w') as f:
f.write("Old log content")
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_CLEAR', 'True'), \
patch('init.log_files', log_files):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
# Check that files were truncated
for log_file in log_files:
with open(log_file, 'r') as f:
assert f.read() == ""
def test_log_clear_false(self, temp_log_dir):
"""Test that log files are not cleared when LOG_CLEAR is False."""
# Create some test log files
log_files = [
os.path.join(temp_log_dir, "thechart.log"),
os.path.join(temp_log_dir, "thechart.warning.log"),
os.path.join(temp_log_dir, "thechart.error.log"),
]
original_content = "Original log content"
for log_file in log_files:
with open(log_file, 'w') as f:
f.write(original_content)
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_CLEAR', 'False'), \
patch('init.log_files', log_files):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
# Check that files were not truncated
for log_file in log_files:
with open(log_file, 'r') as f:
assert f.read() == original_content
def test_log_clear_nonexistent_files(self, temp_log_dir):
"""Test log clearing when some log files don't exist."""
log_files = [
os.path.join(temp_log_dir, "thechart.log"),
os.path.join(temp_log_dir, "nonexistent.log"),
]
# Create only one of the files
with open(log_files[0], 'w') as f:
f.write("Content")
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_CLEAR', 'True'), \
patch('init.log_files', log_files):
# This should not raise an exception
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
def test_log_clear_permission_error(self, temp_log_dir):
"""Test handling of permission errors during log clearing."""
log_files = [os.path.join(temp_log_dir, "thechart.log")]
with open(log_files[0], 'w') as f:
f.write("Content")
with patch('init.LOG_PATH', temp_log_dir), \
patch('init.LOG_CLEAR', 'True'), \
patch('init.log_files', log_files), \
patch('builtins.open', side_effect=PermissionError("Permission denied")), \
patch('init.logger') as mock_logger:
mock_logger.error = Mock()
# Should raise the exception after logging
with pytest.raises(PermissionError):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
def test_module_exports(self, temp_log_dir):
"""Test that module exports expected objects."""
with patch('init.LOG_PATH', temp_log_dir):
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
# Check that expected objects are available
assert hasattr(init, 'logger')
assert hasattr(init, 'log_files')
assert hasattr(init, 'testing_mode')
def test_log_path_printing(self, temp_log_dir):
"""Test that LOG_PATH is printed when directory is created."""
with patch('init.LOG_PATH', temp_log_dir + '/new_dir'), \
patch('os.path.exists', return_value=False), \
patch('os.mkdir'), \
patch('builtins.print') as mock_print:
import importlib
if 'init' in sys.modules:
importlib.reload(sys.modules['init'])
else:
import init
mock_print.assert_called_with(temp_log_dir + '/new_dir')
+180
View File
@@ -0,0 +1,180 @@
"""
Tests for logger module.
"""
import os
import logging
import tempfile
import pytest
from unittest.mock import patch, Mock
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from logger import init_logger
class TestLogger:
"""Test cases for the logger module."""
def test_init_logger_basic(self, temp_log_dir):
"""Test basic logger initialization."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
assert isinstance(logger, logging.Logger)
assert logger.name == "test_logger"
assert logger.level == logging.INFO
def test_init_logger_testing_mode(self, temp_log_dir):
"""Test logger initialization in testing mode."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=True)
assert logger.level == logging.DEBUG
def test_init_logger_production_mode(self, temp_log_dir):
"""Test logger initialization in production mode."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
assert logger.level == logging.INFO
def test_file_handlers_created(self, temp_log_dir):
"""Test that file handlers are created correctly."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
# Check that handlers were added
assert len(logger.handlers) >= 3 # At least 3 file handlers
def test_file_handler_levels(self, temp_log_dir):
"""Test that file handlers have correct log levels."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
handler_levels = [handler.level for handler in logger.handlers if isinstance(handler, logging.FileHandler)]
# Should have handlers for DEBUG, WARNING, and ERROR levels
assert logging.DEBUG in handler_levels
assert logging.WARNING in handler_levels
assert logging.ERROR in handler_levels
def test_log_file_paths(self, temp_log_dir):
"""Test that log files are created with correct paths."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
# Log something to trigger file creation
logger.debug("Test debug message")
logger.warning("Test warning message")
logger.error("Test error message")
# Check that log files would be created (paths are correct)
expected_files = [
os.path.join(temp_log_dir, "app.log"),
os.path.join(temp_log_dir, "app.warning.log"),
os.path.join(temp_log_dir, "app.error.log")
]
# The files should exist or be ready to be created
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
assert handler.baseFilename in expected_files
def test_formatter_format(self, temp_log_dir):
"""Test that formatters are set correctly."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
expected_format = "%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s"
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
assert handler.formatter._fmt == expected_format
@patch('colorlog.basicConfig')
def test_colorlog_configuration(self, mock_basicConfig, temp_log_dir):
"""Test that colorlog is configured correctly."""
with patch('logger.LOG_PATH', temp_log_dir):
init_logger("test_logger", testing_mode=False)
mock_basicConfig.assert_called_once()
# Check that format includes color and bold formatting
call_args = mock_basicConfig.call_args
assert 'format' in call_args[1]
format_string = call_args[1]['format']
assert '%(log_color)s' in format_string
assert '\033[1m' in format_string # Bold sequence
def test_multiple_logger_instances(self, temp_log_dir):
"""Test creating multiple logger instances."""
with patch('logger.LOG_PATH', temp_log_dir):
logger1 = init_logger("logger1", testing_mode=False)
logger2 = init_logger("logger2", testing_mode=True)
assert logger1.name == "logger1"
assert logger2.name == "logger2"
assert logger1.level == logging.INFO
assert logger2.level == logging.DEBUG
def test_logger_inheritance(self, temp_log_dir):
"""Test that logger follows Python logging hierarchy."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test.module.logger", testing_mode=False)
assert logger.name == "test.module.logger"
@patch('logging.FileHandler')
def test_file_handler_error_handling(self, mock_file_handler, temp_log_dir):
"""Test error handling when file handler creation fails."""
mock_file_handler.side_effect = PermissionError("Cannot create log file")
with patch('logger.LOG_PATH', temp_log_dir):
# Should not raise an exception, but handle gracefully
try:
logger = init_logger("test_logger", testing_mode=False)
# Logger should still be created, just without file handlers
assert isinstance(logger, logging.Logger)
except PermissionError:
pytest.fail("init_logger should handle file creation errors gracefully")
def test_logger_name_parameter(self, temp_log_dir):
"""Test that logger name is set correctly from parameter."""
with patch('logger.LOG_PATH', temp_log_dir):
test_name = "my.custom.logger.name"
logger = init_logger(test_name, testing_mode=False)
assert logger.name == test_name
def test_testing_mode_boolean(self, temp_log_dir):
"""Test that testing_mode parameter accepts boolean values."""
with patch('logger.LOG_PATH', temp_log_dir):
logger_true = init_logger("test1", testing_mode=True)
logger_false = init_logger("test2", testing_mode=False)
assert logger_true.level == logging.DEBUG
assert logger_false.level == logging.INFO
def test_log_format_contains_required_fields(self, temp_log_dir):
"""Test that log format contains all required fields."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
log_format = "%(asctime)s - %(name)s - %(funcName)s - %(levelname)s - %(message)s"
# Check that format contains all expected fields
expected_fields = ['%(asctime)s', '%(name)s', '%(funcName)s', '%(levelname)s', '%(message)s']
for field in expected_fields:
assert field in log_format
def test_handler_file_mode(self, temp_log_dir):
"""Test that file handlers use append mode by default."""
with patch('logger.LOG_PATH', temp_log_dir):
logger = init_logger("test_logger", testing_mode=False)
# File handlers should be in append mode by default
for handler in logger.handlers:
if isinstance(handler, logging.FileHandler):
# FileHandler uses 'a' mode by default
assert hasattr(handler, 'mode') # Basic check that it's a file handler
+411
View File
@@ -0,0 +1,411 @@
"""
Tests for the main application and MedTrackerApp class.
"""
import os
import pytest
import tkinter as tk
from unittest.mock import Mock, patch, MagicMock
import pandas as pd
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from main import MedTrackerApp
class TestMedTrackerApp:
"""Test cases for the MedTrackerApp class."""
@pytest.fixture
def root_window(self):
"""Create a root window for testing."""
root = tk.Tk()
yield root
root.destroy()
@pytest.fixture
def mock_managers(self):
"""Mock the manager classes."""
with patch('main.UIManager') as mock_ui, \
patch('main.DataManager') as mock_data, \
patch('main.GraphManager') as mock_graph:
yield {
'ui': mock_ui,
'data': mock_data,
'graph': mock_graph
}
def test_init_default_filename(self, root_window, mock_managers):
"""Test initialization with default filename."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
assert app.filename == "thechart_data.csv"
assert app.root == root_window
assert root_window.title() == "Thechart - medication tracker"
def test_init_custom_filename_exists(self, root_window, mock_managers):
"""Test initialization with custom filename that exists."""
with patch('sys.argv', ['main.py', 'custom_data.csv']), \
patch('os.path.exists', return_value=True):
app = MedTrackerApp(root_window)
assert app.filename == "custom_data.csv"
def test_init_custom_filename_not_exists(self, root_window, mock_managers):
"""Test initialization with custom filename that doesn't exist."""
with patch('sys.argv', ['main.py', 'nonexistent.csv']), \
patch('os.path.exists', return_value=False):
app = MedTrackerApp(root_window)
assert app.filename == "thechart_data.csv"
@patch('main.LOG_LEVEL', 'DEBUG')
def test_debug_logging(self, root_window, mock_managers):
"""Test debug logging when LOG_LEVEL is DEBUG."""
with patch('sys.argv', ['main.py', 'test.csv']), \
patch('os.path.exists', return_value=True), \
patch('main.logger') as mock_logger:
app = MedTrackerApp(root_window)
# Check that debug messages were logged
mock_logger.debug.assert_called()
def test_setup_main_ui_components(self, root_window, mock_managers):
"""Test that main UI components are set up correctly."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Check that managers were instantiated
mock_managers['ui'].assert_called()
mock_managers['data'].assert_called()
def test_icon_setup(self, root_window, mock_managers):
"""Test icon setup functionality."""
with patch('sys.argv', ['main.py']), \
patch('os.path.exists', return_value=True):
app = MedTrackerApp(root_window)
# Check that setup_icon was called on UI manager
app.ui_manager.setup_icon.assert_called()
def test_icon_setup_fallback_path(self, root_window, mock_managers):
"""Test icon setup with fallback path."""
def mock_exists(path):
return path == "./chart-671.png"
with patch('sys.argv', ['main.py']), \
patch('os.path.exists', side_effect=mock_exists):
app = MedTrackerApp(root_window)
# Check that setup_icon was called with fallback path
app.ui_manager.setup_icon.assert_called_with(img_path="./chart-671.png")
def test_add_entry_success(self, root_window, mock_managers):
"""Test successful entry addition."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Mock the UI variables
app.date_var = Mock()
app.date_var.get.return_value = "2024-01-01"
app.symptom_vars = {
"depression": Mock(), "anxiety": Mock(),
"sleep": Mock(), "appetite": Mock()
}
for var in app.symptom_vars.values():
var.get.return_value = 3
app.medicine_vars = {
"bupropion": [Mock()], "hydroxyzine": [Mock()],
"gabapentin": [Mock()], "propranolol": [Mock()]
}
for med_var in app.medicine_vars.values():
med_var[0].get.return_value = 1
app.note_var = Mock()
app.note_var.get.return_value = "Test note"
# Mock data manager to return success
app.data_manager.add_entry.return_value = True
with patch('tkinter.messagebox.showinfo') as mock_info, \
patch.object(app, '_clear_entries') as mock_clear, \
patch.object(app, 'load_data') as mock_load:
app.add_entry()
mock_info.assert_called_once()
mock_clear.assert_called_once()
mock_load.assert_called_once()
def test_add_entry_empty_date(self, root_window, mock_managers):
"""Test adding entry with empty date."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
app.date_var = Mock()
app.date_var.get.return_value = " " # Empty/whitespace date
with patch('tkinter.messagebox.showerror') as mock_error:
app.add_entry()
mock_error.assert_called_once_with(
"Error", "Please enter a date.", parent=app.root
)
def test_add_entry_duplicate_date(self, root_window, mock_managers):
"""Test adding entry with duplicate date."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Set up UI variables
app.date_var = Mock()
app.date_var.get.return_value = "2024-01-01"
app.symptom_vars = {"depression": Mock(), "anxiety": Mock(),
"sleep": Mock(), "appetite": Mock()}
for var in app.symptom_vars.values():
var.get.return_value = 3
app.medicine_vars = {"bupropion": [Mock()], "hydroxyzine": [Mock()],
"gabapentin": [Mock()], "propranolol": [Mock()]}
for med_var in app.medicine_vars.values():
med_var[0].get.return_value = 1
app.note_var = Mock()
app.note_var.get.return_value = "Test"
# Mock data manager to return failure (duplicate)
app.data_manager.add_entry.return_value = False
# Mock load_data to return DataFrame with existing date
mock_df = pd.DataFrame({'date': ['2024-01-01']})
app.data_manager.load_data.return_value = mock_df
with patch('tkinter.messagebox.showerror') as mock_error:
app.add_entry()
mock_error.assert_called_once()
assert "already exists" in mock_error.call_args[0][1]
def test_on_double_click(self, root_window, mock_managers):
"""Test double-click event handling."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Mock tree with selection
app.tree = Mock()
app.tree.get_children.return_value = ['item1']
app.tree.selection.return_value = ['item1']
app.tree.item.return_value = {'values': ('2024-01-01', '3', '2', '4', '3', '1', '0', '2', '1', 'Note')}
mock_event = Mock()
with patch.object(app, '_create_edit_window') as mock_create_edit:
app.on_double_click(mock_event)
mock_create_edit.assert_called_once()
def test_on_double_click_empty_tree(self, root_window, mock_managers):
"""Test double-click when tree is empty."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
app.tree = Mock()
app.tree.get_children.return_value = []
mock_event = Mock()
with patch.object(app, '_create_edit_window') as mock_create_edit:
app.on_double_click(mock_event)
mock_create_edit.assert_not_called()
def test_save_edit_success(self, root_window, mock_managers):
"""Test successful save edit operation."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Mock edit window
mock_edit_win = Mock()
# Mock data manager to return success
app.data_manager.update_entry.return_value = True
with patch('tkinter.messagebox.showinfo') as mock_info, \
patch.object(app, '_clear_entries') as mock_clear, \
patch.object(app, 'load_data') as mock_load:
app._save_edit(
mock_edit_win, "2024-01-01", "2024-01-01",
3, 2, 4, 3, 1, 0, 2, 1, "Updated note"
)
mock_edit_win.destroy.assert_called_once()
mock_info.assert_called_once()
mock_clear.assert_called_once()
mock_load.assert_called_once()
def test_save_edit_duplicate_date(self, root_window, mock_managers):
"""Test save edit with duplicate date."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
mock_edit_win = Mock()
# Mock data manager to return failure
app.data_manager.update_entry.return_value = False
# Mock load_data to return DataFrame with existing date
mock_df = pd.DataFrame({'date': ['2024-01-02']})
app.data_manager.load_data.return_value = mock_df
with patch('tkinter.messagebox.showerror') as mock_error:
app._save_edit(
mock_edit_win, "2024-01-01", "2024-01-02", # Different dates
3, 2, 4, 3, 1, 0, 2, 1, "Updated note"
)
mock_error.assert_called_once()
assert "already exists" in mock_error.call_args[0][1]
def test_delete_entry_success(self, root_window, mock_managers):
"""Test successful entry deletion."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
mock_edit_win = Mock()
app.tree = Mock()
app.tree.item.return_value = {'values': ['2024-01-01']}
# Mock data manager to return success
app.data_manager.delete_entry.return_value = True
with patch('tkinter.messagebox.askyesno', return_value=True) as mock_confirm, \
patch('tkinter.messagebox.showinfo') as mock_info, \
patch.object(app, 'load_data') as mock_load:
app._delete_entry(mock_edit_win, 'item1')
mock_confirm.assert_called_once()
mock_edit_win.destroy.assert_called_once()
mock_info.assert_called_once()
mock_load.assert_called_once()
def test_delete_entry_cancelled(self, root_window, mock_managers):
"""Test deletion when user cancels."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
mock_edit_win = Mock()
with patch('tkinter.messagebox.askyesno', return_value=False) as mock_confirm:
app._delete_entry(mock_edit_win, 'item1')
mock_confirm.assert_called_once()
mock_edit_win.destroy.assert_not_called()
def test_clear_entries(self, root_window, mock_managers):
"""Test clearing input entries."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Mock variables
app.date_var = Mock()
app.symptom_vars = {"depression": Mock(), "anxiety": Mock()}
app.medicine_vars = {"bupropion": [Mock()], "hydroxyzine": [Mock()]}
app.note_var = Mock()
app._clear_entries()
app.date_var.set.assert_called_with("")
app.note_var.set.assert_called_with("")
for var in app.symptom_vars.values():
var.set.assert_called_with(0)
for med_var in app.medicine_vars.values():
med_var[0].set.assert_called_with(0)
def test_load_data(self, root_window, mock_managers):
"""Test loading data into tree and graph."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# Mock tree
app.tree = Mock()
app.tree.get_children.return_value = ['item1', 'item2']
# Mock data
mock_df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-02'],
'depression': [3, 2],
'note': ['Note1', 'Note2']
})
app.data_manager.load_data.return_value = mock_df
app.load_data()
# Check that tree was cleared and populated
app.tree.delete.assert_called()
app.tree.insert.assert_called()
# Check that graph was updated
app.graph_manager.update_graph.assert_called_with(mock_df)
def test_load_data_empty_dataframe(self, root_window, mock_managers):
"""Test loading empty data."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
app.tree = Mock()
app.tree.get_children.return_value = []
# Mock empty DataFrame
empty_df = pd.DataFrame()
app.data_manager.load_data.return_value = empty_df
app.load_data()
# Graph should still be updated even with empty data
app.graph_manager.update_graph.assert_called_with(empty_df)
def test_on_closing_confirmed(self, root_window, mock_managers):
"""Test application closing when confirmed."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
with patch('tkinter.messagebox.askokcancel', return_value=True) as mock_confirm:
app.on_closing()
mock_confirm.assert_called_once()
app.graph_manager.close.assert_called_once()
def test_on_closing_cancelled(self, root_window, mock_managers):
"""Test application closing when cancelled."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
with patch('tkinter.messagebox.askokcancel', return_value=False) as mock_confirm:
app.on_closing()
mock_confirm.assert_called_once()
app.graph_manager.close.assert_not_called()
def test_protocol_handler_setup(self, root_window, mock_managers):
"""Test that window close protocol is set up."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
# The protocol should be set during initialization
# This is more of a structural test
assert app.root is root_window
def test_window_properties(self, root_window, mock_managers):
"""Test window properties are set correctly."""
with patch('sys.argv', ['main.py']):
app = MedTrackerApp(root_window)
assert root_window.title() == "Thechart - medication tracker"
# Note: Testing resizable would require more complex mocking
+307
View File
@@ -0,0 +1,307 @@
"""
Tests for the UIManager class.
"""
import os
import pytest
import tkinter as tk
from tkinter import ttk
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from ui_manager import UIManager
class TestUIManager:
"""Test cases for the UIManager class."""
@pytest.fixture
def root_window(self):
"""Create a root window for testing."""
root = tk.Tk()
yield root
root.destroy()
@pytest.fixture
def ui_manager(self, root_window, mock_logger):
"""Create a UIManager instance for testing."""
return UIManager(root_window, mock_logger)
def test_init(self, root_window, mock_logger):
"""Test UIManager initialization."""
ui = UIManager(root_window, mock_logger)
assert ui.root == root_window
assert ui.logger == mock_logger
@patch('os.path.exists')
@patch('PIL.Image.open')
def test_setup_icon_success(self, mock_image_open, mock_exists, ui_manager):
"""Test successful icon setup."""
mock_exists.return_value = True
mock_image = Mock()
mock_image.resize.return_value = mock_image
mock_image_open.return_value = mock_image
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
mock_photo_instance = Mock()
mock_photo.return_value = mock_photo_instance
result = ui_manager.setup_icon("test_icon.png")
assert result is True
mock_image_open.assert_called_once_with("test_icon.png")
mock_image.resize.assert_called_once_with(size=(32, 32), resample=Mock())
ui_manager.logger.info.assert_called_with("Trying to load icon from: test_icon.png")
@patch('os.path.exists')
def test_setup_icon_file_not_found(self, mock_exists, ui_manager):
"""Test icon setup when file is not found."""
mock_exists.return_value = False
result = ui_manager.setup_icon("nonexistent_icon.png")
assert result is False
ui_manager.logger.warning.assert_called_with("Icon file not found at nonexistent_icon.png")
@patch('os.path.exists')
@patch('PIL.Image.open')
def test_setup_icon_exception(self, mock_image_open, mock_exists, ui_manager):
"""Test icon setup with exception."""
mock_exists.return_value = True
mock_image_open.side_effect = Exception("Test error")
result = ui_manager.setup_icon("test_icon.png")
assert result is False
ui_manager.logger.error.assert_called_with("Error setting up icon: Test error")
@patch('sys._MEIPASS', '/test/bundle/path', create=True)
@patch('os.path.exists')
@patch('PIL.Image.open')
def test_setup_icon_pyinstaller_bundle(self, mock_image_open, mock_exists, ui_manager):
"""Test icon setup in PyInstaller bundle."""
# Mock exists to return False for original path, True for bundle path
def mock_exists_side_effect(path):
if 'test_icon.png' in path and '/test/bundle/path' in path:
return True
return False
mock_exists.side_effect = mock_exists_side_effect
mock_image = Mock()
mock_image.resize.return_value = mock_image
mock_image_open.return_value = mock_image
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
mock_photo_instance = Mock()
mock_photo.return_value = mock_photo_instance
result = ui_manager.setup_icon("test_icon.png")
assert result is True
ui_manager.logger.info.assert_called_with("Found icon in PyInstaller bundle: /test/bundle/path/test_icon.png")
def test_create_graph_frame(self, ui_manager, root_window):
"""Test creation of graph frame."""
main_frame = ttk.Frame(root_window)
graph_frame = ui_manager.create_graph_frame(main_frame)
assert isinstance(graph_frame, ttk.LabelFrame)
assert graph_frame.winfo_parent() == str(main_frame)
def test_create_input_frame(self, ui_manager, root_window):
"""Test creation of input frame."""
main_frame = ttk.Frame(root_window)
input_ui = ui_manager.create_input_frame(main_frame)
assert isinstance(input_ui, dict)
assert "frame" in input_ui
assert "symptom_vars" in input_ui
assert "medicine_vars" in input_ui
assert "note_var" in input_ui
assert "date_var" in input_ui
assert isinstance(input_ui["frame"], ttk.LabelFrame)
assert isinstance(input_ui["symptom_vars"], dict)
assert isinstance(input_ui["medicine_vars"], dict)
assert isinstance(input_ui["note_var"], tk.StringVar)
assert isinstance(input_ui["date_var"], tk.StringVar)
def test_create_input_frame_symptom_vars(self, ui_manager, root_window):
"""Test that symptom variables are created correctly."""
main_frame = ttk.Frame(root_window)
input_ui = ui_manager.create_input_frame(main_frame)
symptom_vars = input_ui["symptom_vars"]
expected_symptoms = ["depression", "anxiety", "sleep", "appetite"]
for symptom in expected_symptoms:
assert symptom in symptom_vars
assert isinstance(symptom_vars[symptom], tk.IntVar)
def test_create_input_frame_medicine_vars(self, ui_manager, root_window):
"""Test that medicine variables are created correctly."""
main_frame = ttk.Frame(root_window)
input_ui = ui_manager.create_input_frame(main_frame)
medicine_vars = input_ui["medicine_vars"]
expected_medicines = ["bupropion", "hydroxyzine", "gabapentin", "propranolol"]
for medicine in expected_medicines:
assert medicine in medicine_vars
assert isinstance(medicine_vars[medicine], list)
assert len(medicine_vars[medicine]) == 2 # IntVar and Spinbox
assert isinstance(medicine_vars[medicine][0], tk.IntVar)
assert isinstance(medicine_vars[medicine][1], ttk.Spinbox)
@patch('ui_manager.datetime')
def test_create_input_frame_default_date(self, mock_datetime, ui_manager, root_window):
"""Test that default date is set to today."""
mock_datetime.now.return_value.strftime.return_value = "2024-01-15"
main_frame = ttk.Frame(root_window)
input_ui = ui_manager.create_input_frame(main_frame)
assert input_ui["date_var"].get() == "2024-01-15"
def test_create_table_frame(self, ui_manager, root_window):
"""Test creation of table frame."""
main_frame = ttk.Frame(root_window)
table_ui = ui_manager.create_table_frame(main_frame)
assert isinstance(table_ui, dict)
assert "tree" in table_ui
assert isinstance(table_ui["tree"], ttk.Treeview)
def test_create_table_frame_columns(self, ui_manager, root_window):
"""Test that table columns are set up correctly."""
main_frame = ttk.Frame(root_window)
table_ui = ui_manager.create_table_frame(main_frame)
tree = table_ui["tree"]
expected_columns = [
"date", "depression", "anxiety", "sleep", "appetite",
"bupropion", "hydroxyzine", "gabapentin", "propranolol", "note"
]
# Check that columns are configured
assert tree["columns"] == tuple(expected_columns)
def test_add_buttons(self, ui_manager, root_window):
"""Test adding buttons to a frame."""
frame = ttk.Frame(root_window)
buttons_config = [
{"text": "Test Button 1", "command": lambda: None},
{"text": "Test Button 2", "command": lambda: None, "fill": "x"},
]
ui_manager.add_buttons(frame, buttons_config)
# Check that buttons were added (basic structure test)
children = frame.winfo_children()
assert len(children) >= 2
def test_create_edit_window(self, ui_manager):
"""Test creation of edit window."""
values = ("2024-01-01", "3", "2", "4", "3", "1", "0", "2", "1", "Test note")
callbacks = {
"save": lambda win, *args: None,
"delete": lambda win: None
}
edit_window = ui_manager.create_edit_window(values, callbacks)
assert isinstance(edit_window, tk.Toplevel)
assert edit_window.title() == "Edit Entry"
def test_create_edit_window_widgets(self, ui_manager):
"""Test that edit window contains expected widgets."""
values = ("2024-01-01", "3", "2", "4", "3", "1", "0", "2", "1", "Test note")
callbacks = {
"save": lambda win, *args: None,
"delete": lambda win: None
}
edit_window = ui_manager.create_edit_window(values, callbacks)
# Check that window has children (widgets)
children = edit_window.winfo_children()
assert len(children) > 0
def test_create_edit_window_initial_values(self, ui_manager):
"""Test that edit window is populated with initial values."""
values = ("2024-01-01", "3", "2", "4", "3", "1", "0", "2", "1", "Test note")
callbacks = {
"save": lambda win, *args: None,
"delete": lambda win: None
}
edit_window = ui_manager.create_edit_window(values, callbacks)
# The window should be created successfully
assert edit_window is not None
# More detailed testing would require examining the internal widgets
def test_create_scale_with_var(self, ui_manager, root_window):
"""Test creation of scale widget with variable."""
frame = ttk.Frame(root_window)
var = tk.IntVar()
scale = ui_manager._create_scale_with_var(frame, var, "Test Label", 0, 0)
assert isinstance(scale, ttk.Scale)
def test_create_spinbox_with_var(self, ui_manager, root_window):
"""Test creation of spinbox widget with variable."""
frame = ttk.Frame(root_window)
var = tk.IntVar()
result = ui_manager._create_spinbox_with_var(frame, var, "Test Label", 0, 0)
assert isinstance(result, list)
assert len(result) == 2
assert isinstance(result[0], tk.IntVar)
assert isinstance(result[1], ttk.Spinbox)
def test_frame_positioning(self, ui_manager, root_window):
"""Test that frames are positioned correctly."""
main_frame = ttk.Frame(root_window)
# Create multiple frames
graph_frame = ui_manager.create_graph_frame(main_frame)
input_ui = ui_manager.create_input_frame(main_frame)
table_ui = ui_manager.create_table_frame(main_frame)
# All frames should be created successfully
assert graph_frame is not None
assert input_ui["frame"] is not None
assert table_ui["tree"] is not None
def test_widget_configuration(self, ui_manager, root_window):
"""Test that widgets are configured with appropriate properties."""
main_frame = ttk.Frame(root_window)
input_ui = ui_manager.create_input_frame(main_frame)
# Check that variables have default values
for var in input_ui["symptom_vars"].values():
assert var.get() == 0
for medicine_data in input_ui["medicine_vars"].values():
assert medicine_data[0].get() == 0
@patch('tkinter.messagebox.showerror')
def test_error_handling_in_setup_icon(self, mock_showerror, ui_manager):
"""Test error handling in setup_icon method."""
with patch('PIL.Image.open') as mock_open:
mock_open.side_effect = Exception("Image error")
result = ui_manager.setup_icon("test.png")
assert result is False
ui_manager.logger.error.assert_called()