Skip to content

Commit fb286f0

Browse files
committed
chore: Test worker crash recovery
1 parent 2e7f739 commit fb286f0

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""
2+
Tests for process pool crash recovery
3+
"""
4+
5+
import asyncio
6+
import os
7+
import tempfile
8+
import unittest
9+
from unittest.mock import MagicMock, patch
10+
from concurrent.futures import BrokenExecutor, Future
11+
12+
# Set dummy API key for testing
13+
os.environ["OPENAI_API_KEY"] = "test"
14+
15+
from openevolve.config import Config
16+
from openevolve.database import Program, ProgramDatabase
17+
from openevolve.process_parallel import ProcessParallelController, SerializableResult
18+
19+
20+
class TestProcessPoolRecovery(unittest.TestCase):
21+
"""Tests for process pool crash recovery"""
22+
23+
def setUp(self):
24+
"""Set up test environment"""
25+
self.test_dir = tempfile.mkdtemp()
26+
27+
# Create test config
28+
self.config = Config()
29+
self.config.max_iterations = 10
30+
self.config.evaluator.parallel_evaluations = 2
31+
self.config.evaluator.timeout = 10
32+
self.config.database.num_islands = 2
33+
self.config.database.in_memory = True
34+
self.config.checkpoint_interval = 5
35+
36+
# Create test evaluation file
37+
self.eval_content = """
38+
def evaluate(program_path):
39+
return {"score": 0.5}
40+
"""
41+
self.eval_file = os.path.join(self.test_dir, "evaluator.py")
42+
with open(self.eval_file, "w") as f:
43+
f.write(self.eval_content)
44+
45+
# Create test database
46+
self.database = ProgramDatabase(self.config.database)
47+
48+
# Add some test programs
49+
for i in range(2):
50+
program = Program(
51+
id=f"test_{i}",
52+
code=f"def func_{i}(): return {i}",
53+
language="python",
54+
metrics={"score": 0.5},
55+
iteration_found=0,
56+
)
57+
self.database.add(program)
58+
59+
def tearDown(self):
60+
"""Clean up test environment"""
61+
import shutil
62+
63+
shutil.rmtree(self.test_dir, ignore_errors=True)
64+
65+
def test_controller_has_recovery_tracking(self):
66+
"""Test that controller initializes with recovery tracking attributes"""
67+
controller = ProcessParallelController(self.config, self.eval_file, self.database)
68+
69+
self.assertEqual(controller.recovery_attempts, 0)
70+
self.assertEqual(controller.max_recovery_attempts, 3)
71+
72+
def test_recover_process_pool_recreates_executor(self):
73+
"""Test that _recover_process_pool recreates the executor"""
74+
controller = ProcessParallelController(self.config, self.eval_file, self.database)
75+
76+
# Start the controller to create initial executor
77+
controller.start()
78+
self.assertIsNotNone(controller.executor)
79+
original_executor = controller.executor
80+
81+
# Simulate recovery
82+
with patch("time.sleep"):
83+
controller._recover_process_pool()
84+
85+
# Verify executor was recreated
86+
self.assertIsNotNone(controller.executor)
87+
self.assertIsNot(controller.executor, original_executor)
88+
89+
# Clean up
90+
controller.stop()
91+
92+
def test_broken_executor_triggers_recovery_and_resets_on_success(self):
93+
"""Test that BrokenExecutor triggers recovery and counter resets on success"""
94+
95+
async def run_test():
96+
controller = ProcessParallelController(self.config, self.eval_file, self.database)
97+
98+
# Track recovery calls
99+
recovery_called = []
100+
101+
def mock_recover(failed_iterations=None):
102+
recovery_called.append(failed_iterations)
103+
104+
controller._recover_process_pool = mock_recover
105+
106+
# First call raises BrokenExecutor, subsequent calls succeed
107+
call_count = [0]
108+
109+
def mock_submit(iteration, island_id):
110+
call_count[0] += 1
111+
mock_future = MagicMock(spec=Future)
112+
113+
if call_count[0] == 1:
114+
# First future raises BrokenExecutor when result() is called
115+
mock_future.done.return_value = True
116+
mock_future.result.side_effect = BrokenExecutor("Pool crashed")
117+
else:
118+
# Subsequent calls succeed
119+
mock_result = SerializableResult(
120+
child_program_dict={
121+
"id": f"child_{call_count[0]}",
122+
"code": "def evolved(): return 1",
123+
"language": "python",
124+
"parent_id": "test_0",
125+
"generation": 1,
126+
"metrics": {"score": 0.7},
127+
"iteration_found": iteration,
128+
"metadata": {"island": island_id},
129+
},
130+
parent_id="test_0",
131+
iteration_time=0.1,
132+
iteration=iteration,
133+
)
134+
mock_future.done.return_value = True
135+
mock_future.result.return_value = mock_result
136+
mock_future.cancel.return_value = True
137+
138+
return mock_future
139+
140+
with patch.object(controller, "_submit_iteration", side_effect=mock_submit):
141+
controller.start()
142+
143+
# Run evolution - should recover from crash and reset counter on success
144+
await controller.run_evolution(
145+
start_iteration=1, max_iterations=2, target_score=None
146+
)
147+
148+
# Verify recovery was triggered
149+
self.assertEqual(len(recovery_called), 1)
150+
# Verify counter was reset after successful iteration
151+
self.assertEqual(controller.recovery_attempts, 0)
152+
153+
asyncio.run(run_test())
154+
155+
156+
if __name__ == "__main__":
157+
unittest.main()

0 commit comments

Comments
 (0)