@@ -35,20 +35,17 @@ def get_outputs(self) -> list[Path]:
3535
3636@pytest .fixture
3737def executor (tmp_path : Path ) -> Executor :
38- """Fixture for creating an Executor with a cache directory."""
3938 cache_dir = tmp_path / "cache"
4039 cache_dir .mkdir ()
4140 return Executor (cache_dir = cache_dir )
4241
4342
4443def test_no_previous_info (executor : Executor ) -> None :
45- """Test that Executor correctly detects that a runnable has not been executed before."""
4644 runnable = MyRunnable ()
4745 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
4846
4947
5048def test_previous_info_matches (executor : Executor , tmp_path : Path ) -> None :
51- """Test that Executor correctly skips execution when previous info matches."""
5249 input_path = tmp_path / "input.txt"
5350 output_path = tmp_path / "output.txt"
5451 input_path .write_text ("input" )
@@ -61,7 +58,6 @@ def test_previous_info_matches(executor: Executor, tmp_path: Path) -> None:
6158
6259
6360def test_file_changed (executor : Executor , tmp_path : Path ) -> None :
64- """Test that Executor correctly detects when a file has changed."""
6561 input_path = tmp_path / "input.txt"
6662 input_path .write_text ("input" )
6763 runnable = MyRunnable (inputs = [input_path ])
@@ -71,7 +67,6 @@ def test_file_changed(executor: Executor, tmp_path: Path) -> None:
7167
7268
7369def test_file_removed (executor : Executor , tmp_path : Path ) -> None :
74- """Test that Executor correctly detects when a file has been removed."""
7570 output_path = tmp_path / "output.txt"
7671 output_path .write_text ("output" )
7772 runnable = MyRunnable (outputs = [output_path ])
@@ -81,7 +76,6 @@ def test_file_removed(executor: Executor, tmp_path: Path) -> None:
8176
8277
8378def test_directory_exists (executor : Executor , tmp_path : Path ) -> None :
84- """Test that Executor correctly handles existing directories."""
8579 input_dir = tmp_path / "input_dir"
8680 output_dir = tmp_path / "output_dir"
8781 input_dir .mkdir ()
@@ -92,7 +86,6 @@ def test_directory_exists(executor: Executor, tmp_path: Path) -> None:
9286
9387
9488def test_directory_removed (executor : Executor , tmp_path : Path ) -> None :
95- """Test that Executor correctly detects when a directory has been removed."""
9689 input_dir = tmp_path / "input_dir"
9790 output_dir = tmp_path / "output_dir"
9891 input_dir .mkdir ()
@@ -104,7 +97,6 @@ def test_directory_removed(executor: Executor, tmp_path: Path) -> None:
10497
10598
10699def test_mixed_files_and_directories (executor : Executor , tmp_path : Path ) -> None :
107- """Test that Executor correctly handles a mix of files and directories."""
108100 input_file = tmp_path / "input.txt"
109101 input_dir = tmp_path / "input_dir"
110102 output_file = tmp_path / "output.txt"
@@ -119,14 +111,12 @@ def test_mixed_files_and_directories(executor: Executor, tmp_path: Path) -> None
119111
120112
121113def test_no_inputs_and_no_outputs (executor : Executor ) -> None :
122- """Test that Executor correctly handles a runnable with no inputs and no outputs."""
123114 runnable = MyRunnable ()
124115 executor .execute (runnable )
125116 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NOTHING_TO_CHECK
126117
127118
128119def test_dry_run (executor : Executor ) -> None :
129- """Test that Executor does not execute the run method when dry_run is True."""
130120 runnable = MyRunnable (return_code = 1 )
131121 executor .dry_run = True
132122 assert executor .execute (runnable ) == 0
@@ -135,30 +125,29 @@ def test_dry_run(executor: Executor) -> None:
135125
136126
137127def test_no_dependency_management (executor : Executor ) -> None :
138- """Test that Executor executes runnables without dependency management directly."""
139128 runnable = MyRunnable (needs_dependency_management = False , return_code = 2 )
140129 assert executor .execute (runnable ) == 2
141130 # Ensure it doesn't store or check run info
142131 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
143132
144133
134+ class ConfigurableRunnable (MyRunnable ):
135+ def __init__ (
136+ self ,
137+ config : dict [str , str ],
138+ inputs : list [Path ] | None = None ,
139+ ) -> None :
140+ super ().__init__ (inputs = inputs )
141+ self ._config = config
142+
143+ def get_config (self ) -> dict [str , str ] | None :
144+ return self ._config
145+
146+
145147def test_config_changed (executor : Executor , tmp_path : Path ) -> None :
146- """Test that Executor detects when the configuration has changed."""
147148 input_path = tmp_path / "input.txt"
148149 input_path .write_text ("input" )
149150
150- class ConfigurableRunnable (MyRunnable ):
151- def __init__ (
152- self ,
153- config : dict [str , str ],
154- inputs : list [Path ] | None = None ,
155- ) -> None :
156- super ().__init__ (inputs = inputs )
157- self ._config = config
158-
159- def get_config (self ) -> dict [str , str ] | None :
160- return self ._config
161-
162151 runnable = ConfigurableRunnable (config = {"key" : "value" }, inputs = [input_path ])
163152 executor .execute (runnable )
164153
@@ -171,22 +160,9 @@ def get_config(self) -> dict[str, str] | None:
171160
172161
173162def test_config_stored (executor : Executor , tmp_path : Path ) -> None :
174- """Test that Executor stores the configuration alongside inputs and outputs."""
175163 input_path = tmp_path / "input.txt"
176164 input_path .write_text ("input" )
177165
178- class ConfigurableRunnable (MyRunnable ):
179- def __init__ (
180- self ,
181- config : dict [str , str ],
182- inputs : list [Path ] | None = None ,
183- ) -> None :
184- super ().__init__ (inputs = inputs )
185- self ._config = config
186-
187- def get_config (self ) -> dict [str , str ] | None :
188- return self ._config
189-
190166 config = {"key" : "value" }
191167 runnable = ConfigurableRunnable (config = config , inputs = [input_path ])
192168 executor .execute (runnable )
@@ -199,7 +175,6 @@ def get_config(self) -> dict[str, str] | None:
199175
200176
201177def test_config_not_stored_if_none (executor : Executor , tmp_path : Path ) -> None :
202- """Test that Executor does not store a config if the runnable has no config."""
203178 input_path = tmp_path / "input.txt"
204179 input_path .write_text ("input" )
205180
@@ -211,3 +186,64 @@ def test_config_not_stored_if_none(executor: Executor, tmp_path: Path) -> None:
211186 with run_info_path .open () as f :
212187 run_info = json .load (f )
213188 assert "config" not in run_info
189+
190+
191+ class DynamicInputRunnable (MyRunnable ):
192+ def __init__ (self , input_dir : Path ) -> None :
193+ super ().__init__ ()
194+ self .input_dir = input_dir
195+
196+ def get_inputs (self ) -> list [Path ]:
197+ # Simulates a runnable that parses all .yaml files from a directory
198+ return list (self .input_dir .glob ("*.yaml" ))
199+
200+
201+ def test_new_input_files_trigger_execution (executor : Executor , tmp_path : Path ) -> None :
202+ input_dir = tmp_path / "configs"
203+ input_dir .mkdir ()
204+
205+ # Create initial yaml file
206+ yaml_file1 = input_dir / "config1.yaml"
207+ yaml_file1 .write_text ("config: value1" )
208+
209+ runnable = DynamicInputRunnable (input_dir )
210+
211+ # First execution should run (no previous info)
212+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
213+ executor .execute (runnable )
214+
215+ # Second execution should be skipped (nothing changed)
216+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .MATCH
217+
218+ # Create a new yaml file - this should trigger re-execution
219+ yaml_file2 = input_dir / "config2.yaml"
220+ yaml_file2 .write_text ("config: value2" )
221+
222+ # This should detect the new input file and require re-execution
223+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .INPUT_FILES_CHANGED
224+
225+
226+ def test_removed_input_files_trigger_execution (executor : Executor , tmp_path : Path ) -> None :
227+ input_dir = tmp_path / "configs"
228+ input_dir .mkdir ()
229+
230+ # Create initial yaml files
231+ yaml_file1 = input_dir / "config1.yaml"
232+ yaml_file1 .write_text ("config: value1" )
233+ yaml_file2 = input_dir / "config2.yaml"
234+ yaml_file2 .write_text ("config: value2" )
235+
236+ runnable = DynamicInputRunnable (input_dir )
237+
238+ # First execution should run (no previous info)
239+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
240+ executor .execute (runnable )
241+
242+ # Second execution should be skipped (nothing changed)
243+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .MATCH
244+
245+ # Remove one yaml file - this should trigger re-execution
246+ yaml_file2 .unlink ()
247+
248+ # This should detect the missing input file and require re-execution
249+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .INPUT_FILES_CHANGED
0 commit comments