1+ import json
12import os
23from pathlib import Path
34from typing import List , Optional
89
910
1011class MyRunnable (Runnable ):
11- def __init__ (self , inputs : Optional [List [Path ]] = None , outputs : Optional [List [Path ]] = None , return_code : int = 0 , needs_dependency_management : bool = True ):
12+ def __init__ (
13+ self ,
14+ inputs : Optional [List [Path ]] = None ,
15+ outputs : Optional [List [Path ]] = None ,
16+ return_code : int = 0 ,
17+ needs_dependency_management : bool = True ,
18+ ) -> None :
1219 super ().__init__ (needs_dependency_management = needs_dependency_management )
1320 self ._inputs = inputs if inputs is not None else []
1421 self ._outputs = outputs if outputs is not None else []
@@ -28,20 +35,20 @@ def get_outputs(self) -> List[Path]:
2835
2936
3037@pytest .fixture
31- def executor (tmp_path ) :
38+ def executor (tmp_path : Path ) -> Executor :
3239 """Fixture for creating an Executor with a cache directory."""
3340 cache_dir = tmp_path / "cache"
3441 cache_dir .mkdir ()
3542 return Executor (cache_dir = cache_dir )
3643
3744
38- def test_no_previous_info (executor ) :
45+ def test_no_previous_info (executor : Executor ) -> None :
3946 """Test that Executor correctly detects that a runnable has not been executed before."""
4047 runnable = MyRunnable ()
4148 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
4249
4350
44- def test_previous_info_matches (executor , tmp_path ) :
51+ def test_previous_info_matches (executor : Executor , tmp_path : Path ) -> None :
4552 """Test that Executor correctly skips execution when previous info matches."""
4653 input_path = tmp_path / "input.txt"
4754 output_path = tmp_path / "output.txt"
@@ -54,7 +61,7 @@ def test_previous_info_matches(executor, tmp_path):
5461 assert new_executor .previous_run_info_matches (runnable ) == RunInfoStatus .FORCED_RUN
5562
5663
57- def test_file_changed (executor , tmp_path ) :
64+ def test_file_changed (executor : Executor , tmp_path : Path ) -> None :
5865 """Test that Executor correctly detects when a file has changed."""
5966 input_path = tmp_path / "input.txt"
6067 input_path .write_text ("input" )
@@ -64,7 +71,7 @@ def test_file_changed(executor, tmp_path):
6471 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .FILE_CHANGED
6572
6673
67- def test_file_removed (executor , tmp_path ) :
74+ def test_file_removed (executor : Executor , tmp_path : Path ) -> None :
6875 """Test that Executor correctly detects when a file has been removed."""
6976 output_path = tmp_path / "output.txt"
7077 output_path .write_text ("output" )
@@ -74,7 +81,7 @@ def test_file_removed(executor, tmp_path):
7481 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .FILE_NOT_FOUND
7582
7683
77- def test_directory_exists (executor , tmp_path ) :
84+ def test_directory_exists (executor : Executor , tmp_path : Path ) -> None :
7885 """Test that Executor correctly handles existing directories."""
7986 input_dir = tmp_path / "input_dir"
8087 output_dir = tmp_path / "output_dir"
@@ -85,7 +92,7 @@ def test_directory_exists(executor, tmp_path):
8592 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .MATCH
8693
8794
88- def test_directory_removed (executor , tmp_path ) :
95+ def test_directory_removed (executor : Executor , tmp_path : Path ) -> None :
8996 """Test that Executor correctly detects when a directory has been removed."""
9097 input_dir = tmp_path / "input_dir"
9198 output_dir = tmp_path / "output_dir"
@@ -97,7 +104,7 @@ def test_directory_removed(executor, tmp_path):
97104 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .FILE_NOT_FOUND
98105
99106
100- def test_mixed_files_and_directories (executor , tmp_path ) :
107+ def test_mixed_files_and_directories (executor : Executor , tmp_path : Path ) -> None :
101108 """Test that Executor correctly handles a mix of files and directories."""
102109 input_file = tmp_path / "input.txt"
103110 input_dir = tmp_path / "input_dir"
@@ -112,14 +119,14 @@ def test_mixed_files_and_directories(executor, tmp_path):
112119 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .MATCH
113120
114121
115- def test_no_inputs_and_no_outputs (executor ) :
122+ def test_no_inputs_and_no_outputs (executor : Executor ) -> None :
116123 """Test that Executor correctly handles a runnable with no inputs and no outputs."""
117124 runnable = MyRunnable ()
118125 executor .execute (runnable )
119126 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NOTHING_TO_CHECK
120127
121128
122- def test_dry_run (executor ) :
129+ def test_dry_run (executor : Executor ) -> None :
123130 """Test that Executor does not execute the run method when dry_run is True."""
124131 runnable = MyRunnable (return_code = 1 )
125132 executor .dry_run = True
@@ -128,9 +135,80 @@ def test_dry_run(executor):
128135 assert executor .execute (runnable ) == 1
129136
130137
131- def test_no_dependency_management (executor ) :
138+ def test_no_dependency_management (executor : Executor ) -> None :
132139 """Test that Executor executes runnables without dependency management directly."""
133140 runnable = MyRunnable (needs_dependency_management = False , return_code = 2 )
134141 assert executor .execute (runnable ) == 2
135142 # Ensure it doesn't store or check run info
136143 assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .NO_INFO
144+
145+
146+ def test_config_changed (executor : Executor , tmp_path : Path ) -> None :
147+ """Test that Executor detects when the configuration has changed."""
148+ input_path = tmp_path / "input.txt"
149+ input_path .write_text ("input" )
150+
151+ class ConfigurableRunnable (MyRunnable ):
152+ def __init__ (
153+ self ,
154+ config : dict [str , str ],
155+ inputs : Optional [List [Path ]] = None ,
156+ ) -> None :
157+ super ().__init__ (inputs = inputs )
158+ self ._config = config
159+
160+ def get_config (self ) -> Optional [dict [str , str ]]:
161+ return self ._config
162+
163+ runnable = ConfigurableRunnable (config = {"key" : "value" }, inputs = [input_path ])
164+ executor .execute (runnable )
165+
166+ # Ensure it matches initially
167+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .MATCH
168+
169+ # Change the configuration
170+ runnable = ConfigurableRunnable (config = {"key" : "new_value" }, inputs = [input_path ])
171+ assert executor .previous_run_info_matches (runnable ) == RunInfoStatus .CONFIG_CHANGED
172+
173+
174+ def test_config_stored (executor : Executor , tmp_path : Path ) -> None :
175+ """Test that Executor stores the configuration alongside inputs and outputs."""
176+ input_path = tmp_path / "input.txt"
177+ input_path .write_text ("input" )
178+
179+ class ConfigurableRunnable (MyRunnable ):
180+ def __init__ (
181+ self ,
182+ config : dict [str , str ],
183+ inputs : Optional [List [Path ]] = None ,
184+ ) -> None :
185+ super ().__init__ (inputs = inputs )
186+ self ._config = config
187+
188+ def get_config (self ) -> Optional [dict [str , str ]]:
189+ return self ._config
190+
191+ config = {"key" : "value" }
192+ runnable = ConfigurableRunnable (config = config , inputs = [input_path ])
193+ executor .execute (runnable )
194+
195+ # Verify the stored run info contains the configuration
196+ run_info_path = executor .get_runnable_run_info_file (runnable )
197+ with run_info_path .open () as f :
198+ run_info = json .load (f )
199+ assert run_info ["config" ] == config
200+
201+
202+ def test_config_not_stored_if_none (executor : Executor , tmp_path : Path ) -> None :
203+ """Test that Executor does not store a config if the runnable has no config."""
204+ input_path = tmp_path / "input.txt"
205+ input_path .write_text ("input" )
206+
207+ runnable = MyRunnable (inputs = [input_path ])
208+ executor .execute (runnable )
209+
210+ # Verify the stored run info does not contain a config field
211+ run_info_path = executor .get_runnable_run_info_file (runnable )
212+ with run_info_path .open () as f :
213+ run_info = json .load (f )
214+ assert "config" not in run_info
0 commit comments