langfun 0.1.2.dev202509210803__py3-none-any.whl → 0.1.2.dev202509230805__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langfun might be problematic. Click here for more details.

langfun/env/base_test.py CHANGED
@@ -11,21 +11,23 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- import concurrent.futures
15
14
  import contextlib
16
15
  import time
17
- from typing import Iterator
16
+ from typing import Any, Iterator, Type
18
17
  import unittest
19
18
 
20
19
  from langfun.env import base_environment
21
20
  from langfun.env import base_feature
22
21
  from langfun.env import base_sandbox
23
22
  from langfun.env import interface
23
+ import pyglove as pg
24
24
 
25
25
 
26
26
  class TestingEnvironment(base_environment.BaseEnvironment):
27
27
 
28
- ping_simulate_error: bool = False
28
+ simulate_start_error: Type[BaseException] | None = None
29
+ simulate_shutdown_error: Type[BaseException] | None = None
30
+ simulate_ping_error: Type[BaseException] | None = None
29
31
  keepalive_interval: float | None = 60.0
30
32
  offline: bool = False
31
33
 
@@ -33,21 +35,16 @@ class TestingEnvironment(base_environment.BaseEnvironment):
33
35
  def id(self) -> interface.EnvironmentId:
34
36
  return interface.EnvironmentId('testing-env')
35
37
 
36
- def set_offline(self, offline: bool) -> None:
37
- self.rebind(
38
- offline=offline, skip_notification=True, raise_on_no_change=False
39
- )
40
- for sandbox in self._sandbox_pool:
41
- sandbox.rebind(
42
- ping_simulate_error=offline,
43
- skip_notification=True,
44
- raise_on_no_change=False
45
- )
38
+ def wait_for_next_maintenance(self):
39
+ maintenance_count = self._maintenance_count
40
+ while self._maintenance_count == maintenance_count:
41
+ time.sleep(0.1)
46
42
 
47
43
  def _create_sandbox(
48
44
  self,
49
45
  sandbox_id: str,
50
- reusable: bool
46
+ reusable: bool,
47
+ proactive_session_setup: bool,
51
48
  ) -> interface.Sandbox:
52
49
  if self.offline:
53
50
  raise interface.EnvironmentError(
@@ -61,44 +58,91 @@ class TestingEnvironment(base_environment.BaseEnvironment):
61
58
  sandbox_id=sandbox_id
62
59
  ),
63
60
  reusable=reusable,
64
- ping_simulate_error=self.ping_simulate_error,
61
+ proactive_session_setup=proactive_session_setup,
62
+ simulate_start_error=self.simulate_start_error,
63
+ simulate_shutdown_error=self.simulate_shutdown_error,
64
+ simulate_ping_error=self.simulate_ping_error,
65
65
  keepalive_interval=self.keepalive_interval,
66
66
  )
67
67
 
68
68
 
69
69
  class TestingSandbox(base_sandbox.BaseSandbox):
70
70
 
71
- ping_simulate_error: bool = False
71
+ simulate_start_error: Type[BaseException] | None = None
72
+ simulate_shutdown_error: Type[BaseException] | None = None
73
+ simulate_ping_error: Type[BaseException] | None = None
72
74
 
73
75
  def _on_bound(self) -> None:
74
76
  super()._on_bound()
75
77
  self._shell_history = []
76
78
  self._ping_history = []
77
79
 
80
+ def _raise_error(self, message, error_type: Type[BaseException], **kwargs):
81
+ if (error_type is interface.SandboxStateError or
82
+ issubclass(error_type, interface.SandboxStateError)):
83
+ kwargs['sandbox'] = self
84
+ raise error_type(message, **kwargs)
85
+ else:
86
+ raise error_type(message)
87
+
88
+ def wait_until(
89
+ self,
90
+ status: interface.Sandbox.Status | tuple[interface.Sandbox.Status, ...]
91
+ ) -> None:
92
+ if not isinstance(status, tuple):
93
+ status = (status,)
94
+ while self.status not in status:
95
+ time.sleep(0.1)
96
+
97
+ def wait_until_not(
98
+ self,
99
+ status: interface.Sandbox.Status | tuple[interface.Sandbox.Status, ...]
100
+ ) -> None:
101
+ if not isinstance(status, tuple):
102
+ status = (status,)
103
+ while self.status in status:
104
+ time.sleep(0.1)
105
+
106
+ def wait_until_next_housekeep(self) -> None:
107
+ housekeep_count = self._housekeep_count
108
+ while self._housekeep_count == housekeep_count:
109
+ time.sleep(0.1)
110
+
111
+ def _start(self) -> None:
112
+ if self.simulate_start_error:
113
+ self._raise_error('Sandbox start error', self.simulate_start_error)
114
+ super()._start()
115
+
116
+ def _shutdown(self) -> None:
117
+ if self.simulate_shutdown_error:
118
+ self._raise_error('Sandbox shutdown error', self.simulate_shutdown_error)
119
+ super()._shutdown()
120
+
78
121
  @base_sandbox.sandbox_service(critical_errors=(RuntimeError,))
79
122
  def shell(
80
123
  self,
81
124
  code: str,
82
- must_succeed: bool = True,
125
+ raise_error: Type[BaseException] | None = None,
83
126
  ) -> str:
84
127
  self._shell_history.append(code)
85
- if 'bad' not in code:
86
- return f'shell {len(self._shell_history)} succeeded'
87
-
88
- message = f'shell {len(self._shell_history)} failed'
89
- if must_succeed:
90
- raise RuntimeError(message)
91
- raise ValueError(message)
128
+ if raise_error is not None:
129
+ self._raise_error(f'shell "{code}" failed', raise_error)
130
+ return f'shell "{code}" succeeded'
92
131
 
93
132
  def _ping(self) -> None:
94
- self._ping_history.append(not self.ping_simulate_error)
95
- if self.ping_simulate_error:
96
- raise interface.SandboxStateError(sandbox=self, code='ping')
133
+ self._ping_history.append(not self.simulate_ping_error)
134
+ if self.simulate_ping_error:
135
+ self._raise_error('Ping error', self.simulate_ping_error, code='ping')
97
136
 
98
137
 
99
138
  class TestingFeature(base_feature.BaseFeature):
100
139
  housekeep_interval = 0
101
- simulate_housekeep_error: bool = False
140
+ setup_session_delay: float = 0.0
141
+ simulate_housekeep_error: Type[BaseException] | None = None
142
+ simulate_setup_error: Type[BaseException] | None = None
143
+ simulate_teardown_error: Type[BaseException] | None = None
144
+ simulate_setup_session_error: Type[BaseException] | None = None
145
+ simulate_teardown_session_error: Type[BaseException] | None = None
102
146
 
103
147
  class Service:
104
148
  """Sandbox."""
@@ -106,14 +150,40 @@ class TestingFeature(base_feature.BaseFeature):
106
150
  def __init__(self, sandbox: interface.Sandbox):
107
151
  self._sandbox = sandbox
108
152
 
109
- def do(self, code: str):
110
- self._sandbox.shell(code)
153
+ def do(self, code: str, raise_error: Type[BaseException] | None = None):
154
+ self._sandbox.shell(code, raise_error=raise_error)
155
+
156
+ def _raise_error(self, message, error_type: Type[BaseException], **kwargs):
157
+ self._sandbox._raise_error(message, error_type, **kwargs)
111
158
 
112
159
  def _setup(self) -> None:
113
- self.sandbox.shell(f'echo "setup {self.name}"')
160
+ if self.simulate_setup_error:
161
+ self._raise_error(f'{self.name} setup error', self.simulate_setup_error)
162
+ self.sandbox.shell(f'"{self.name}" setup')
114
163
 
115
164
  def _teardown(self) -> None:
116
- self.sandbox.shell(f'echo "teardown {self.name}"')
165
+ if self.simulate_teardown_error:
166
+ self._raise_error(
167
+ f'{self.name} teardown error', self.simulate_teardown_error
168
+ )
169
+ self.sandbox.shell(f'"{self.name}" teardown')
170
+
171
+ def _setup_session(self) -> None:
172
+ if self.setup_session_delay > 0:
173
+ time.sleep(self.setup_session_delay)
174
+
175
+ if self.simulate_setup_session_error:
176
+ self._raise_error(
177
+ 'Feature session setup error', self.simulate_setup_session_error
178
+ )
179
+ self.sandbox.shell(f'"{self.name}" setup session')
180
+
181
+ def _teardown_session(self) -> None:
182
+ if self.simulate_teardown_session_error:
183
+ self._raise_error(
184
+ 'Feature session teardown error', self.simulate_teardown_session_error
185
+ )
186
+ self.sandbox.shell(f'"{self.name}" teardown session')
117
187
 
118
188
  @base_sandbox.sandbox_service()
119
189
  def num_shell_calls(self) -> None:
@@ -121,12 +191,17 @@ class TestingFeature(base_feature.BaseFeature):
121
191
 
122
192
  @base_sandbox.sandbox_service()
123
193
  def bad_shell_call(self) -> None:
124
- self.sandbox.shell('bad command')
194
+ self.sandbox.shell('bad command', raise_error=RuntimeError)
125
195
 
126
196
  @base_sandbox.sandbox_service()
127
197
  def show_session_id(self):
128
198
  return self.session_id
129
199
 
200
+ @base_sandbox.sandbox_service()
201
+ def call_with_varargs(self, code: str, *args, **kwargs):
202
+ del code, args, kwargs
203
+ return 0
204
+
130
205
  def _on_bound(self) -> None:
131
206
  super()._on_bound()
132
207
  self._service = None
@@ -147,21 +222,31 @@ class TestingFeature(base_feature.BaseFeature):
147
222
  )
148
223
 
149
224
 
150
- class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
225
+ class TestingEnvironmentEventHandler(
226
+ pg.Object, interface.EnvironmentEventHandler
227
+ ):
228
+ log_sandbox_status: bool = False
229
+ log_feature_setup: bool = True
230
+ log_session_setup: bool = False
151
231
 
152
- def __init__(self):
153
- self.history = []
232
+ def _on_bound(self) -> None:
233
+ super()._on_bound()
234
+ self._logs = []
235
+
236
+ @property
237
+ def logs(self) -> list[str]:
238
+ return self._logs
154
239
 
155
240
  def _add_message(self, message: str, error: Exception | None) -> None:
156
241
  """Adds a message to the history."""
157
242
  if error is None:
158
- self.history.append(message)
243
+ self._logs.append(message)
159
244
  else:
160
- self.history.append(f'{message} with error: {error}')
245
+ self._logs.append(f'{message} with {error.__class__.__name__}')
161
246
 
162
247
  def on_environment_start(
163
248
  self,
164
- environment: 'Environment',
249
+ environment: interface.Environment,
165
250
  error: Exception | None
166
251
  ) -> None:
167
252
  """Called when the environment is started."""
@@ -169,7 +254,7 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
169
254
 
170
255
  def on_environment_shutdown(
171
256
  self,
172
- environment: 'Environment',
257
+ environment: interface.Environment,
173
258
  error: Exception | None
174
259
  ) -> None:
175
260
  """Called when the environment is shutdown."""
@@ -184,6 +269,19 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
184
269
  del environment
185
270
  self._add_message(f'[{sandbox.id}] sandbox started', error)
186
271
 
272
+ def on_sandbox_status_change(
273
+ self,
274
+ environment: interface.Environment,
275
+ sandbox: interface.Sandbox,
276
+ old_status: interface.Sandbox.Status,
277
+ new_status: interface.Sandbox.Status,
278
+ ) -> None:
279
+ if self.log_sandbox_status:
280
+ self._add_message(
281
+ f'[{sandbox.id}] {old_status.value} -> {new_status.value}',
282
+ None,
283
+ )
284
+
187
285
  def on_sandbox_shutdown(
188
286
  self,
189
287
  environment: interface.Environment,
@@ -192,7 +290,7 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
192
290
  ) -> None:
193
291
  self._add_message(f'[{sandbox.id}] sandbox shutdown', error)
194
292
 
195
- def on_sandbox_feature_setup(
293
+ def on_feature_setup(
196
294
  self,
197
295
  environment: interface.Environment,
198
296
  sandbox: interface.Sandbox,
@@ -200,26 +298,56 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
200
298
  error: Exception | None
201
299
  ) -> None:
202
300
  """Called when a sandbox feature is setup."""
203
- self._add_message(
204
- f'[{sandbox.id}/{feature.name}] feature setup', error
205
- )
301
+ if self.log_feature_setup:
302
+ self._add_message(
303
+ f'[{sandbox.id}/{feature.name}] feature setup', error
304
+ )
206
305
 
207
- def on_sandbox_feature_teardown(
306
+ def on_feature_teardown(
208
307
  self,
209
- environment: 'Environment',
210
- sandbox: 'Sandbox',
211
- feature: 'Feature',
308
+ environment: interface.Environment,
309
+ sandbox: interface.Sandbox,
310
+ feature: interface.Feature,
212
311
  error: Exception | None
213
312
  ) -> None:
214
313
  """Called when a sandbox feature is teardown."""
215
- self._add_message(
216
- f'[{sandbox.id}/{feature.name}] feature teardown', error
217
- )
314
+ if self.log_feature_setup:
315
+ self._add_message(
316
+ f'[{sandbox.id}/{feature.name}] feature teardown', error
317
+ )
318
+
319
+ def on_feature_setup_session(
320
+ self,
321
+ environment: interface.Environment,
322
+ sandbox: interface.Sandbox,
323
+ feature: interface.Feature,
324
+ session_id: str | None,
325
+ error: Exception | None
326
+ ) -> None:
327
+ """Called when a sandbox feature is setup."""
328
+ if self.log_session_setup:
329
+ self._add_message(
330
+ f'[{sandbox.id}/{feature.name}] feature setup session', error
331
+ )
332
+
333
+ def on_feature_teardown_session(
334
+ self,
335
+ environment: interface.Environment,
336
+ sandbox: interface.Sandbox,
337
+ feature: interface.Feature,
338
+ session_id: str,
339
+ error: Exception | None
340
+ ) -> None:
341
+ """Called when a sandbox feature is teardown."""
342
+ if self.log_session_setup:
343
+ self._add_message(
344
+ f'[{sandbox.id}/{feature.name}] feature teardown session', error
345
+ )
218
346
 
219
- def on_sandbox_session_start(
347
+ def on_session_start(
220
348
  self,
221
- environment: 'Environment',
222
- sandbox: 'Sandbox',
349
+ environment: interface.Environment,
350
+ sandbox: interface.Sandbox,
223
351
  session_id: str,
224
352
  error: Exception | None
225
353
  ) -> None:
@@ -228,10 +356,10 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
228
356
  f'[{sandbox.id}] session {session_id!r} started', error
229
357
  )
230
358
 
231
- def on_sandbox_session_end(
359
+ def on_session_end(
232
360
  self,
233
- environment: 'Environment',
234
- sandbox: 'Sandbox',
361
+ environment: interface.Environment,
362
+ sandbox: interface.Sandbox,
235
363
  session_id: str,
236
364
  error: Exception | None
237
365
  ) -> None:
@@ -240,19 +368,42 @@ class TestingEnvironmentEventHandler(interface.EnvironmentEventHandler):
240
368
  f'[{sandbox.id}] session {session_id!r} ended', error
241
369
  )
242
370
 
371
+ def on_session_activity(
372
+ self,
373
+ session_id: str,
374
+ name: str,
375
+ environment: interface.Environment,
376
+ sandbox: interface.Sandbox,
377
+ feature: interface.Feature | None,
378
+ error: Exception | None,
379
+ *,
380
+ code: str | None = None,
381
+ **kwargs
382
+ ) -> None:
383
+ """Called when a sandbox activity is performed."""
384
+ del environment, kwargs
385
+ self._add_message(
386
+ f'[{sandbox.id}/{session_id}] {name}: {code}', error
387
+ )
388
+
389
+ #
390
+ # Tests
391
+ #
243
392
 
244
- class EnvironmentTest(unittest.TestCase):
245
393
 
246
- def test_environment_no_pooling_normal(self):
247
- event_handler = TestingEnvironmentEventHandler()
394
+ class EnvironmentTests(unittest.TestCase):
395
+
396
+ def test_basics(self):
248
397
  env = TestingEnvironment(
398
+ root_dir='/tmp',
249
399
  pool_size=0,
250
400
  features={'test_feature': TestingFeature()},
251
401
  outage_grace_period=1,
252
- event_handler=event_handler,
253
402
  outage_retry_interval=0,
254
403
  )
255
- self.assertFalse(env.is_alive)
404
+ self.assertIsNone(interface.Environment.current())
405
+ self.assertEqual(env.status, interface.Environment.Status.CREATED)
406
+ self.assertFalse(env.is_online)
256
407
  self.assertEqual(env.min_pool_size, 0)
257
408
  self.assertEqual(env.max_pool_size, 0)
258
409
  self.assertEqual(env.sandbox_pool, [])
@@ -264,311 +415,843 @@ class EnvironmentTest(unittest.TestCase):
264
415
  self.assertIsNone(env.start_time)
265
416
 
266
417
  with env:
267
- self.assertEqual(
268
- event_handler.history,
269
- [
270
- '[testing-env] environment started',
271
- ]
272
- )
418
+ self.assertEqual(env.status, interface.Environment.Status.ONLINE)
273
419
  self.assertIs(interface.Environment.current(), env)
274
- self.assertTrue(env.is_alive)
420
+ self.assertTrue(env.is_online)
275
421
  self.assertIsNotNone(env.start_time)
276
422
  self.assertEqual(env.offline_duration, 0.0)
277
423
  self.assertEqual(env.sandbox_pool, [])
278
- self.assertIsNone(env.working_dir)
424
+ self.assertEqual(env.working_dir, '/tmp/testing-env')
279
425
 
280
426
  with env.sandbox('session1') as sb:
281
- self.assertEqual(env.sandbox_pool, [])
282
- self.assertTrue(sb.is_alive)
283
- self.assertTrue(sb.is_busy)
284
- self.assertEqual(sb.session_id, 'session1')
285
- self.assertIsNone(sb.working_dir)
286
427
  self.assertEqual(
287
428
  sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
288
429
  )
289
- sb.shell('echo "foo"')
290
- self.assertIsInstance(sb.test_feature, TestingFeature)
291
- self.assertEqual(sb.test_feature.session_id, 'session1')
430
+ self.assertEqual(sb.session_id, 'session1')
431
+ self.assertEqual(sb.working_dir, '/tmp/testing-env/0')
432
+ self.assertTrue(sb.is_online)
433
+ self.assertIs(sb.test_feature, sb.features['test_feature'])
292
434
  with self.assertRaises(AttributeError):
293
435
  _ = sb.test_feature2
294
- self.assertIsNone(sb.test_feature._service)
295
- with sb.test_feature.my_service() as service:
296
- self.assertIs(sb.test_feature._service, service)
297
- self.assertIsNone(sb.test_feature._service)
298
- self.assertEqual(sb.session_id, 'session1')
436
+ self.assertFalse(sb.is_online)
299
437
 
300
- with env.sandbox('session2') as sb:
301
- self.assertEqual(env.sandbox_pool, [])
302
- self.assertTrue(sb.is_alive)
303
- self.assertTrue(sb.is_busy)
304
- self.assertEqual(sb.session_id, 'session2')
305
- self.assertEqual(
306
- sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='1')
307
- )
308
- sb.shell('echo "bar"')
309
-
310
- # Access the feature from the environment without obtaining a sandbox.
311
- self.assertEqual(
312
- env.test_feature.num_shell_calls(session_id='num_shell_session'), 1
313
- )
438
+ self.assertIsInstance(env.test_feature, TestingFeature)
314
439
  with self.assertRaises(AttributeError):
315
440
  _ = env.test_feature2
316
- # Access the feature from the environment without obtaining a sandbox.
317
- with env.test_feature.my_service(
318
- session_id='my_service_session'
319
- ) as service:
320
- service.do('echo "baz"')
321
441
 
322
- self.assertFalse(env.is_alive)
323
- self.assertIsNone(interface.Environment.current())
324
- self.assertEqual(
325
- event_handler.history,
326
- [
327
- '[testing-env] environment started',
328
- '[testing-env/0/test_feature] feature setup',
329
- '[testing-env/0] sandbox started',
330
- '[testing-env/0] session \'session1\' started',
331
- '[testing-env/0] session \'session1\' ended',
332
- '[testing-env/0/test_feature] feature teardown',
333
- '[testing-env/0] sandbox shutdown',
334
- '[testing-env/1/test_feature] feature setup',
335
- '[testing-env/1] sandbox started',
336
- '[testing-env/1] session \'session2\' started',
337
- '[testing-env/1] session \'session2\' ended',
338
- '[testing-env/1/test_feature] feature teardown',
339
- '[testing-env/1] sandbox shutdown',
340
- '[testing-env/2/test_feature] feature setup',
341
- '[testing-env/2] sandbox started',
342
- '[testing-env/2] session \'num_shell_session\' started',
343
- '[testing-env/2] session \'num_shell_session\' ended',
344
- '[testing-env/2/test_feature] feature teardown',
345
- '[testing-env/2] sandbox shutdown',
346
- '[testing-env/3/test_feature] feature setup',
347
- '[testing-env/3] sandbox started',
348
- '[testing-env/3] session \'my_service_session\' started',
349
- '[testing-env/3] session \'my_service_session\' ended',
350
- '[testing-env/3/test_feature] feature teardown',
351
- '[testing-env/3] sandbox shutdown',
352
- '[testing-env] environment shutdown',
353
- ]
442
+ def test_acquire_env_offline(self):
443
+ env = TestingEnvironment(
444
+ features={'test_feature': TestingFeature()},
445
+ pool_size=0,
446
+ outage_grace_period=1,
447
+ outage_retry_interval=0,
448
+ keepalive_interval=0,
449
+ stats_report_interval=1,
354
450
  )
451
+ with self.assertRaises(interface.EnvironmentOutageError):
452
+ env.acquire()
355
453
 
356
- def test_environment_no_pooling_state_error(self):
357
- event_handler = TestingEnvironmentEventHandler()
454
+ def test_acquire_no_pooling(self):
358
455
  env = TestingEnvironment(
359
- root_dir='/tmp',
360
- pool_size=0,
361
456
  features={'test_feature': TestingFeature()},
457
+ pool_size=0,
362
458
  outage_grace_period=1,
363
459
  outage_retry_interval=0,
364
- event_handler=event_handler,
460
+ keepalive_interval=0,
461
+ stats_report_interval=1,
365
462
  )
366
- self.assertEqual(env.working_dir, '/tmp/testing-env')
367
463
  with env:
368
- with env.sandbox('session1') as sb:
369
- self.assertEqual(env.sandbox_pool, [])
370
- self.assertTrue(sb.is_alive)
371
- self.assertTrue(sb.is_busy)
372
- self.assertFalse(sb.is_pending)
373
- self.assertEqual(sb.session_id, 'session1')
374
- self.assertEqual(sb.working_dir, '/tmp/testing-env/0')
375
- self.assertEqual(
376
- sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
377
- )
378
- # Non-critical error.
379
- with self.assertRaises(ValueError):
380
- sb.shell('bad command', must_succeed=False)
464
+ sb = env.acquire()
465
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
381
466
 
382
- # Critical error.
383
- with sb.test_feature.my_service() as service:
384
- with self.assertRaises(interface.SandboxStateError):
385
- service.do('bad command')
386
- self.assertFalse(sb.is_alive)
467
+ def test_acquire_no_pooling_with_error(self):
468
+ env = TestingEnvironment(
469
+ features={
470
+ 'test_feature': TestingFeature(
471
+ simulate_setup_error=interface.SandboxStateError
472
+ )
473
+ },
474
+ pool_size=0,
475
+ outage_grace_period=1,
476
+ outage_retry_interval=0,
477
+ keepalive_interval=0,
478
+ stats_report_interval=1,
479
+ )
480
+ with env:
481
+ with self.assertRaises(interface.EnvironmentOutageError):
482
+ env.acquire()
387
483
 
388
- with self.assertRaises(interface.SandboxStateError):
389
- env.test_feature.bad_shell_call(session_id='bad_shell_session')
484
+ def test_acquire_with_pooling(self):
485
+ env = TestingEnvironment(
486
+ features={'test_feature': TestingFeature()},
487
+ pool_size=1,
488
+ outage_grace_period=1,
489
+ outage_retry_interval=0,
490
+ keepalive_interval=0,
491
+ stats_report_interval=1,
492
+ )
493
+ with env:
494
+ sb = env.acquire()
495
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
390
496
 
391
- # Access the feature from the environment without obtaining a sandbox.
392
- with env.test_feature.my_service(
393
- session_id='my_service_session'
394
- ) as service:
395
- with self.assertRaises(interface.SandboxStateError):
396
- service.do('bad command')
497
+ def test_acquire_with_pooling_overload(self):
498
+ env = TestingEnvironment(
499
+ features={'test_feature': TestingFeature()},
500
+ pool_size=1,
501
+ outage_grace_period=1,
502
+ outage_retry_interval=0,
503
+ keepalive_interval=0,
504
+ stats_report_interval=1,
505
+ )
506
+ with env:
507
+ sb = env.acquire()
508
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
509
+ with self.assertRaises(interface.EnvironmentOverloadError):
510
+ env.acquire()
397
511
 
398
- self.assertFalse(env.is_alive)
399
- self.assertIsNone(interface.Environment.current())
400
- self.assertEqual(
401
- event_handler.history,
402
- [
403
- '[testing-env] environment started',
404
- '[testing-env/0/test_feature] feature setup',
405
- '[testing-env/0] sandbox started',
406
- '[testing-env/0] session \'session1\' started',
407
- # Sandbox shutdown is triggered by the SandboxStateError before
408
- # the session end event is triggered.
409
- '[testing-env/0/test_feature] feature teardown',
410
- '[testing-env/0] sandbox shutdown',
411
- '[testing-env/0] session \'session1\' ended',
412
- '[testing-env/1/test_feature] feature setup',
413
- '[testing-env/1] sandbox started',
414
- "[testing-env/1] session 'bad_shell_session' started",
415
- '[testing-env/1/test_feature] feature teardown',
416
- '[testing-env/1] sandbox shutdown',
417
- "[testing-env/1] session 'bad_shell_session' ended",
418
- '[testing-env/2/test_feature] feature setup',
419
- '[testing-env/2] sandbox started',
420
- "[testing-env/2] session 'my_service_session' started",
421
- '[testing-env/2/test_feature] feature teardown',
422
- '[testing-env/2] sandbox shutdown',
423
- "[testing-env/2] session 'my_service_session' ended",
424
- '[testing-env] environment shutdown',
425
- ]
512
+ def test_acquire_with_growing_pool(self):
513
+ env = TestingEnvironment(
514
+ features={'test_feature': TestingFeature()},
515
+ pool_size=(1, 3),
516
+ outage_grace_period=1,
517
+ outage_retry_interval=0,
518
+ keepalive_interval=0,
519
+ stats_report_interval=1,
426
520
  )
521
+ with env:
522
+ self.assertEqual(len(env.sandbox_pool), 1)
523
+ sb = env.acquire()
524
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
525
+ self.assertEqual(len(env.sandbox_pool), 1)
526
+ sb2 = env.acquire()
527
+ self.assertEqual(sb2.status, interface.Sandbox.Status.ACQUIRED)
528
+ self.assertEqual(len(env.sandbox_pool), 2)
427
529
 
428
- def test_environment_with_pooling_normal(self):
429
- event_handler = TestingEnvironmentEventHandler()
530
+ def test_acquire_with_growing_pool_failure(self):
430
531
  env = TestingEnvironment(
431
- pool_size=(1, 2),
432
532
  features={'test_feature': TestingFeature()},
433
- event_handler=event_handler,
434
- # To make pool operation deterministic in the test.
435
- pool_operation_max_parallelism=1,
533
+ pool_size=(1, 3),
436
534
  outage_grace_period=1,
437
535
  outage_retry_interval=0,
536
+ keepalive_interval=0,
537
+ stats_report_interval=1,
438
538
  )
439
- self.assertFalse(env.is_alive)
440
- self.assertEqual(env.min_pool_size, 1)
441
- self.assertEqual(env.max_pool_size, 2)
442
- self.assertEqual(env.sandbox_pool, [])
443
- self.assertEqual(env.id, interface.EnvironmentId('testing-env'))
444
- self.assertEqual(env.outage_grace_period, 1)
445
- self.assertEqual(env.stats_report_interval, 60)
446
- self.assertEqual(env.features['test_feature'].name, 'test_feature')
539
+ with env:
540
+ self.assertEqual(len(env.sandbox_pool), 1)
541
+ sb = env.acquire()
542
+ self.assertEqual(sb.status, interface.Sandbox.Status.ACQUIRED)
447
543
 
448
- self.assertIsNone(env.start_time)
544
+ # Make future sandbox setup to fail.
545
+ env.features.test_feature.rebind(
546
+ simulate_setup_error=interface.SandboxStateError,
547
+ skip_notification=True
548
+ )
549
+ with self.assertRaises(interface.EnvironmentOutageError):
550
+ env.acquire()
551
+
552
+ def test_maintenance_error(self):
553
+ env = TestingEnvironment(
554
+ features={'test_feature': TestingFeature()},
555
+ pool_size=1,
556
+ proactive_session_setup=True,
557
+ outage_grace_period=1,
558
+ outage_retry_interval=0,
559
+ keepalive_interval=0,
560
+ stats_report_interval=1,
561
+ )
562
+ with env:
563
+ self.assertEqual(len(env.sandbox_pool), 1)
564
+ self.assertEqual(
565
+ env.sandbox_pool[0].status, interface.Sandbox.Status.READY
566
+ )
567
+ # Make future sandbox setup to fail.
568
+ env.features.test_feature.rebind(
569
+ simulate_setup_error=interface.SandboxStateError,
570
+ skip_notification=True
571
+ )
572
+ with env.sandbox() as sb:
573
+ with self.assertRaises(interface.SandboxStateError):
574
+ sb.shell('bad command', raise_error=interface.SandboxStateError)
575
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
576
+ env.wait_for_next_maintenance()
577
+ self.assertFalse(env.is_online)
578
+
579
+
580
+ class SandboxStatusTests(unittest.TestCase):
449
581
 
582
+ def setUp(self):
583
+ super().setUp()
584
+ self.event_handler = TestingEnvironmentEventHandler(
585
+ log_sandbox_status=True,
586
+ log_feature_setup=True,
587
+ log_session_setup=True,
588
+ )
589
+ self.maxDiff = None
590
+
591
+ def _create_env(
592
+ self,
593
+ features,
594
+ *,
595
+ pool_size=0,
596
+ **kwargs
597
+ ) -> TestingEnvironment:
598
+ return TestingEnvironment(
599
+ pool_size=pool_size,
600
+ features=features,
601
+ outage_grace_period=0,
602
+ event_handlers=[self.event_handler],
603
+ outage_retry_interval=0,
604
+ **kwargs
605
+ )
606
+
607
+ def test_passive_session_setup(self):
608
+ env = self._create_env(
609
+ features={
610
+ 'feature1': TestingFeature(),
611
+ 'feature2': TestingFeature(),
612
+ },
613
+ )
450
614
  with env:
615
+ with env.sandbox('session1') as sb:
616
+ sb.shell('echo "hello"')
451
617
  self.assertEqual(
452
- event_handler.history,
618
+ self.event_handler.logs,
453
619
  [
454
- '[testing-env/0/test_feature] feature setup',
455
- '[testing-env/0] sandbox started',
456
620
  '[testing-env] environment started',
621
+ '[testing-env/0/feature1] feature setup',
622
+ '[testing-env/0/feature2] feature setup',
623
+ '[testing-env/0] created -> ready',
624
+ '[testing-env/0] sandbox started',
625
+ '[testing-env/0] ready -> acquired',
626
+ '[testing-env/0] acquired -> setting_up',
627
+ '[testing-env/0/session1] shell: "feature1" setup session',
628
+ '[testing-env/0/feature1] feature setup session',
629
+ '[testing-env/0/session1] shell: "feature2" setup session',
630
+ '[testing-env/0/feature2] feature setup session',
631
+ '[testing-env/0] setting_up -> in_session',
632
+ "[testing-env/0] session 'session1' started",
633
+ '[testing-env/0/session1] shell: echo "hello"',
634
+ '[testing-env/0/session1] shell: "feature1" teardown session',
635
+ '[testing-env/0/feature1] feature teardown session',
636
+ '[testing-env/0/session1] shell: "feature2" teardown session',
637
+ '[testing-env/0/feature2] feature teardown session',
638
+ "[testing-env/0] session 'session1' ended",
639
+ '[testing-env/0] in_session -> acquired',
640
+ '[testing-env/0] acquired -> shutting_down',
641
+ '[testing-env/0/feature1] feature teardown',
642
+ '[testing-env/0/feature2] feature teardown',
643
+ '[testing-env/0] shutting_down -> offline',
644
+ '[testing-env/0] sandbox shutdown'
457
645
  ]
458
646
  )
459
- self.assertIs(interface.Environment.current(), env)
460
- self.assertEqual(len(env.sandbox_pool), 1)
461
- self.assertTrue(env.sandbox_pool[0].is_alive)
462
- self.assertFalse(env.sandbox_pool[0].is_busy)
463
- self.assertFalse(env.sandbox_pool[0].is_pending)
464
647
 
465
- # Use the pooled server.
648
+ def test_proactive_session_setup(self):
649
+ env = self._create_env(
650
+ features={
651
+ 'feature1': TestingFeature(setup_session_delay=0.1),
652
+ 'feature2': TestingFeature(),
653
+ },
654
+ pool_size=1,
655
+ proactive_session_setup=True,
656
+ )
657
+ with env:
466
658
  with env.sandbox('session1') as sb:
467
- self.assertEqual(len(env.sandbox_pool), 1)
468
- self.assertTrue(sb.is_alive)
469
- self.assertFalse(sb.is_pending)
470
- self.assertTrue(sb.is_busy)
471
- self.assertEqual(sb.session_id, 'session1')
472
- self.assertEqual(
473
- sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
474
- )
475
- sb.shell('echo "foo"')
476
-
477
- self.assertEqual(len(env.sandbox_pool), 1)
478
-
479
- # Reuse the pooled server.
480
- with env.sandbox('session2') as sb2:
481
- self.assertEqual(len(env.sandbox_pool), 1)
482
- self.assertTrue(sb2.is_alive)
483
- self.assertTrue(sb2.is_busy)
484
- self.assertEqual(sb2.session_id, 'session2')
485
- self.assertEqual(
486
- sb2.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
487
- )
488
- sb2.shell('echo "bar"')
489
-
490
- # Dynamically bring up a new server in the pool.
491
- with env.sandbox('session3') as sb3:
492
- self.assertEqual(len(env.sandbox_pool), 2)
493
- self.assertTrue(sb3.is_alive)
494
- self.assertFalse(sb3.is_pending)
495
- self.assertTrue(sb3.is_busy)
496
- self.assertEqual(sb3.session_id, 'session3')
497
- self.assertEqual(
498
- sb3.id, interface.SandboxId(environment_id=env.id, sandbox_id='1')
499
- )
500
- sb3.shell('echo "baz"')
501
-
502
- self.assertEqual(
503
- env.stats(),
504
- dict(
505
- sandbox=dict(
506
- num_total=2,
507
- num_busy=2,
508
- num_free=0,
509
- num_dead=0,
510
- )
511
- )
659
+ sb.shell('echo "hello"')
660
+ sb.wait_until_not(
661
+ (
662
+ interface.Sandbox.Status.IN_SESSION,
663
+ interface.Sandbox.Status.SETTING_UP
512
664
  )
665
+ )
666
+ self.assertEqual(sb.status, interface.Sandbox.Status.READY)
667
+ self.assertEqual(
668
+ self.event_handler.logs,
669
+ [
670
+ '[testing-env/0/feature1] feature setup',
671
+ '[testing-env/0/feature2] feature setup',
672
+ '[testing-env/0/feature1] feature setup session',
673
+ '[testing-env/0/feature2] feature setup session',
674
+ '[testing-env/0] created -> ready',
675
+ '[testing-env/0] sandbox started',
676
+ '[testing-env] environment started',
677
+ '[testing-env/0] ready -> acquired',
678
+ '[testing-env/0] acquired -> setting_up',
679
+ '[testing-env/0] setting_up -> in_session',
680
+ "[testing-env/0] session 'session1' started",
681
+ '[testing-env/0/session1] shell: echo "hello"',
682
+ '[testing-env/0/session1] shell: "feature1" teardown session',
683
+ '[testing-env/0/feature1] feature teardown session',
684
+ '[testing-env/0/session1] shell: "feature2" teardown session',
685
+ '[testing-env/0/feature2] feature teardown session',
686
+ "[testing-env/0] session 'session1' ended",
687
+ '[testing-env/0] in_session -> setting_up',
688
+ '[testing-env/0/feature1] feature setup session',
689
+ '[testing-env/0/feature2] feature setup session',
690
+ '[testing-env/0] setting_up -> ready'
691
+ ]
692
+ )
513
693
 
514
- # Environment overloaded as all pooled servers are busy, and there
515
- # is no more quota to bring up new servers.
516
- with self.assertRaises(interface.EnvironmentOverloadError):
517
- with env.sandbox('session4'):
518
- pass
519
-
520
- self.assertEqual(
521
- env.stats(),
522
- dict(
523
- sandbox=dict(
524
- num_total=2,
525
- num_busy=1,
526
- num_free=1,
527
- num_dead=0,
528
- )
529
- )
694
+ def test_practive_session_setup_with_setup_session_error(self):
695
+ env = self._create_env(
696
+ features={'test_feature': TestingFeature(setup_session_delay=0.5)},
697
+ pool_size=1,
698
+ )
699
+ event_handler = TestingEnvironmentEventHandler(
700
+ log_sandbox_status=True,
701
+ log_feature_setup=True,
702
+ log_session_setup=True,
703
+ )
704
+ with env:
705
+ with env.sandbox('session1') as sb:
706
+ sb.add_event_handler(event_handler)
707
+ sb.test_feature.rebind(
708
+ simulate_setup_session_error=interface.SandboxStateError,
709
+ skip_notification=True
530
710
  )
711
+ sb.wait_until_not(
712
+ (
713
+ interface.Sandbox.Status.SETTING_UP,
714
+ interface.Sandbox.Status.SHUTTING_DOWN
715
+ )
716
+ )
717
+ self.assertEqual(len(sb.state_errors), 1)
718
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
719
+ self.assertEqual(
720
+ event_handler.logs,
721
+ [
722
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
723
+ '[testing-env/0/test_feature] feature teardown session',
724
+ "[testing-env/0] session 'session1' ended",
725
+ '[testing-env/0] in_session -> setting_up',
726
+ '[testing-env/0/test_feature] feature setup session with SandboxStateError', # pylint: disable=line-too-long
727
+ '[testing-env/0] setting_up -> shutting_down',
728
+ '[testing-env/0/test_feature] feature teardown',
729
+ '[testing-env/0] shutting_down -> offline',
730
+ '[testing-env/0] sandbox shutdown'
731
+ ]
732
+ )
531
733
 
532
- # Access the feature from the environment without obtaining a sandbox.
533
- self.assertEqual(
534
- env.test_feature.num_shell_calls(session_id='num_shell_session'), 2
535
- )
734
+ def test_sandbox_start_non_state_error(self):
735
+ env = self._create_env(
736
+ features={
737
+ 'feature1': TestingFeature(),
738
+ 'feature2': TestingFeature(),
739
+ },
740
+ simulate_start_error=ValueError,
741
+ )
742
+ with env:
743
+ with self.assertRaises(ValueError):
744
+ with env.sandbox('session1'):
745
+ pass
746
+ self.assertTrue(env.is_online)
747
+ self.assertEqual(
748
+ self.event_handler.logs,
749
+ [
750
+ '[testing-env] environment started',
751
+ '[testing-env/0] sandbox started with ValueError',
752
+ '[testing-env/0] created -> shutting_down',
753
+ '[testing-env/0] shutting_down -> offline',
754
+ '[testing-env/0] sandbox shutdown'
755
+ ]
756
+ )
536
757
 
537
- self.assertFalse(env.is_alive)
538
- self.assertEqual(env.sandbox_pool, [])
539
- self.assertIsNone(interface.Environment.current())
758
+ def test_sandbox_start_state_error(self):
759
+ env = self._create_env(
760
+ features={
761
+ 'feature1': TestingFeature(),
762
+ 'feature2': TestingFeature(),
763
+ },
764
+ pool_size=1,
765
+ simulate_start_error=interface.SandboxStateError,
766
+ )
767
+ with self.assertRaises(interface.EnvironmentOutageError):
768
+ with env:
769
+ pass
540
770
  self.assertEqual(
541
- event_handler.history,
771
+ self.event_handler.logs,
542
772
  [
543
- '[testing-env/0/test_feature] feature setup',
773
+ '[testing-env/0] sandbox started with SandboxStateError',
774
+ '[testing-env/0] created -> shutting_down',
775
+ '[testing-env/0] shutting_down -> offline',
776
+ '[testing-env/0] sandbox shutdown',
777
+ '[testing-env] environment started with EnvironmentOutageError',
778
+ '[testing-env] environment shutdown'
779
+ ]
780
+ )
781
+
782
+ def test_sandbox_shutdown_non_state_error(self):
783
+ env = self._create_env(
784
+ features={
785
+ 'feature1': TestingFeature(),
786
+ 'feature2': TestingFeature(),
787
+ },
788
+ simulate_shutdown_error=ValueError,
789
+ )
790
+ with env:
791
+ with self.assertRaises(ValueError):
792
+ with env.sandbox('session1') as sb:
793
+ sb.shell('echo "hello"')
794
+ self.assertEqual(len(sb.state_errors), 0)
795
+
796
+ self.assertEqual(
797
+ self.event_handler.logs,
798
+ [
799
+ '[testing-env] environment started',
800
+ '[testing-env/0/feature1] feature setup',
801
+ '[testing-env/0/feature2] feature setup',
802
+ '[testing-env/0] created -> ready',
803
+ '[testing-env/0] sandbox started',
804
+ '[testing-env/0] ready -> acquired',
805
+ '[testing-env/0] acquired -> setting_up',
806
+ '[testing-env/0/session1] shell: "feature1" setup session',
807
+ '[testing-env/0/feature1] feature setup session',
808
+ '[testing-env/0/session1] shell: "feature2" setup session',
809
+ '[testing-env/0/feature2] feature setup session',
810
+ '[testing-env/0] setting_up -> in_session',
811
+ "[testing-env/0] session 'session1' started",
812
+ '[testing-env/0/session1] shell: echo "hello"',
813
+ '[testing-env/0/session1] shell: "feature1" teardown session',
814
+ '[testing-env/0/feature1] feature teardown session',
815
+ '[testing-env/0/session1] shell: "feature2" teardown session',
816
+ '[testing-env/0/feature2] feature teardown session',
817
+ "[testing-env/0] session 'session1' ended",
818
+ '[testing-env/0] in_session -> acquired',
819
+ '[testing-env/0] acquired -> shutting_down',
820
+ '[testing-env/0/feature1] feature teardown',
821
+ '[testing-env/0/feature2] feature teardown',
822
+ '[testing-env/0] shutting_down -> offline',
823
+ '[testing-env/0] sandbox shutdown with ValueError',
824
+ '[testing-env] environment shutdown'
825
+ ]
826
+ )
827
+
828
+ def test_env_shutdown_non_state_error(self):
829
+ env = self._create_env(
830
+ pool_size=1,
831
+ features={
832
+ 'feature1': TestingFeature(),
833
+ 'feature2': TestingFeature(),
834
+ },
835
+ simulate_shutdown_error=ValueError,
836
+ )
837
+ with self.assertRaises(ValueError):
838
+ with env:
839
+ pass
840
+
841
+ self.assertEqual(
842
+ self.event_handler.logs,
843
+ [
844
+ '[testing-env/0/feature1] feature setup',
845
+ '[testing-env/0/feature2] feature setup',
846
+ '[testing-env/0/feature1] feature setup session',
847
+ '[testing-env/0/feature2] feature setup session',
848
+ '[testing-env/0] created -> ready',
544
849
  '[testing-env/0] sandbox started',
545
- # Environment ready is after the first sandbox is started.
546
850
  '[testing-env] environment started',
547
- '[testing-env/0] session \'session1\' started',
548
- '[testing-env/0] session \'session1\' ended',
549
- '[testing-env/0] session \'session2\' started',
550
- '[testing-env/1/test_feature] feature setup',
551
- '[testing-env/1] sandbox started',
552
- '[testing-env/1] session \'session3\' started',
553
- '[testing-env/1] session \'session3\' ended',
554
- '[testing-env/1] session \'num_shell_session\' started',
555
- '[testing-env/1] session \'num_shell_session\' ended',
556
- '[testing-env/0] session \'session2\' ended',
557
- '[testing-env/0/test_feature] feature teardown',
558
- '[testing-env/0] sandbox shutdown',
559
- '[testing-env/1/test_feature] feature teardown',
560
- '[testing-env/1] sandbox shutdown',
561
- '[testing-env] environment shutdown',
851
+ '[testing-env/0] ready -> shutting_down',
852
+ '[testing-env/0/feature1] feature teardown',
853
+ '[testing-env/0/feature2] feature teardown',
854
+ '[testing-env/0] shutting_down -> offline',
855
+ '[testing-env/0] sandbox shutdown with ValueError',
856
+ '[testing-env] environment shutdown with ValueError'
562
857
  ]
563
858
  )
564
859
 
860
+ def test_sandbox_shutdown_state_error(self):
861
+ env = self._create_env(
862
+ features={
863
+ 'feature1': TestingFeature(),
864
+ 'feature2': TestingFeature(),
865
+ },
866
+ simulate_shutdown_error=interface.SandboxStateError,
867
+ )
868
+ with env:
869
+ with env.sandbox('session1') as sb:
870
+ sb.shell('echo "hello"')
871
+ self.assertEqual(len(sb.state_errors), 1)
872
+ self.assertEqual(
873
+ self.event_handler.logs,
874
+ [
875
+ '[testing-env] environment started',
876
+ '[testing-env/0/feature1] feature setup',
877
+ '[testing-env/0/feature2] feature setup',
878
+ '[testing-env/0] created -> ready',
879
+ '[testing-env/0] sandbox started',
880
+ '[testing-env/0] ready -> acquired',
881
+ '[testing-env/0] acquired -> setting_up',
882
+ '[testing-env/0/session1] shell: "feature1" setup session',
883
+ '[testing-env/0/feature1] feature setup session',
884
+ '[testing-env/0/session1] shell: "feature2" setup session',
885
+ '[testing-env/0/feature2] feature setup session',
886
+ '[testing-env/0] setting_up -> in_session',
887
+ "[testing-env/0] session 'session1' started",
888
+ '[testing-env/0/session1] shell: echo "hello"',
889
+ '[testing-env/0/session1] shell: "feature1" teardown session',
890
+ '[testing-env/0/feature1] feature teardown session',
891
+ '[testing-env/0/session1] shell: "feature2" teardown session',
892
+ '[testing-env/0/feature2] feature teardown session',
893
+ "[testing-env/0] session 'session1' ended",
894
+ '[testing-env/0] in_session -> acquired',
895
+ '[testing-env/0] acquired -> shutting_down',
896
+ '[testing-env/0/feature1] feature teardown',
897
+ '[testing-env/0/feature2] feature teardown',
898
+ '[testing-env/0] shutting_down -> offline',
899
+ '[testing-env/0] sandbox shutdown with SandboxStateError',
900
+ '[testing-env] environment shutdown'
901
+ ]
902
+ )
903
+
904
+ def test_feature_setup_non_state_error(self):
905
+ env = self._create_env(
906
+ features={
907
+ 'feature1': TestingFeature(),
908
+ 'feature2': TestingFeature(
909
+ simulate_setup_error=ValueError
910
+ ),
911
+ },
912
+ )
913
+ with env:
914
+ with self.assertRaises(ValueError):
915
+ with env.sandbox('session1'):
916
+ pass
917
+ self.assertEqual(
918
+ self.event_handler.logs,
919
+ [
920
+ '[testing-env] environment started',
921
+ '[testing-env/0/feature1] feature setup',
922
+ '[testing-env/0/feature2] feature setup with ValueError',
923
+ '[testing-env/0] sandbox started with ValueError',
924
+ '[testing-env/0] created -> shutting_down',
925
+ '[testing-env/0/feature1] feature teardown',
926
+ '[testing-env/0/feature2] feature teardown',
927
+ '[testing-env/0] shutting_down -> offline',
928
+ '[testing-env/0] sandbox shutdown'
929
+ ]
930
+ )
931
+
932
+ def test_feature_setup_state_error(self):
933
+ env = self._create_env(
934
+ features={
935
+ 'feature1': TestingFeature(
936
+ simulate_setup_error=interface.SandboxStateError
937
+ ),
938
+ 'feature2': TestingFeature(),
939
+ },
940
+ )
941
+ with env:
942
+ with self.assertRaises(interface.EnvironmentOutageError):
943
+ with env.sandbox('session1'):
944
+ pass
945
+ self.assertEqual(
946
+ self.event_handler.logs,
947
+ [
948
+ '[testing-env] environment started',
949
+ '[testing-env/0/feature1] feature setup with SandboxStateError',
950
+ '[testing-env/0] sandbox started with SandboxStateError',
951
+ '[testing-env/0] created -> shutting_down',
952
+ '[testing-env/0/feature1] feature teardown',
953
+ '[testing-env/0] shutting_down -> offline',
954
+ '[testing-env/0] sandbox shutdown',
955
+ '[testing-env] environment shutdown'
956
+ ]
957
+ )
958
+
959
+ def test_feature_teardown_non_state_error(self):
960
+ env = self._create_env(
961
+ features={
962
+ 'feature1': TestingFeature(),
963
+ 'feature2': TestingFeature(
964
+ simulate_teardown_error=ValueError
965
+ ),
966
+ },
967
+ )
968
+ with env:
969
+ with self.assertRaises(interface.FeatureTeardownError):
970
+ with env.sandbox('session1'):
971
+ pass
972
+ self.assertEqual(
973
+ self.event_handler.logs,
974
+ [
975
+ '[testing-env] environment started',
976
+ '[testing-env/0/feature1] feature setup',
977
+ '[testing-env/0/feature2] feature setup',
978
+ '[testing-env/0] created -> ready',
979
+ '[testing-env/0] sandbox started',
980
+ '[testing-env/0] ready -> acquired',
981
+ '[testing-env/0] acquired -> setting_up',
982
+ '[testing-env/0/session1] shell: "feature1" setup session',
983
+ '[testing-env/0/feature1] feature setup session',
984
+ '[testing-env/0/session1] shell: "feature2" setup session',
985
+ '[testing-env/0/feature2] feature setup session',
986
+ '[testing-env/0] setting_up -> in_session',
987
+ "[testing-env/0] session 'session1' started",
988
+ '[testing-env/0/session1] shell: "feature1" teardown session',
989
+ '[testing-env/0/feature1] feature teardown session',
990
+ '[testing-env/0/session1] shell: "feature2" teardown session',
991
+ '[testing-env/0/feature2] feature teardown session',
992
+ "[testing-env/0] session 'session1' ended",
993
+ '[testing-env/0] in_session -> acquired',
994
+ '[testing-env/0] acquired -> shutting_down',
995
+ '[testing-env/0/feature1] feature teardown',
996
+ '[testing-env/0/feature2] feature teardown with ValueError',
997
+ '[testing-env/0] shutting_down -> offline',
998
+ '[testing-env/0] sandbox shutdown with FeatureTeardownError',
999
+ ]
1000
+ )
1001
+
1002
+ def test_feature_teardown_state_error(self):
1003
+ env = self._create_env(
1004
+ features={
1005
+ 'feature1': TestingFeature(
1006
+ simulate_teardown_error=interface.SandboxStateError
1007
+ ),
1008
+ 'feature2': TestingFeature(
1009
+ ),
1010
+ },
1011
+ )
1012
+ with env:
1013
+ with env.sandbox('session1') as sb:
1014
+ pass
1015
+ self.assertEqual(len(sb.state_errors), 1)
1016
+ self.assertEqual(
1017
+ self.event_handler.logs,
1018
+ [
1019
+ '[testing-env] environment started',
1020
+ '[testing-env/0/feature1] feature setup',
1021
+ '[testing-env/0/feature2] feature setup',
1022
+ '[testing-env/0] created -> ready',
1023
+ '[testing-env/0] sandbox started',
1024
+ '[testing-env/0] ready -> acquired',
1025
+ '[testing-env/0] acquired -> setting_up',
1026
+ '[testing-env/0/session1] shell: "feature1" setup session',
1027
+ '[testing-env/0/feature1] feature setup session',
1028
+ '[testing-env/0/session1] shell: "feature2" setup session',
1029
+ '[testing-env/0/feature2] feature setup session',
1030
+ '[testing-env/0] setting_up -> in_session',
1031
+ "[testing-env/0] session 'session1' started",
1032
+ '[testing-env/0/session1] shell: "feature1" teardown session',
1033
+ '[testing-env/0/feature1] feature teardown session',
1034
+ '[testing-env/0/session1] shell: "feature2" teardown session',
1035
+ '[testing-env/0/feature2] feature teardown session',
1036
+ "[testing-env/0] session 'session1' ended",
1037
+ '[testing-env/0] in_session -> acquired',
1038
+ '[testing-env/0] acquired -> shutting_down',
1039
+ '[testing-env/0/feature1] feature teardown with SandboxStateError', # pylint: disable=line-too-long
1040
+ '[testing-env/0/feature2] feature teardown',
1041
+ '[testing-env/0] shutting_down -> offline',
1042
+ '[testing-env/0] sandbox shutdown with FeatureTeardownError'
1043
+ ]
1044
+ )
1045
+
1046
+ def test_feature_setup_session_non_state_error(self):
1047
+ env = self._create_env(
1048
+ features={
1049
+ 'feature1': TestingFeature(),
1050
+ 'feature2': TestingFeature(
1051
+ simulate_setup_session_error=ValueError
1052
+ ),
1053
+ },
1054
+ )
1055
+ with env:
1056
+ with self.assertRaises(ValueError):
1057
+ with env.sandbox('session1') as sb:
1058
+ sb.shell('echo "hello"')
1059
+ self.assertEqual(
1060
+ self.event_handler.logs,
1061
+ [
1062
+ '[testing-env] environment started',
1063
+ '[testing-env/0/feature1] feature setup',
1064
+ '[testing-env/0/feature2] feature setup',
1065
+ '[testing-env/0] created -> ready',
1066
+ '[testing-env/0] sandbox started',
1067
+ '[testing-env/0] ready -> acquired',
1068
+ '[testing-env/0] acquired -> setting_up',
1069
+ '[testing-env/0/session1] shell: "feature1" setup session',
1070
+ '[testing-env/0/feature1] feature setup session',
1071
+ '[testing-env/0/feature2] feature setup session with ValueError',
1072
+ "[testing-env/0] session 'session1' started with ValueError",
1073
+ '[testing-env/0] setting_up -> shutting_down',
1074
+ '[testing-env/0/session1] shell: "feature1" teardown',
1075
+ '[testing-env/0/feature1] feature teardown',
1076
+ '[testing-env/0/session1] shell: "feature2" teardown',
1077
+ '[testing-env/0/feature2] feature teardown',
1078
+ '[testing-env/0] shutting_down -> offline',
1079
+ '[testing-env/0] sandbox shutdown'
1080
+ ]
1081
+ )
1082
+
1083
+ def test_feature_teardown_session_non_state_error(self):
1084
+ env = self._create_env(
1085
+ features={
1086
+ 'feature1': TestingFeature(
1087
+ simulate_teardown_session_error=ValueError
1088
+ ),
1089
+ 'feature2': TestingFeature(),
1090
+ },
1091
+ )
1092
+ with env:
1093
+ with self.assertRaises(interface.SessionTeardownError):
1094
+ with env.sandbox('session1') as sb:
1095
+ sb.shell('echo "hello"')
1096
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1097
+ self.assertEqual(
1098
+ self.event_handler.logs,
1099
+ [
1100
+ '[testing-env] environment started',
1101
+ '[testing-env/0/feature1] feature setup',
1102
+ '[testing-env/0/feature2] feature setup',
1103
+ '[testing-env/0] created -> ready',
1104
+ '[testing-env/0] sandbox started',
1105
+ '[testing-env/0] ready -> acquired',
1106
+ '[testing-env/0] acquired -> setting_up',
1107
+ '[testing-env/0/session1] shell: "feature1" setup session',
1108
+ '[testing-env/0/feature1] feature setup session',
1109
+ '[testing-env/0/session1] shell: "feature2" setup session',
1110
+ '[testing-env/0/feature2] feature setup session',
1111
+ '[testing-env/0] setting_up -> in_session',
1112
+ "[testing-env/0] session 'session1' started",
1113
+ '[testing-env/0/session1] shell: echo "hello"',
1114
+ '[testing-env/0/feature1] feature teardown session with ValueError', # pylint: disable=line-too-long
1115
+ '[testing-env/0/session1] shell: "feature2" teardown session',
1116
+ '[testing-env/0/feature2] feature teardown session',
1117
+ "[testing-env/0] session 'session1' ended",
1118
+ '[testing-env/0] in_session -> acquired',
1119
+ '[testing-env/0] acquired -> shutting_down',
1120
+ '[testing-env/0/feature1] feature teardown',
1121
+ '[testing-env/0/feature2] feature teardown',
1122
+ '[testing-env/0] shutting_down -> offline',
1123
+ '[testing-env/0] sandbox shutdown'
1124
+ ]
1125
+ )
1126
+
1127
+ def test_feature_teardown_session_state_error(self):
1128
+ env = self._create_env(
1129
+ features={
1130
+ 'feature1': TestingFeature(
1131
+ simulate_teardown_session_error=interface.SandboxStateError
1132
+ ),
1133
+ 'feature2': TestingFeature(),
1134
+ },
1135
+ )
1136
+ with env:
1137
+ with env.sandbox('session1') as sb:
1138
+ sb.shell('echo "hello"')
1139
+ self.assertEqual(len(sb.state_errors), 1)
1140
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1141
+ self.assertEqual(
1142
+ self.event_handler.logs,
1143
+ [
1144
+ '[testing-env] environment started',
1145
+ '[testing-env/0/feature1] feature setup',
1146
+ '[testing-env/0/feature2] feature setup',
1147
+ '[testing-env/0] created -> ready',
1148
+ '[testing-env/0] sandbox started',
1149
+ '[testing-env/0] ready -> acquired',
1150
+ '[testing-env/0] acquired -> setting_up',
1151
+ '[testing-env/0/session1] shell: "feature1" setup session',
1152
+ '[testing-env/0/feature1] feature setup session',
1153
+ '[testing-env/0/session1] shell: "feature2" setup session',
1154
+ '[testing-env/0/feature2] feature setup session',
1155
+ '[testing-env/0] setting_up -> in_session',
1156
+ "[testing-env/0] session 'session1' started",
1157
+ '[testing-env/0/session1] shell: echo "hello"',
1158
+ '[testing-env/0/feature1] feature teardown session with SandboxStateError', # pylint: disable=line-too-long
1159
+ '[testing-env/0/session1] shell: "feature2" teardown session',
1160
+ '[testing-env/0/feature2] feature teardown session',
1161
+ "[testing-env/0] session 'session1' ended with SandboxStateError",
1162
+ '[testing-env/0] in_session -> acquired',
1163
+ '[testing-env/0] acquired -> shutting_down',
1164
+ '[testing-env/0/feature1] feature teardown',
1165
+ '[testing-env/0/feature2] feature teardown',
1166
+ '[testing-env/0] shutting_down -> offline',
1167
+ '[testing-env/0] sandbox shutdown'
1168
+ ]
1169
+ )
1170
+
1171
+ def test_session_activity_non_state_error(self):
1172
+ env = self._create_env(
1173
+ pool_size=1,
1174
+ features={
1175
+ 'feature1': TestingFeature(),
1176
+ },
1177
+ )
1178
+ with env:
1179
+ with env.sandbox('session1') as sb:
1180
+ with self.assertRaises(ValueError):
1181
+ sb.shell('echo foo', raise_error=ValueError)
1182
+ self.assertEqual(len(sb.state_errors), 0)
1183
+ sb.shell('echo bar')
1184
+ self.assertEqual(sb.status, interface.Sandbox.Status.IN_SESSION)
1185
+ self.assertEqual(sb.status, interface.Sandbox.Status.READY)
1186
+ self.assertEqual(
1187
+ self.event_handler.logs,
1188
+ [
1189
+ '[testing-env/0/feature1] feature setup',
1190
+ '[testing-env/0/feature1] feature setup session',
1191
+ '[testing-env/0] created -> ready',
1192
+ '[testing-env/0] sandbox started',
1193
+ '[testing-env] environment started',
1194
+ '[testing-env/0] ready -> acquired',
1195
+ '[testing-env/0] acquired -> setting_up',
1196
+ '[testing-env/0] setting_up -> in_session',
1197
+ "[testing-env/0] session 'session1' started",
1198
+ '[testing-env/0/session1] shell: echo foo with ValueError',
1199
+ '[testing-env/0/session1] shell: echo bar',
1200
+ '[testing-env/0/session1] shell: "feature1" teardown session',
1201
+ '[testing-env/0/feature1] feature teardown session',
1202
+ "[testing-env/0] session 'session1' ended",
1203
+ '[testing-env/0] in_session -> setting_up',
1204
+ '[testing-env/0/feature1] feature setup session',
1205
+ '[testing-env/0] setting_up -> ready',
1206
+ ]
1207
+ )
1208
+
1209
+ def test_session_activity_state_error(self):
1210
+ env = self._create_env(
1211
+ pool_size=1,
1212
+ features={
1213
+ 'feature1': TestingFeature(),
1214
+ },
1215
+ )
1216
+ with env:
1217
+ with env.sandbox('session1') as sb:
1218
+ with self.assertRaises(interface.SandboxStateError):
1219
+ sb.shell('echo foo', raise_error=RuntimeError)
1220
+ self.assertEqual(len(sb.state_errors), 1)
1221
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1222
+ sb.shell('echo bar')
1223
+ self.assertEqual(
1224
+ self.event_handler.logs,
1225
+ [
1226
+ '[testing-env/0/feature1] feature setup',
1227
+ '[testing-env/0/feature1] feature setup session',
1228
+ '[testing-env/0] created -> ready',
1229
+ '[testing-env/0] sandbox started',
1230
+ '[testing-env] environment started',
1231
+ '[testing-env/0] ready -> acquired',
1232
+ '[testing-env/0] acquired -> setting_up',
1233
+ '[testing-env/0] setting_up -> in_session',
1234
+ "[testing-env/0] session 'session1' started",
1235
+ '[testing-env/0/session1] shell: echo foo with RuntimeError',
1236
+ '[testing-env/0/session1] shell: "feature1" teardown session',
1237
+ '[testing-env/0/feature1] feature teardown session',
1238
+ "[testing-env/0] session 'session1' ended with SandboxStateError",
1239
+ '[testing-env/0] in_session -> acquired',
1240
+ '[testing-env/0] acquired -> shutting_down',
1241
+ '[testing-env/0/feature1] feature teardown',
1242
+ '[testing-env/0] shutting_down -> offline',
1243
+ '[testing-env/0] sandbox shutdown'
1244
+ ]
1245
+ )
1246
+
1247
+
1248
+ class SandboxActivityTests(unittest.TestCase):
1249
+
565
1250
  def test_session_id(self):
566
- event_handler = TestingEnvironmentEventHandler()
567
1251
  env = TestingEnvironment(
568
1252
  features={'test_feature': TestingFeature()},
569
- event_handler=event_handler,
1253
+ pool_size=0
570
1254
  )
571
-
572
1255
  with env:
573
1256
  with env.sandbox() as sb:
574
1257
  self.assertRegex(sb.session_id, r'session-[0-9a-f]{7}')
@@ -587,208 +1270,334 @@ class EnvironmentTest(unittest.TestCase):
587
1270
  def foo(session_id: str):
588
1271
  del session_id
589
1272
 
590
- def test_environment_with_pooling_state_error(self):
591
- event_handler = TestingEnvironmentEventHandler()
1273
+ def test_ping_error(self):
592
1274
  env = TestingEnvironment(
593
- root_dir='/tmp',
594
- pool_size=(1, 2),
595
- outage_grace_period=1,
596
- outage_retry_interval=0,
597
- event_handler=event_handler,
1275
+ features={'test_feature': TestingFeature(housekeep_interval=0)},
1276
+ pool_size=1,
1277
+ keepalive_interval=0,
598
1278
  )
599
- self.assertEqual(env.working_dir, '/tmp/testing-env')
600
1279
  with env:
601
1280
  with env.sandbox('session1') as sb:
602
- self.assertEqual(len(env.sandbox_pool), 1)
603
- self.assertTrue(sb.is_alive)
604
- self.assertTrue(sb.is_busy)
605
- self.assertEqual(sb.session_id, 'session1')
606
- self.assertEqual(
607
- sb.id, interface.SandboxId(environment_id=env.id, sandbox_id='0')
1281
+ sb.rebind(
1282
+ simulate_ping_error=interface.SandboxStateError,
1283
+ skip_notification=True
608
1284
  )
1285
+ sb.wait_until_next_housekeep()
1286
+ self.assertEqual(sb.status, sb.Status.OFFLINE)
609
1287
 
610
- # Non-critical error.
611
- with self.assertRaises(ValueError):
612
- sb.shell('bad command', must_succeed=False)
613
-
614
- # Critical error.
615
- with self.assertRaises(interface.SandboxStateError):
616
- sb.shell('bad command')
617
- maintenance_count = env._maintenance_count
618
- self.assertFalse(sb.is_alive)
619
-
620
- self.assertTrue(env.is_alive)
621
-
622
- # Wait dead sandbox to be replaced.
623
- while env._maintenance_count == maintenance_count:
624
- time.sleep(0.5)
625
-
626
- self.assertTrue(env.sandbox_pool[0].is_alive)
627
- self.assertEqual(
628
- env.stats(),
629
- dict(
630
- sandbox=dict(
631
- num_total=1,
632
- num_busy=0,
633
- num_free=1,
634
- num_dead=0,
635
- )
636
- )
637
- )
638
-
639
- self.assertFalse(env.is_alive)
640
- self.assertIsNone(interface.Environment.current())
641
- self.assertEqual(
642
- event_handler.history,
643
- [
644
- '[testing-env/0] sandbox started',
645
- '[testing-env] environment started',
646
- '[testing-env/0] session \'session1\' started',
647
- # Sandbox shutdown is triggered by the SandboxStateError before
648
- # the session end event is triggered.
649
- '[testing-env/0] sandbox shutdown',
650
- '[testing-env/0] session \'session1\' ended',
651
- # The maintenance loop will replace the dead sandbox and start a new
652
- # sandbox.
653
- '[testing-env/0] sandbox started',
654
- '[testing-env/0] sandbox shutdown',
655
- '[testing-env] environment shutdown',
656
- ]
657
- )
658
- self.assertEqual(
659
- env.stats(),
660
- dict(
661
- sandbox=dict(
662
- num_total=0,
663
- num_busy=0,
664
- num_free=0,
665
- num_dead=0,
666
- )
667
- )
668
- )
669
-
670
- def test_environment_outage_with_pooling(self):
671
- event_handler = TestingEnvironmentEventHandler()
1288
+ def test_housekeep_error(self):
672
1289
  env = TestingEnvironment(
673
- features={'test_feature': TestingFeature()},
674
- pool_size=(1, 2),
1290
+ features={'test_feature': TestingFeature(housekeep_interval=0)},
1291
+ pool_size=1,
675
1292
  outage_grace_period=0,
676
1293
  outage_retry_interval=0,
677
1294
  keepalive_interval=0,
678
- event_handler=event_handler,
679
- stats_report_interval=1,
680
1295
  )
681
1296
  with env:
682
1297
  with env.sandbox('session1') as sb:
683
1298
  self.assertEqual(len(env.sandbox_pool), 1)
684
- self.assertTrue(sb.is_alive)
685
- self.assertTrue(sb.is_busy)
1299
+ self.assertEqual(sb.status, interface.Sandbox.Status.IN_SESSION)
686
1300
  self.assertEqual(sb.session_id, 'session1')
687
- env.set_offline(True)
688
1301
  housekeep_count = sb._housekeep_count
689
- while sb._housekeep_count == housekeep_count:
690
- time.sleep(1.0)
691
- self.assertFalse(sb.is_alive)
692
- maintenance_count = env._maintenance_count
693
- while env._maintenance_count == maintenance_count:
694
- time.sleep(1.0)
695
- self.assertFalse(env.is_alive)
696
-
697
- with self.assertRaises(interface.EnvironmentOutageError):
698
- with env.sandbox('session2'):
699
- pass
700
-
701
- with self.assertRaises(interface.EnvironmentOutageError):
702
- with env.test_feature.my_service():
703
- pass
704
- self.assertGreater(env.offline_duration, 0)
1302
+ sb.test_feature.rebind(
1303
+ simulate_housekeep_error=interface.SandboxStateError,
1304
+ skip_notification=True
1305
+ )
1306
+ while sb._housekeep_count == housekeep_count or (
1307
+ sb.status == interface.Sandbox.Status.IN_SESSION
1308
+ ):
1309
+ time.sleep(0.1)
1310
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
705
1311
 
706
- def test_environment_outage_during_acquire(self):
707
- event_handler = TestingEnvironmentEventHandler()
1312
+ def test_remove_event_handler(self):
708
1313
  env = TestingEnvironment(
709
- features={'test_feature': TestingFeature()},
710
- pool_size=(2, 3),
1314
+ features={'test_feature': TestingFeature(housekeep_interval=0)},
1315
+ pool_size=1,
711
1316
  outage_grace_period=0,
712
1317
  outage_retry_interval=0,
713
1318
  keepalive_interval=0,
714
- event_handler=event_handler,
715
1319
  stats_report_interval=1,
716
1320
  )
1321
+ event_handler = TestingEnvironmentEventHandler()
1322
+ with env:
1323
+ with env.sandbox('session1') as sb:
1324
+ sb.add_event_handler(event_handler)
1325
+ sb.shell('test_feature')
1326
+ sb.remove_event_handler(event_handler)
1327
+ events = list(event_handler.logs)
1328
+ self.assertGreater(len(events), 0)
1329
+ with env.sandbox('session2') as sb:
1330
+ sb.shell('test_feature')
1331
+ self.assertEqual(len(events), len(event_handler.logs))
717
1332
 
718
- def _thread_func(i) -> bool:
719
- time.sleep(0.8 * i)
720
- with env.sandbox(f'session{i}') as sb:
721
- return sb.shell('echo "foo"')
722
1333
 
723
- with env:
724
- with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
725
- fs = [
726
- executor.submit(_thread_func, i) for i in range(5)
727
- ]
728
- time.sleep(1.0)
729
- env.set_offline(True)
730
- vs = []
731
- for f in fs:
732
- try:
733
- vs.append(f.result())
734
- except interface.EnvironmentError as e:
735
- vs.append(e)
736
-
737
- def test_environment_outage_during_acquire_pool_not_full(self):
738
- event_handler = TestingEnvironmentEventHandler()
739
- env = TestingEnvironment(
740
- features={
741
- 'test_feature': TestingFeature(simulate_housekeep_error=True)
742
- },
743
- pool_size=(1, 3),
744
- outage_grace_period=1,
1334
+ class SandboxServiceTests(unittest.TestCase):
1335
+
1336
+ def setUp(self):
1337
+ super().setUp()
1338
+ self.maxDiff = None
1339
+ self.event_handler = TestingEnvironmentEventHandler()
1340
+ self.env = TestingEnvironment(
1341
+ features={'test_feature': TestingFeature()},
1342
+ pool_size=0,
1343
+ outage_grace_period=0,
745
1344
  outage_retry_interval=0,
746
1345
  keepalive_interval=0,
747
- event_handler=event_handler,
1346
+ event_handlers=[self.event_handler],
748
1347
  stats_report_interval=1,
1348
+ random_seed=1,
749
1349
  )
750
1350
 
751
- def _thread_func() -> bool:
752
- with env.sandbox('session1') as sb:
753
- return sb.shell('echo "foo"')
1351
+ def test_service_call_activity_log(self):
754
1352
 
755
- with env:
756
- self.assertEqual(len(env.sandbox_pool), 1)
757
- self.assertFalse(env.sandbox_pool[0].is_alive)
758
- with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
759
- env.set_offline(True)
760
- f = executor.submit(_thread_func)
761
- with self.assertRaises(interface.EnvironmentOutageError):
762
- f.result()
1353
+ class CustomEventHandler(interface.EnvironmentEventHandler):
763
1354
 
764
- def test_housekeep_error(self):
765
- event_handler = TestingEnvironmentEventHandler()
1355
+ def __init__(self):
1356
+ self.calls = []
1357
+
1358
+ def on_session_activity(
1359
+ self,
1360
+ session_id: str,
1361
+ name: str,
1362
+ environment: interface.Environment,
1363
+ sandbox: interface.Sandbox,
1364
+ feature: interface.Feature | None,
1365
+ error: BaseException | None,
1366
+ **kwargs: Any):
1367
+ self.calls.append((session_id, name, kwargs))
1368
+
1369
+ event_handler = CustomEventHandler()
766
1370
  env = TestingEnvironment(
767
- features={
768
- 'test_feature': TestingFeature(
769
- housekeep_interval=0
770
- )
771
- },
772
- pool_size=1,
773
- outage_grace_period=0,
774
- outage_retry_interval=0,
775
- keepalive_interval=0,
776
- event_handler=event_handler,
777
- stats_report_interval=1,
1371
+ features={'test_feature': TestingFeature()},
1372
+ pool_size=0,
1373
+ event_handlers=[event_handler],
778
1374
  )
779
1375
  with env:
780
- with env.sandbox('session1') as sb:
781
- self.assertEqual(len(env.sandbox_pool), 1)
782
- self.assertTrue(sb.is_alive)
783
- self.assertTrue(sb.is_busy)
784
- self.assertEqual(sb.session_id, 'session1')
785
- housekeep_count = sb._housekeep_count
786
- sb.test_feature.rebind(
787
- simulate_housekeep_error=True, skip_notification=True
788
- )
789
- while sb._housekeep_count == housekeep_count or sb.is_alive:
790
- time.sleep(1.0)
791
- self.assertFalse(sb.is_alive)
1376
+ env.test_feature.call_with_varargs(
1377
+ 'sum', 1, 2, debug=True, session_id='session1'
1378
+ )
1379
+ self.assertEqual(
1380
+ event_handler.calls,
1381
+ [
1382
+ ('session1', 'shell', {'code': '"test_feature" setup session'}),
1383
+ ('session1', 'call_with_varargs', {'args': (1, 2), 'code': 'sum', 'debug': True}), # pylint: disable=line-too-long
1384
+ ('session1', 'shell', {'code': '"test_feature" teardown session'}),
1385
+ ]
1386
+ )
1387
+
1388
+ def test_service_call_from_feature(self):
1389
+ with self.env:
1390
+ with self.env.sandbox('session1') as sb:
1391
+ self.assertEqual(sb.test_feature.num_shell_calls(), 2)
1392
+ self.assertEqual(sb.test_feature.num_shell_calls(), 2)
1393
+ self.assertEqual(
1394
+ self.event_handler.logs,
1395
+ [
1396
+ # pylint: disable=line-too-long
1397
+ '[testing-env] environment started',
1398
+ '[testing-env/0/test_feature] feature setup',
1399
+ '[testing-env/0] sandbox started',
1400
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1401
+ "[testing-env/0] session 'session1' started",
1402
+ '[testing-env/0/session1] num_shell_calls: None',
1403
+ '[testing-env/0/session1] num_shell_calls: None',
1404
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1405
+ "[testing-env/0] session 'session1' ended",
1406
+ '[testing-env/0/test_feature] feature teardown',
1407
+ '[testing-env/0] sandbox shutdown',
1408
+ '[testing-env] environment shutdown'
1409
+ # pylint: enable=line-too-long
1410
+ ]
1411
+ )
1412
+
1413
+ def test_service_call_from_feature_with_error(self):
1414
+ with self.env:
1415
+ with self.env.sandbox('session1') as sb:
1416
+ with self.assertRaises(interface.SandboxStateError):
1417
+ sb.test_feature.bad_shell_call()
1418
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1419
+
1420
+ self.assertEqual(
1421
+ self.event_handler.logs,
1422
+ [
1423
+ # pylint: disable=line-too-long
1424
+ '[testing-env] environment started',
1425
+ '[testing-env/0/test_feature] feature setup',
1426
+ '[testing-env/0] sandbox started',
1427
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1428
+ "[testing-env/0] session 'session1' started",
1429
+ '[testing-env/0/session1] shell: bad command with RuntimeError',
1430
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1431
+ "[testing-env/0] session 'session1' ended with SandboxStateError",
1432
+ '[testing-env/0/test_feature] feature teardown',
1433
+ '[testing-env/0] sandbox shutdown',
1434
+ '[testing-env/0/session1] bad_shell_call: None with SandboxStateError',
1435
+ '[testing-env] environment shutdown'
1436
+ # pylint: enable=line-too-long
1437
+ ]
1438
+ )
1439
+
1440
+ def test_service_call_from_environment(self):
1441
+ with self.env:
1442
+ self.assertEqual(self.env.test_feature.num_shell_calls(), 2)
1443
+ self.assertEqual(
1444
+ self.event_handler.logs,
1445
+ [
1446
+ # pylint: disable=line-too-long
1447
+ '[testing-env] environment started',
1448
+ '[testing-env/0/test_feature] feature setup',
1449
+ '[testing-env/0] sandbox started',
1450
+ '[testing-env/0/session-2291d8c] shell: "test_feature" setup session',
1451
+ "[testing-env/0] session 'session-2291d8c' started",
1452
+ '[testing-env/0/session-2291d8c] num_shell_calls: None',
1453
+ '[testing-env/0/session-2291d8c] shell: "test_feature" teardown session',
1454
+ "[testing-env/0] session 'session-2291d8c' ended",
1455
+ '[testing-env/0/test_feature] feature teardown',
1456
+ '[testing-env/0] sandbox shutdown',
1457
+ '[testing-env] environment shutdown'
1458
+ # pylint: enable=line-too-long
1459
+ ]
1460
+ )
1461
+
1462
+ def test_service_call_from_environment_with_error(self):
1463
+ with self.env:
1464
+ with self.assertRaises(interface.SandboxStateError):
1465
+ self.env.test_feature.bad_shell_call(session_id='session1')
1466
+ self.assertEqual(
1467
+ self.event_handler.logs,
1468
+ [
1469
+ # pylint: disable=line-too-long
1470
+ '[testing-env] environment started',
1471
+ '[testing-env/0/test_feature] feature setup',
1472
+ '[testing-env/0] sandbox started',
1473
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1474
+ "[testing-env/0] session 'session1' started",
1475
+ '[testing-env/0/session1] shell: bad command with RuntimeError',
1476
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1477
+ "[testing-env/0] session 'session1' ended with SandboxStateError",
1478
+ '[testing-env/0/test_feature] feature teardown',
1479
+ '[testing-env/0] sandbox shutdown',
1480
+ '[testing-env/0/session1] bad_shell_call: None with SandboxStateError',
1481
+ '[testing-env] environment shutdown'
1482
+ # pylint: enable=line-too-long
1483
+ ]
1484
+ )
1485
+
1486
+ def test_service_context_manager_from_feature(self):
1487
+ with self.env:
1488
+ with self.env.sandbox('session1') as sb:
1489
+ with sb.test_feature.my_service() as service:
1490
+ service.do('hello')
1491
+ sb.shell('foo')
1492
+ self.assertEqual(sb.status, interface.Sandbox.Status.IN_SESSION)
1493
+ self.assertEqual(
1494
+ self.event_handler.logs,
1495
+ [
1496
+ # pylint: disable=line-too-long
1497
+ '[testing-env] environment started',
1498
+ '[testing-env/0/test_feature] feature setup',
1499
+ '[testing-env/0] sandbox started',
1500
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1501
+ "[testing-env/0] session 'session1' started",
1502
+ '[testing-env/0/session1] my_service: None',
1503
+ '[testing-env/0/session1] shell: hello',
1504
+ '[testing-env/0/session1] shell: foo',
1505
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1506
+ "[testing-env/0] session 'session1' ended",
1507
+ '[testing-env/0/test_feature] feature teardown',
1508
+ '[testing-env/0] sandbox shutdown',
1509
+ '[testing-env] environment shutdown'
1510
+ # pylint: enable=line-too-long
1511
+ ]
1512
+ )
1513
+
1514
+ def test_service_context_manager_from_feature_with_error(self):
1515
+ with self.env:
1516
+ with self.env.sandbox('session1') as sb:
1517
+ with self.assertRaises(interface.SandboxStateError):
1518
+ with sb.test_feature.my_service() as service:
1519
+ service.do('hello', raise_error=interface.SandboxStateError)
1520
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1521
+ self.assertEqual(
1522
+ self.event_handler.logs,
1523
+ [
1524
+ # pylint: disable=line-too-long
1525
+ '[testing-env] environment started',
1526
+ '[testing-env/0/test_feature] feature setup',
1527
+ '[testing-env/0] sandbox started',
1528
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1529
+ "[testing-env/0] session 'session1' started",
1530
+ '[testing-env/0/session1] my_service: None',
1531
+ '[testing-env/0/session1] shell: hello with SandboxStateError',
1532
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1533
+ "[testing-env/0] session 'session1' ended with SandboxStateError",
1534
+ '[testing-env/0/test_feature] feature teardown',
1535
+ '[testing-env/0] sandbox shutdown',
1536
+ '[testing-env] environment shutdown'
1537
+ # pylint: enable=line-too-long
1538
+ ]
1539
+ )
1540
+
1541
+ def test_service_context_manager_from_environment(self):
1542
+ with self.env:
1543
+ with self.env.test_feature.my_service(session_id='session1') as service:
1544
+ service.do('foo')
1545
+ with self.env.test_feature.my_service() as service:
1546
+ service.do('bar')
1547
+ self.assertEqual(
1548
+ self.event_handler.logs,
1549
+ [
1550
+ # pylint: disable=line-too-long
1551
+ '[testing-env] environment started',
1552
+ '[testing-env/0/test_feature] feature setup',
1553
+ '[testing-env/0] sandbox started',
1554
+ '[testing-env/0/session1] shell: "test_feature" setup session',
1555
+ "[testing-env/0] session 'session1' started",
1556
+ '[testing-env/0/session1] my_service: None',
1557
+ '[testing-env/0/session1] shell: foo',
1558
+ '[testing-env/0/session1] shell: "test_feature" teardown session',
1559
+ "[testing-env/0] session 'session1' ended",
1560
+ '[testing-env/0/test_feature] feature teardown',
1561
+ '[testing-env/0] sandbox shutdown',
1562
+ '[testing-env/1/test_feature] feature setup',
1563
+ '[testing-env/1] sandbox started',
1564
+ '[testing-env/1/session-2291d8c] shell: "test_feature" setup session',
1565
+ "[testing-env/1] session 'session-2291d8c' started",
1566
+ '[testing-env/1/session-2291d8c] my_service: None',
1567
+ '[testing-env/1/session-2291d8c] shell: bar',
1568
+ '[testing-env/1/session-2291d8c] shell: "test_feature" teardown session',
1569
+ "[testing-env/1] session 'session-2291d8c' ended",
1570
+ '[testing-env/1/test_feature] feature teardown',
1571
+ '[testing-env/1] sandbox shutdown',
1572
+ '[testing-env] environment shutdown'
1573
+ # pylint: enable=line-too-long
1574
+ ]
1575
+ )
1576
+
1577
+ def test_service_context_manager_from_environment_with_error(self):
1578
+ with self.env:
1579
+ with self.assertRaises(interface.SandboxStateError):
1580
+ with self.env.test_feature.my_service() as service:
1581
+ service.do('hello', raise_error=interface.SandboxStateError)
1582
+ self.assertEqual(
1583
+ self.event_handler.logs,
1584
+ [
1585
+ # pylint: disable=line-too-long
1586
+ '[testing-env] environment started',
1587
+ '[testing-env/0/test_feature] feature setup',
1588
+ '[testing-env/0] sandbox started',
1589
+ '[testing-env/0/session-2291d8c] shell: "test_feature" setup session',
1590
+ "[testing-env/0] session 'session-2291d8c' started",
1591
+ '[testing-env/0/session-2291d8c] my_service: None',
1592
+ '[testing-env/0/session-2291d8c] shell: hello with SandboxStateError',
1593
+ '[testing-env/0/session-2291d8c] shell: "test_feature" teardown session',
1594
+ "[testing-env/0] session 'session-2291d8c' ended with SandboxStateError",
1595
+ '[testing-env/0/test_feature] feature teardown',
1596
+ '[testing-env/0] sandbox shutdown',
1597
+ '[testing-env] environment shutdown'
1598
+ # pylint: enable=line-too-long
1599
+ ]
1600
+ )
792
1601
 
793
1602
 
794
1603
  if __name__ == '__main__':