-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathtest_pyiron_workflow.py
More file actions
243 lines (208 loc) · 7.65 KB
/
Copy pathtest_pyiron_workflow.py
File metadata and controls
243 lines (208 loc) · 7.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
"""
The purpose of these tests is the check executor behaviour when the python objects
are dynamically generated.
This is a special (and rather difficult) case for serializing objects which cannot
be pickled using the standard pickle module, and thus poses a relatively thorough test
for the general un-pickle-able case.
"""
from concurrent.futures._base import TimeoutError as cfbTimeoutError
from functools import partialmethod
from time import sleep
from typing import Callable
import unittest
from executorlib import SingleNodeExecutor
from executorlib.standalone.serialize import cloudpickle_register
class Foo:
"""
A base class to be dynamically modified for putting an executor/serializer through
its paces.
"""
def __init__(self, fnc: Callable):
self.fnc = fnc
self.result = None
self.running = False
@property
def run(self):
self.running = True
return self.fnc
def process_result(self, future):
self.result = future.result()
self.running = False
def dynamic_foo():
"""
A decorator for dynamically modifying the Foo class to test
CloudpickleProcessPoolExecutor.
Overrides the `fnc` input of `Foo` with the decorated function.
"""
def as_dynamic_foo(fnc: Callable):
return type(
"DynamicFoo",
(Foo,), # Define parentage
{"__init__": partialmethod(Foo.__init__, fnc)},
)
return as_dynamic_foo
class TestDynamicallyDefinedObjects(unittest.TestCase):
def test_args(self):
"""
We should be able to use a dynamically defined return value.
"""
@dynamic_foo()
def does_nothing():
return
@dynamic_foo()
def slowly_returns_dynamic(dynamic_arg):
"""
Returns a complex, dynamically defined variable
"""
sleep(0.1)
dynamic_arg.attribute_on_dynamic = "attribute updated"
return dynamic_arg
dynamic_dynamic = slowly_returns_dynamic()
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
dynamic_object = does_nothing()
fs = executor.submit(dynamic_dynamic.run, dynamic_object)
self.assertEqual(
fs.result().attribute_on_dynamic,
"attribute updated",
msg="The submit callable should have modified the mutable, dynamically "
"defined object with a new attribute.",
)
def test_callable(self):
"""
We should be able to use a dynamic callable -- in this case, a method of
a dynamically defined class.
"""
fortytwo = 42 # No magic numbers; we use it in a couple places so give it a var
@dynamic_foo()
def slowly_returns_42():
sleep(0.1)
return fortytwo
dynamic_42 = slowly_returns_42() # Instantiate the dynamically defined class
self.assertIsInstance(
dynamic_42, Foo, msg="Just a sanity check that the test is set up right"
)
self.assertIsNone(
dynamic_42.result, msg="Just a sanity check that the test is set up right"
)
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
fs = executor.submit(dynamic_42.run)
fs.add_done_callback(dynamic_42.process_result)
self.assertFalse(
fs.done(),
msg="The submit callable sleeps long enough that we expect to still be "
"running here -- did something fail to get submit to an executor??",
)
self.assertEqual(
fortytwo, fs.result(), msg="The future is expected to behave as usual"
)
self.assertEqual(
fortytwo,
dynamic_42.result,
msg="The callback modifies its object and should run by the time the result"
"is available -- did it fail to get called?",
)
def test_callback(self):
"""Make sure the callback methods can modify their owners"""
@dynamic_foo()
def returns_42():
return 42
dynamic_42 = returns_42()
self.assertFalse(
dynamic_42.running,
msg="Sanity check that the test starts in the expected condition",
)
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
fs = executor.submit(dynamic_42.run)
fs.add_done_callback(dynamic_42.process_result)
self.assertTrue(
dynamic_42.running,
msg="Submit method need to be able to modify their owners",
)
fs.result() # Wait for the process to finish
self.assertFalse(
dynamic_42.running,
msg="Callback methods need to be able to modify their owners",
)
def test_exception(self):
"""
Exceptions from dynamically defined callables should get cleanly raised.
"""
@dynamic_foo()
def raise_error():
raise RuntimeError
re = raise_error()
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
fs = executor.submit(re.run)
with self.assertRaises(
RuntimeError,
msg="The callable just raises an error -- this should get shown to the user",
):
fs.result()
def test_return(self):
"""
We should be able to use a dynamic return value -- in this case, a
method of a dynamically defined class.
"""
@dynamic_foo()
def does_nothing():
return
@dynamic_foo()
def slowly_returns_dynamic():
"""
Returns a complex, dynamically defined variable
"""
sleep(0.1)
inside_variable = does_nothing()
inside_variable.result = "it was an inside job!"
return inside_variable
dynamic_dynamic = slowly_returns_dynamic()
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
fs = executor.submit(dynamic_dynamic.run)
self.assertIsInstance(
fs.result(),
Foo,
msg="Just a sanity check that we're getting the right type of dynamically "
"defined type of object",
)
self.assertEqual(
fs.result().result,
"it was an inside job!",
msg="The submit callable modifies the object that owns it, and this should"
"be reflected in the main process after deserialziation",
)
def test_timeout(self):
"""
Timeouts for dynamically defined callables should be handled ok.
"""
fortytwo = 42
@dynamic_foo()
def slow():
sleep(0.1)
return fortytwo
f = slow()
executor = SingleNodeExecutor(block_allocation=True, max_workers=1)
self.assertTrue(executor)
cloudpickle_register(ind=1)
fs = executor.submit(f.run)
self.assertEqual(
fs.result(timeout=30),
fortytwo,
msg="waiting long enough should get the result",
)
with self.assertRaises(
(TimeoutError, cfbTimeoutError),
msg="With a timeout time smaller than our submit callable's sleep time, "
"we had better get an exception!",
):
fs = executor.submit(f.run)
fs.result(timeout=0.0001)