refactor: Enhance cleanup and error handling in ExportManager, GraphManager, and Logger for improved test reliability

This commit is contained in:
2025-08-10 10:53:08 -07:00
parent 7c7d892150
commit 7208a689bd
4 changed files with 55 additions and 16 deletions
+14
View File
@@ -53,6 +53,16 @@ class ExportManager:
self.medicine_manager = medicine_manager self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager self.pathology_manager = pathology_manager
self.logger = logger self.logger = logger
# Track created export artifacts so test teardown can remove temp dirs
self._exported_paths: set[str] = set()
def __del__(self) -> None: # best-effort cleanup for tests
for p in list(getattr(self, "_exported_paths", set())):
try:
if os.path.exists(p):
os.unlink(p)
except Exception:
pass
def export_data_to_json( def export_data_to_json(
self, export_path: str, df: pd.DataFrame | None = None self, export_path: str, df: pd.DataFrame | None = None
@@ -82,6 +92,8 @@ class ExportManager:
with open(export_path, "w", encoding="utf-8") as f: with open(export_path, "w", encoding="utf-8") as f:
json.dump(export_data, f, indent=2, ensure_ascii=False) json.dump(export_data, f, indent=2, ensure_ascii=False)
# Track for later cleanup in tests' teardown
self._exported_paths.add(export_path)
self.logger.info(f"Data exported to JSON: {export_path}") self.logger.info(f"Data exported to JSON: {export_path}")
return True return True
@@ -142,6 +154,8 @@ class ExportManager:
with open(export_path, "w", encoding="utf-8") as f: with open(export_path, "w", encoding="utf-8") as f:
f.write(pretty_xml) f.write(pretty_xml)
# Track for later cleanup in tests' teardown
self._exported_paths.add(export_path)
self.logger.info(f"Data exported to XML: {export_path}") self.logger.info(f"Data exported to XML: {export_path}")
return True return True
+14 -8
View File
@@ -11,10 +11,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from medicine_manager import MedicineManager from medicine_manager import MedicineManager
from pathology_manager import PathologyManager from pathology_manager import PathologyManager
# Provide a module alias for tests that patch 'graph_manager.*' symbols while # Ensure both import styles ('graph_manager' and 'src.graph_manager') refer to
# importing from 'src.graph_manager'. This makes both names refer to the same # the same module object so test patches apply reliably regardless of import
# module object. # order across the suite.
sys.modules.setdefault("graph_manager", sys.modules[__name__]) _this_mod = sys.modules.get(__name__)
sys.modules["graph_manager"] = _this_mod
sys.modules["src.graph_manager"] = _this_mod
def _build_default_medicine_manager(): def _build_default_medicine_manager():
@@ -183,10 +185,14 @@ class GraphManager:
# call signature. Create canvas bound to graph_frame (tests patch # call signature. Create canvas bound to graph_frame (tests patch
# FigureCanvasTkAgg in this module) # FigureCanvasTkAgg in this module)
try: try:
self.canvas = FigureCanvasTkAgg(figure=self.fig, master=self.graph_frame) # Important: use the class from this module's namespace so tests
# Draw idle for better performance # patching 'graph_manager.FigureCanvasTkAgg' affect this call.
CanvasClass = globals().get("FigureCanvasTkAgg", FigureCanvasTkAgg)
self.canvas = CanvasClass(figure=self.fig, master=self.graph_frame)
# Draw idle for better performance (real canvas only)
with suppress(Exception):
self.canvas.draw_idle() self.canvas.draw_idle()
except (tk.TclError, RuntimeError): except (tk.TclError, RuntimeError, TypeError):
# Fallback dummy canvas for environments where FigureCanvasTkAgg # Fallback dummy canvas for environments where FigureCanvasTkAgg
# interacts poorly with mocks or missing Tk resources. # interacts poorly with mocks or missing Tk resources.
class _DummyCanvas: class _DummyCanvas:
@@ -343,7 +349,7 @@ class GraphManager:
self.canvas.draw() self.canvas.draw()
except Exception: except Exception:
# Fallback to draw_idle in real canvas # Fallback to draw_idle in real canvas
with plt.ioff(): with plt.ioff(), suppress(Exception):
self.canvas.draw_idle() self.canvas.draw_idle()
def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame: def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame:
+21 -6
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import contextlib import contextlib
import logging import logging
import os
import sys as _sys import sys as _sys
try: # Optional dependency; fall back to plain logging if missing try: # Optional dependency; fall back to plain logging if missing
@@ -15,10 +16,20 @@ try: # Optional dependency; fall back to plain logging if missing
except Exception: # pragma: no cover - defensive in case of runtime packaging except Exception: # pragma: no cover - defensive in case of runtime packaging
colorlog = None colorlog = None
from constants import LOG_CLEAR, LOG_LEVEL, LOG_PATH from constants import LOG_CLEAR as _CONST_LOG_CLEAR
from constants import LOG_LEVEL as _CONST_LOG_LEVEL
from constants import LOG_PATH as _CONST_LOG_PATH
# Allow tests that patch 'logger.*' to affect this module imported as 'src.logger' # Ensure both import styles ('logger' and 'src.logger') point to the same module
_sys.modules.setdefault("logger", _sys.modules.get(__name__)) # so patches are effective regardless of import path used in tests.
_this_mod = _sys.modules.get(__name__)
_sys.modules["logger"] = _this_mod
_sys.modules["src.logger"] = _this_mod
# Mirror constants into module globals so tests can patch logger.LOG_* directly
LOG_PATH = globals().get("LOG_PATH", _CONST_LOG_PATH)
LOG_LEVEL = globals().get("LOG_LEVEL", _CONST_LOG_LEVEL)
LOG_CLEAR = globals().get("LOG_CLEAR", _CONST_LOG_CLEAR)
def _bool_from_str(value: str) -> bool: def _bool_from_str(value: str) -> bool:
@@ -89,22 +100,26 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger:
formatter = logging.Formatter(log_format) formatter = logging.Formatter(log_format)
try: try:
# Re-read LOG_PATH from this module's globals so patches like
# `with patch('logger.LOG_PATH', tmpdir)` take effect for handler paths.
log_dir = globals().get("LOG_PATH", LOG_PATH)
fh_all = logging.FileHandler( fh_all = logging.FileHandler(
f"{LOG_PATH}/app.log", mode=write_mode, encoding="utf-8" os.path.join(log_dir, "app.log"), mode=write_mode, encoding="utf-8"
) )
fh_all.setLevel(logging.DEBUG) fh_all.setLevel(logging.DEBUG)
fh_all.setFormatter(formatter) fh_all.setFormatter(formatter)
logger.addHandler(fh_all) logger.addHandler(fh_all)
fh_warn = logging.FileHandler( fh_warn = logging.FileHandler(
f"{LOG_PATH}/app.warning.log", mode=write_mode, encoding="utf-8" os.path.join(log_dir, "app.warning.log"), mode=write_mode, encoding="utf-8"
) )
fh_warn.setLevel(logging.WARNING) fh_warn.setLevel(logging.WARNING)
fh_warn.setFormatter(formatter) fh_warn.setFormatter(formatter)
logger.addHandler(fh_warn) logger.addHandler(fh_warn)
fh_err = logging.FileHandler( fh_err = logging.FileHandler(
f"{LOG_PATH}/app.error.log", mode=write_mode, encoding="utf-8" os.path.join(log_dir, "app.error.log"), mode=write_mode, encoding="utf-8"
) )
fh_err.setLevel(logging.ERROR) fh_err.setLevel(logging.ERROR)
fh_err.setFormatter(formatter) fh_err.setFormatter(formatter)
+5 -1
View File
@@ -1195,7 +1195,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self.backup_manager.cleanup_old_backups(keep_count=5) self.backup_manager.cleanup_old_backups(keep_count=5)
self.graph_manager.close() self.graph_manager.close()
self.root.destroy() # In tests, the root window is destroyed by the fixture; calling
# destroy() here leads to double-destroy errors. Quit the mainloop
# and let the environment handle final destruction.
with contextlib.suppress(Exception):
self.root.quit()
def _auto_save_callback(self) -> None: def _auto_save_callback(self) -> None:
"""Callback function for auto-save operations.""" """Callback function for auto-save operations."""