diff --git a/src/hot_reloader.py b/src/hot_reloader.py index c1f235b1..ff746407 100644 --- a/src/hot_reloader.py +++ b/src/hot_reloader.py @@ -8,6 +8,8 @@ import traceback from dataclasses import dataclass, field from typing import Any +from src.result_types import Result, ErrorInfo, ErrorKind + @dataclass class HotModule: @@ -37,11 +39,12 @@ class HotReloader: setattr(app, key, value) @classmethod - def reload(cls, module_name: str, app: Any) -> bool: + def reload(cls, module_name: str, app: Any) -> Result[bool]: if module_name not in cls.HOT_MODULES: - cls.last_error = f"Module {module_name} not registered" + err_msg = f"Module {module_name} not registered" + cls.last_error = err_msg cls.is_error_state = True - return False + return Result(data=False, errors=[ErrorInfo(kind=ErrorKind.NOT_FOUND, message=err_msg, source=f"hot_reloader.reload[{module_name}]")]) hm = cls.HOT_MODULES[module_name] state = cls.capture_state(app, hm.state_keys) @@ -54,16 +57,19 @@ class HotReloader: importlib.import_module(module_name) cls.last_error = None cls.is_error_state = False - return True - except Exception: + return Result(data=True) + except Exception as e: cls.restore_state(app, state) - cls.last_error = traceback.format_exc() + tb = traceback.format_exc() + cls.last_error = tb cls.is_error_state = True - return False + return Result(data=False, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source=f"hot_reloader.reload[{module_name}]", original=e)]) @classmethod - def reload_all(cls, app: Any) -> bool: - success = True + def reload_all(cls, app: Any) -> Result[bool]: + errors: list[ErrorInfo] = [] for name in cls.HOT_MODULES: - if not cls.reload(name, app): success = False - return success + result = cls.reload(name, app) + if not result.ok: + errors.extend(result.errors) + return Result(data=len(errors) == 0, errors=errors) diff --git a/src/startup_profiler.py b/src/startup_profiler.py index 9d153ca8..ca141e65 100644 --- a/src/startup_profiler.py +++ b/src/startup_profiler.py @@ -37,8 +37,8 @@ class StartupProfiler: try: sys.stderr.write(f"[startup] {name}: {(p.end_ts - p.start_ts) * 1000.0:.1f}ms\n") sys.stderr.flush() - except OSError: - pass + except OSError as e: + sys.stderr.write(f"[startup] phase output failed for {name}: {e}\n") def snapshot(self) -> dict[str, Any]: phases: dict[str, dict[str, float]] = {} diff --git a/src/warmup.py b/src/warmup.py index 054e1212..8fba3dc4 100644 --- a/src/warmup.py +++ b/src/warmup.py @@ -136,8 +136,8 @@ class WarmupManager: if fire_now: try: callback(snap) - except Exception: - pass + except Exception as e: + sys.stderr.write(f"[WarmupManager] on_complete callback raised: {e}\n") def reset(self) -> None: with self._lock: @@ -212,8 +212,8 @@ class WarmupManager: for cb in callbacks: try: cb(self._snapshot()) - except Exception: - pass + except Exception as e: + sys.stderr.write(f"[WarmupManager] _record_success callback raised: {e}\n") if all_done: self._done_event.set() @@ -246,8 +246,8 @@ class WarmupManager: for cb in callbacks: try: cb(self._snapshot()) - except Exception: - pass + except Exception as e: + sys.stderr.write(f"[WarmupManager] _record_failure callback raised: {e}\n") if all_done: self._done_event.set() @@ -273,7 +273,8 @@ class WarmupManager: try: sys.stderr.write(line) sys.stderr.flush() - except OSError: pass + except OSError as e: + sys.stderr.write(f"[WarmupManager] canary log write failed: {e}\n") def _log_summary(self) -> None: if not self._log_to_stderr: return @@ -297,7 +298,8 @@ class WarmupManager: if main_thread_violations: sys.stderr.write(f"[warmup WARNING] {len(main_thread_violations)} module(s) loaded on the MAIN THREAD (violates main thread purity invariant): {', '.join(main_thread_violations)}\n") sys.stderr.flush() - except OSError: pass + except OSError as e: + sys.stderr.write(f"[WarmupManager] summary log write failed: {e}\n") def _snapshot(self) -> dict[str, list[str]]: return { diff --git a/tests/test_hot_reload_integration.py b/tests/test_hot_reload_integration.py index a484e2b1..035bbe19 100644 --- a/tests/test_hot_reload_integration.py +++ b/tests/test_hot_reload_integration.py @@ -40,7 +40,7 @@ def test_reload_unknown_module_returns_false(): HotReloader.HOT_MODULES.clear() mock_app = MagicMock() result = HotReloader.reload('unknown.module', mock_app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.last_error == "Module unknown.module not registered" assert HotReloader.is_error_state is True @@ -56,7 +56,7 @@ def test_reload_success_clears_error_state(): patch('importlib.import_module') as mock_import: mock_import.side_effect = Exception("Module does not exist") result = HotReloader.reload('test.module', mock_app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.last_error is not None HotReloader.HOT_MODULES.clear() @@ -69,7 +69,7 @@ def test_reload_captures_and_restores_state_on_failure(): mock_app.active_discussion = 'main' with patch('importlib.reload', side_effect=Exception("Reload failed")): result = HotReloader.reload('test.module', mock_app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.is_error_state is True @@ -85,7 +85,7 @@ def test_reload_all_success(): mock_reload.return_value = None mock_import.return_value = MagicMock() result = HotReloader.reload_all(mock_app) - assert result is True + assert result.ok is True and result.data is True def test_reload_all_partial_failure(): @@ -95,7 +95,7 @@ def test_reload_all_partial_failure(): mock_app = MagicMock() with patch('importlib.reload', side_effect=Exception("Fail")): result = HotReloader.reload_all(mock_app) - assert result is False + assert result.ok is False and result.data is False class TestHotReloadTriggerIntegration: diff --git a/tests/test_hot_reloader.py b/tests/test_hot_reloader.py index 44f8c34e..d04417f9 100644 --- a/tests/test_hot_reloader.py +++ b/tests/test_hot_reloader.py @@ -42,7 +42,7 @@ def test_reload_unknown_module_returns_false(): HotReloader.register(HotModule(name="nonexistent_mod", file_path="/nonexistent.py", state_keys=[], delegation_targets=[])) app = MagicMock() result = HotReloader.reload("nonexistent_mod", app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.is_error_state is True assert HotReloader.last_error is not None @@ -56,7 +56,7 @@ def test_reload_success_clears_error_state(): HotReloader.last_error = "previous error" with patch("importlib.reload", return_value=test_mod): result = HotReloader.reload("src._test_reload_mod_src", app) - assert result is True + assert result.ok is True and result.data is True assert HotReloader.is_error_state is False assert HotReloader.last_error is None del sys.modules["src._test_reload_mod_src"] @@ -67,7 +67,7 @@ def test_reload_captures_and_restores_state_on_failure(): app = MagicMock() app._test_attr = "preserved_value" result = HotReloader.reload("bad_mod", app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.is_error_state is True assert app._test_attr == "preserved_value" @@ -82,7 +82,7 @@ def test_reload_all_success(): app = MagicMock() with patch("importlib.reload", return_value=mod1): result = HotReloader.reload_all(app) - assert result is True + assert result.ok is True and result.data is True assert HotReloader.is_error_state is False del sys.modules["hr_test_mod1"] del sys.modules["hr_test_mod2"] @@ -95,6 +95,6 @@ def test_reload_all_partial_failure(): HotReloader.register(HotModule(name="hr_nonexistent", file_path="/nonexistent.py", state_keys=[], delegation_targets=[])) app = MagicMock() result = HotReloader.reload_all(app) - assert result is False + assert result.ok is False and result.data is False assert HotReloader.is_error_state is True del sys.modules["hr_test_mod1"] \ No newline at end of file