@@ -116,17 +116,55 @@ def test_prepare_evaluator_from_callable(self):
116116 """Test _prepare_evaluator with callable function"""
117117 def my_evaluator (program_path ):
118118 return {"score" : 0.8 , "test" : "passed" }
119-
119+
120120 temp_files = []
121121 result = _prepare_evaluator (my_evaluator , self .temp_dir , temp_files )
122-
122+
123123 self .assertTrue (os .path .exists (result ))
124124 self .assertEqual (len (temp_files ), 1 )
125-
125+
126126 with open (result , 'r' ) as f :
127127 content = f .read ()
128128 self .assertIn ("def evaluate(program_path)" , content )
129- self .assertIn ("user_evaluator" , content )
129+ self .assertIn ("my_evaluator" , content )
130+
131+ def test_prepare_evaluator_callable_works_in_subprocess (self ):
132+ """Test that callable evaluator can be executed in a subprocess"""
133+ import subprocess
134+ import sys
135+
136+ def my_evaluator (program_path ):
137+ return {"score" : 0.8 , "combined_score" : 0.8 }
138+
139+ temp_files = []
140+ eval_file = _prepare_evaluator (my_evaluator , self .temp_dir , temp_files )
141+
142+ # Write a dummy program file for the evaluator to receive
143+ program_file = os .path .join (self .temp_dir , "dummy_program.py" )
144+ with open (program_file , 'w' ) as f :
145+ f .write ("x = 1\n " )
146+
147+ # Run the evaluator in a subprocess (simulating process-based parallelism)
148+ test_script = os .path .join (self .temp_dir , "run_eval.py" )
149+ with open (test_script , 'w' ) as f :
150+ f .write (f"""
151+ import sys
152+ import importlib.util
153+ spec = importlib.util.spec_from_file_location("evaluator", { eval_file !r} )
154+ mod = importlib.util.module_from_spec(spec)
155+ spec.loader.exec_module(mod)
156+ result = mod.evaluate({ program_file !r} )
157+ assert isinstance(result, dict), f"Expected dict, got {{type(result)}}"
158+ assert result["score"] == 0.8, f"Expected score 0.8, got {{result['score']}}"
159+ print("OK")
160+ """ )
161+
162+ proc = subprocess .run (
163+ [sys .executable , test_script ],
164+ capture_output = True , text = True , timeout = 10
165+ )
166+ self .assertEqual (proc .returncode , 0 , f"Subprocess failed: { proc .stderr } " )
167+ self .assertIn ("OK" , proc .stdout )
130168
131169 def test_prepare_evaluator_from_string (self ):
132170 """Test _prepare_evaluator with code string"""
@@ -159,12 +197,12 @@ def initial_sort(arr):
159197 if arr [j ] > arr [j + 1 ]:
160198 arr [j ], arr [j + 1 ] = arr [j + 1 ], arr [j ]
161199 return arr
162-
200+
163201 test_cases = [
164202 ([3 , 1 , 2 ], [1 , 2 , 3 ]),
165203 ([5 , 2 ], [2 , 5 ]),
166204 ]
167-
205+
168206 # Mock the async controller to avoid actual evolution
169207 with unittest .mock .patch ('openevolve.api._run_evolution_async' ) as mock_async :
170208 mock_async .return_value = EvolutionResult (
@@ -174,12 +212,78 @@ def initial_sort(arr):
174212 metrics = {"score" : 1.0 , "test_pass_rate" : 1.0 },
175213 output_dir = None
176214 )
177-
215+
178216 result = evolve_function (initial_sort , test_cases , iterations = 1 )
179-
217+
180218 self .assertIsInstance (result , EvolutionResult )
181219 self .assertEqual (result .best_score , 1.0 )
182220 mock_async .assert_called_once ()
221+
222+ def test_evolve_function_evaluator_works_in_subprocess (self ):
223+ """Test that evolve_function generates an evaluator that works in a subprocess.
224+
225+ This is a regression test for the bug where callable evaluators stored in
226+ globals() could not be accessed by process-based worker subprocesses.
227+ """
228+ import subprocess
229+ import sys
230+
231+ def bubble_sort (arr ):
232+ for i in range (len (arr )):
233+ for j in range (len (arr ) - 1 ):
234+ if arr [j ] > arr [j + 1 ]:
235+ arr [j ], arr [j + 1 ] = arr [j + 1 ], arr [j ]
236+ return arr
237+
238+ test_cases = [([3 , 1 , 2 ], [1 , 2 , 3 ]), ([5 , 2 , 8 ], [2 , 5 , 8 ])]
239+
240+ # Call evolve_function but intercept the evaluator code it generates
241+ # by capturing what gets passed to run_evolution
242+ with unittest .mock .patch ('openevolve.api.run_evolution' ) as mock_run :
243+ mock_run .return_value = EvolutionResult (
244+ best_program = None , best_score = 1.0 ,
245+ best_code = "" , metrics = {}, output_dir = None
246+ )
247+ evolve_function (bubble_sort , test_cases , iterations = 1 )
248+
249+ # Extract the evaluator code string passed to run_evolution
250+ call_kwargs = mock_run .call_args
251+ evaluator_code = call_kwargs .kwargs .get ('evaluator' ) or call_kwargs [1 ].get ('evaluator' )
252+
253+ self .assertIsInstance (evaluator_code , str , "evolve_function should pass evaluator as code string" )
254+ self .assertIn ("def evaluate(program_path)" , evaluator_code )
255+ self .assertIn ("combined_score" , evaluator_code )
256+
257+ # Write the evaluator to a file
258+ eval_file = os .path .join (self .temp_dir , "eval_test.py" )
259+ with open (eval_file , 'w' ) as f :
260+ f .write (evaluator_code )
261+
262+ # Write a correct program for the evaluator to test
263+ program_file = os .path .join (self .temp_dir , "program.py" )
264+ with open (program_file , 'w' ) as f :
265+ f .write ("def bubble_sort(arr):\n return sorted(arr)\n " )
266+
267+ # Run in a subprocess to verify it works across process boundaries
268+ test_script = os .path .join (self .temp_dir , "run_eval.py" )
269+ with open (test_script , 'w' ) as f :
270+ f .write (f"""
271+ import importlib.util
272+ spec = importlib.util.spec_from_file_location("evaluator", { eval_file !r} )
273+ mod = importlib.util.module_from_spec(spec)
274+ spec.loader.exec_module(mod)
275+ result = mod.evaluate({ program_file !r} )
276+ assert result["combined_score"] == 1.0, f"Expected 1.0, got {{result['combined_score']}}"
277+ assert result["tests_passed"] == 2, f"Expected 2, got {{result['tests_passed']}}"
278+ print("OK")
279+ """ )
280+
281+ proc = subprocess .run (
282+ [sys .executable , test_script ],
283+ capture_output = True , text = True , timeout = 10
284+ )
285+ self .assertEqual (proc .returncode , 0 , f"Subprocess failed: { proc .stderr } " )
286+ self .assertIn ("OK" , proc .stdout )
183287
184288 def test_evolve_algorithm_basic (self ):
185289 """Test evolve_algorithm with simple class"""
0 commit comments