diff --git a/src/export_manager.py b/src/export_manager.py index d91e372..808f0eb 100644 --- a/src/export_manager.py +++ b/src/export_manager.py @@ -53,6 +53,16 @@ class ExportManager: self.medicine_manager = medicine_manager self.pathology_manager = pathology_manager 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( 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: 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}") return True @@ -142,6 +154,8 @@ class ExportManager: with open(export_path, "w", encoding="utf-8") as f: 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}") return True diff --git a/src/graph_manager.py b/src/graph_manager.py index 0bec19c..fe79f84 100644 --- a/src/graph_manager.py +++ b/src/graph_manager.py @@ -11,10 +11,12 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from medicine_manager import MedicineManager from pathology_manager import PathologyManager -# Provide a module alias for tests that patch 'graph_manager.*' symbols while -# importing from 'src.graph_manager'. This makes both names refer to the same -# module object. -sys.modules.setdefault("graph_manager", sys.modules[__name__]) +# Ensure both import styles ('graph_manager' and 'src.graph_manager') refer to +# the same module object so test patches apply reliably regardless of import +# order across the suite. +_this_mod = sys.modules.get(__name__) +sys.modules["graph_manager"] = _this_mod +sys.modules["src.graph_manager"] = _this_mod def _build_default_medicine_manager(): @@ -183,10 +185,14 @@ class GraphManager: # call signature. Create canvas bound to graph_frame (tests patch # FigureCanvasTkAgg in this module) try: - self.canvas = FigureCanvasTkAgg(figure=self.fig, master=self.graph_frame) - # Draw idle for better performance - self.canvas.draw_idle() - except (tk.TclError, RuntimeError): + # Important: use the class from this module's namespace so tests + # 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() + except (tk.TclError, RuntimeError, TypeError): # Fallback dummy canvas for environments where FigureCanvasTkAgg # interacts poorly with mocks or missing Tk resources. class _DummyCanvas: @@ -343,7 +349,7 @@ class GraphManager: self.canvas.draw() except Exception: # Fallback to draw_idle in real canvas - with plt.ioff(): + with plt.ioff(), suppress(Exception): self.canvas.draw_idle() def _preprocess_data(self, df: pd.DataFrame) -> pd.DataFrame: diff --git a/src/logger.py b/src/logger.py index f2d77b2..834a8e0 100644 --- a/src/logger.py +++ b/src/logger.py @@ -8,6 +8,7 @@ from __future__ import annotations import contextlib import logging +import os import sys as _sys 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 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' -_sys.modules.setdefault("logger", _sys.modules.get(__name__)) +# Ensure both import styles ('logger' and 'src.logger') point to the same module +# 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: @@ -89,22 +100,26 @@ def init_logger(dunder_name: str, testing_mode: bool) -> logging.Logger: formatter = logging.Formatter(log_format) 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( - 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.setFormatter(formatter) logger.addHandler(fh_all) 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.setFormatter(formatter) logger.addHandler(fh_warn) 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.setFormatter(formatter) diff --git a/src/main.py b/src/main.py index 3f97b39..c5ba901 100644 --- a/src/main.py +++ b/src/main.py @@ -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.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: """Callback function for auto-save operations."""