Skip to content

Commit d0fd321

Browse files
codelionclaude
andcommitted
Bump version to 0.2.27 and add subprocess evaluator tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7215694 commit d0fd321

File tree

2 files changed

+113
-9
lines changed

2 files changed

+113
-9
lines changed

openevolve/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version information for openevolve package."""
22

3-
__version__ = "0.2.26"
3+
__version__ = "0.2.27"

tests/test_api.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)