diff --git a/src/mvt/common/command.py b/src/mvt/common/command.py index 6938271c..2e784b29 100644 --- a/src/mvt/common/command.py +++ b/src/mvt/common/command.py @@ -409,6 +409,16 @@ class Command: return handler return None + def _publish_completed_modules( + self, + ordered_modules: list[type[MVTModule]], + completed: dict[type[MVTModule], MVTModule], + ) -> None: + """Expose a stable snapshot of completed modules to initialization hooks.""" + self.executed = [ + completed[module] for module in ordered_modules if module in completed + ] + def _aggregate_modules( self, ordered_modules: list[type[MVTModule]], @@ -419,7 +429,6 @@ class Command: if module not in completed: continue instance = completed[module] - self.executed.append(instance) self.timeline.extend(instance.timeline) self.alertstore.extend(instance.alertstore.alerts) @@ -432,6 +441,7 @@ class Command: if encrypted: return completed, True completed[module] = instance + self._publish_completed_modules(ordered_modules, completed) return completed, False def _run_parallel( @@ -485,6 +495,7 @@ class Command: fatal = True break completed[module] = instance + self._publish_completed_modules(ordered_modules, completed) update_status() continue @@ -504,6 +515,7 @@ class Command: fatal = True break completed[module] = instance + self._publish_completed_modules(ordered_modules, completed) update_status() continue @@ -530,6 +542,7 @@ class Command: fatal = True break completed[module] = instance + self._publish_completed_modules(ordered_modules, completed) update_status() if fatal: diff --git a/tests/common/test_command.py b/tests/common/test_command.py index 61c49797..9b695ef3 100644 --- a/tests/common/test_command.py +++ b/tests/common/test_command.py @@ -9,6 +9,7 @@ import threading import time from io import StringIO +import pytest from rich.console import Console from mvt.common.command import Command @@ -126,6 +127,38 @@ class TestCommand: "ThirdModule", ] + @pytest.mark.parametrize("jobs", [1, 2]) + @pytest.mark.parametrize("parallel_safe", [False, True]) + def test_initialization_hook_sees_completed_dependencies(self, jobs, parallel_safe): + class Prerequisite(FirstModule): + pass + + Prerequisite.parallel_safe = parallel_safe + + class Dependent(SecondModule): + dependencies = (Prerequisite,) + + def run(self): + self.results = self.get_dependency_results(Prerequisite) + ["second"] + + class HookCommand(RecordingCommand): + def module_init(self, module): + if isinstance(module, Dependent): + self.seen = [ + executed.results + for executed in self.executed + if isinstance(executed, Prerequisite) + ] + + cmd = HookCommand(jobs=jobs) + cmd.modules = [Dependent, Prerequisite] + + cmd.run() + + assert cmd.seen == [["first"]] + assert [type(module) for module in cmd.executed] == [Prerequisite, Dependent] + assert cmd.executed[1].results == ["first", "second"] + def test_circular_dependency_warns_and_stops(self, caplog): class CircularOne(RecordingModule): pass