langfun 0.1.2.dev202509120804__py3-none-any.whl → 0.1.2.dev202512040805__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.
Files changed (162) hide show
  1. langfun/__init__.py +1 -1
  2. langfun/core/__init__.py +7 -1
  3. langfun/core/agentic/__init__.py +8 -1
  4. langfun/core/agentic/action.py +740 -112
  5. langfun/core/agentic/action_eval.py +9 -2
  6. langfun/core/agentic/action_test.py +189 -24
  7. langfun/core/async_support.py +104 -5
  8. langfun/core/async_support_test.py +23 -0
  9. langfun/core/coding/python/correction.py +19 -9
  10. langfun/core/coding/python/execution.py +14 -12
  11. langfun/core/coding/python/generation.py +21 -16
  12. langfun/core/coding/python/sandboxing.py +23 -3
  13. langfun/core/component.py +42 -3
  14. langfun/core/concurrent.py +70 -6
  15. langfun/core/concurrent_test.py +9 -2
  16. langfun/core/console.py +1 -1
  17. langfun/core/data/conversion/anthropic.py +12 -3
  18. langfun/core/data/conversion/anthropic_test.py +8 -6
  19. langfun/core/data/conversion/gemini.py +11 -2
  20. langfun/core/data/conversion/gemini_test.py +48 -9
  21. langfun/core/data/conversion/openai.py +145 -31
  22. langfun/core/data/conversion/openai_test.py +161 -17
  23. langfun/core/eval/base.py +48 -44
  24. langfun/core/eval/base_test.py +5 -5
  25. langfun/core/eval/matching.py +5 -2
  26. langfun/core/eval/patching.py +3 -3
  27. langfun/core/eval/scoring.py +4 -3
  28. langfun/core/eval/v2/__init__.py +2 -0
  29. langfun/core/eval/v2/checkpointing.py +76 -7
  30. langfun/core/eval/v2/checkpointing_test.py +9 -2
  31. langfun/core/eval/v2/config_saver.py +37 -0
  32. langfun/core/eval/v2/config_saver_test.py +36 -0
  33. langfun/core/eval/v2/eval_test_helper.py +104 -3
  34. langfun/core/eval/v2/evaluation.py +92 -17
  35. langfun/core/eval/v2/evaluation_test.py +9 -3
  36. langfun/core/eval/v2/example.py +50 -40
  37. langfun/core/eval/v2/example_test.py +16 -8
  38. langfun/core/eval/v2/experiment.py +84 -15
  39. langfun/core/eval/v2/experiment_test.py +19 -0
  40. langfun/core/eval/v2/metric_values.py +31 -3
  41. langfun/core/eval/v2/metric_values_test.py +32 -0
  42. langfun/core/eval/v2/metrics.py +157 -44
  43. langfun/core/eval/v2/metrics_test.py +39 -18
  44. langfun/core/eval/v2/progress.py +31 -1
  45. langfun/core/eval/v2/progress_test.py +27 -0
  46. langfun/core/eval/v2/progress_tracking.py +13 -5
  47. langfun/core/eval/v2/progress_tracking_test.py +9 -1
  48. langfun/core/eval/v2/reporting.py +90 -71
  49. langfun/core/eval/v2/reporting_test.py +24 -6
  50. langfun/core/eval/v2/runners/__init__.py +30 -0
  51. langfun/core/eval/v2/{runners.py → runners/base.py} +72 -180
  52. langfun/core/eval/v2/runners/beam.py +354 -0
  53. langfun/core/eval/v2/runners/beam_test.py +153 -0
  54. langfun/core/eval/v2/runners/ckpt_monitor.py +294 -0
  55. langfun/core/eval/v2/runners/ckpt_monitor_test.py +162 -0
  56. langfun/core/eval/v2/runners/debug.py +40 -0
  57. langfun/core/eval/v2/runners/debug_test.py +76 -0
  58. langfun/core/eval/v2/runners/parallel.py +243 -0
  59. langfun/core/eval/v2/runners/parallel_test.py +182 -0
  60. langfun/core/eval/v2/runners/sequential.py +47 -0
  61. langfun/core/eval/v2/runners/sequential_test.py +169 -0
  62. langfun/core/langfunc.py +45 -130
  63. langfun/core/langfunc_test.py +7 -5
  64. langfun/core/language_model.py +189 -36
  65. langfun/core/language_model_test.py +54 -3
  66. langfun/core/llms/__init__.py +12 -1
  67. langfun/core/llms/anthropic.py +157 -2
  68. langfun/core/llms/azure_openai.py +29 -17
  69. langfun/core/llms/cache/base.py +25 -3
  70. langfun/core/llms/cache/in_memory.py +48 -7
  71. langfun/core/llms/cache/in_memory_test.py +14 -4
  72. langfun/core/llms/compositional.py +25 -1
  73. langfun/core/llms/deepseek.py +30 -2
  74. langfun/core/llms/fake.py +32 -1
  75. langfun/core/llms/gemini.py +64 -12
  76. langfun/core/llms/gemini_test.py +110 -0
  77. langfun/core/llms/google_genai.py +34 -1
  78. langfun/core/llms/groq.py +28 -3
  79. langfun/core/llms/llama_cpp.py +23 -4
  80. langfun/core/llms/openai.py +120 -3
  81. langfun/core/llms/openai_compatible.py +148 -27
  82. langfun/core/llms/openai_compatible_test.py +207 -20
  83. langfun/core/llms/openai_test.py +0 -2
  84. langfun/core/llms/rest.py +16 -1
  85. langfun/core/llms/vertexai.py +58 -8
  86. langfun/core/logging.py +1 -1
  87. langfun/core/mcp/__init__.py +10 -0
  88. langfun/core/mcp/client.py +177 -0
  89. langfun/core/mcp/client_test.py +71 -0
  90. langfun/core/mcp/session.py +241 -0
  91. langfun/core/mcp/session_test.py +54 -0
  92. langfun/core/mcp/testing/simple_mcp_client.py +33 -0
  93. langfun/core/mcp/testing/simple_mcp_server.py +33 -0
  94. langfun/core/mcp/tool.py +254 -0
  95. langfun/core/mcp/tool_test.py +197 -0
  96. langfun/core/memory.py +1 -0
  97. langfun/core/message.py +160 -55
  98. langfun/core/message_test.py +65 -81
  99. langfun/core/modalities/__init__.py +8 -0
  100. langfun/core/modalities/audio.py +21 -1
  101. langfun/core/modalities/image.py +73 -3
  102. langfun/core/modalities/image_test.py +116 -0
  103. langfun/core/modalities/mime.py +64 -3
  104. langfun/core/modalities/mime_test.py +11 -0
  105. langfun/core/modalities/pdf.py +19 -1
  106. langfun/core/modalities/video.py +21 -1
  107. langfun/core/modality.py +167 -29
  108. langfun/core/modality_test.py +42 -12
  109. langfun/core/natural_language.py +1 -1
  110. langfun/core/sampling.py +4 -4
  111. langfun/core/sampling_test.py +20 -4
  112. langfun/core/structured/__init__.py +2 -24
  113. langfun/core/structured/completion.py +34 -44
  114. langfun/core/structured/completion_test.py +23 -43
  115. langfun/core/structured/description.py +54 -50
  116. langfun/core/structured/function_generation.py +29 -12
  117. langfun/core/structured/mapping.py +81 -37
  118. langfun/core/structured/parsing.py +95 -79
  119. langfun/core/structured/parsing_test.py +0 -3
  120. langfun/core/structured/querying.py +230 -154
  121. langfun/core/structured/querying_test.py +69 -33
  122. langfun/core/structured/schema/__init__.py +49 -0
  123. langfun/core/structured/schema/base.py +664 -0
  124. langfun/core/structured/schema/base_test.py +531 -0
  125. langfun/core/structured/schema/json.py +174 -0
  126. langfun/core/structured/schema/json_test.py +121 -0
  127. langfun/core/structured/schema/python.py +316 -0
  128. langfun/core/structured/schema/python_test.py +410 -0
  129. langfun/core/structured/schema_generation.py +33 -14
  130. langfun/core/structured/scoring.py +47 -36
  131. langfun/core/structured/tokenization.py +26 -11
  132. langfun/core/subscription.py +2 -2
  133. langfun/core/template.py +175 -50
  134. langfun/core/template_test.py +123 -17
  135. langfun/env/__init__.py +43 -0
  136. langfun/env/base_environment.py +827 -0
  137. langfun/env/base_environment_test.py +473 -0
  138. langfun/env/base_feature.py +304 -0
  139. langfun/env/base_feature_test.py +228 -0
  140. langfun/env/base_sandbox.py +842 -0
  141. langfun/env/base_sandbox_test.py +1235 -0
  142. langfun/env/event_handlers/__init__.py +14 -0
  143. langfun/env/event_handlers/chain.py +233 -0
  144. langfun/env/event_handlers/chain_test.py +253 -0
  145. langfun/env/event_handlers/event_logger.py +472 -0
  146. langfun/env/event_handlers/event_logger_test.py +304 -0
  147. langfun/env/event_handlers/metric_writer.py +726 -0
  148. langfun/env/event_handlers/metric_writer_test.py +214 -0
  149. langfun/env/interface.py +1640 -0
  150. langfun/env/interface_test.py +153 -0
  151. langfun/env/load_balancers.py +59 -0
  152. langfun/env/load_balancers_test.py +141 -0
  153. langfun/env/test_utils.py +507 -0
  154. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512040805.dist-info}/METADATA +7 -3
  155. langfun-0.1.2.dev202512040805.dist-info/RECORD +217 -0
  156. langfun/core/eval/v2/runners_test.py +0 -343
  157. langfun/core/structured/schema.py +0 -987
  158. langfun/core/structured/schema_test.py +0 -982
  159. langfun-0.1.2.dev202509120804.dist-info/RECORD +0 -172
  160. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512040805.dist-info}/WHEEL +0 -0
  161. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512040805.dist-info}/licenses/LICENSE +0 -0
  162. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512040805.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,304 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Common base class for environment features.
15
+
16
+ This module provides an base class `BaseFeature` for environment features,
17
+ which provides event handlers for the feature lifecycle events, which can be
18
+ overridden by subclasses to provide custom behaviors. Please note that this base
19
+ class is intended to provide a convenient way to implement features, and not
20
+ all feature implementations need to subclass it. Also `BaseFeature` is not
21
+ coupled with `BaseEnvironment` and `BaseSandbox`, and is expected to work with
22
+ the `Environment` and `Sandbox` interfaces directly.
23
+ """
24
+
25
+ import contextlib
26
+ import functools
27
+ import os
28
+ import re
29
+ import time
30
+ from typing import Annotated, Any, Callable, Iterator
31
+
32
+ from langfun.env import interface
33
+ import pyglove as pg
34
+
35
+
36
+ class BaseFeature(interface.Feature):
37
+ """Common base class for environment features."""
38
+
39
+ is_sandbox_based: Annotated[
40
+ bool,
41
+ 'Whether the feature is sandbox-based.'
42
+ ] = True
43
+
44
+ applicable_images: Annotated[
45
+ list[str],
46
+ (
47
+ 'A list of regular expressions for image IDs which enable '
48
+ 'this feature. By default, all images are enabled.'
49
+ )
50
+ ] = ['.*']
51
+
52
+ housekeep_interval: Annotated[
53
+ float | None,
54
+ 'Interval in seconds for feature housekeeping.'
55
+ ] = None
56
+
57
+ #
58
+ # Subclasses can override:
59
+ #
60
+
61
+ def _setup(self) -> None:
62
+ """Subclasses can override this for custom setup.
63
+
64
+ NOTE: always call super()._setup() at the beginning of the implementation.
65
+ """
66
+
67
+ def _teardown(self) -> None:
68
+ """Subclasses can override this for custom teardown.
69
+
70
+ NOTE: always call super()._teardown() at the end of the implementation.
71
+ """
72
+
73
+ def _setup_session(self) -> None:
74
+ """Subclasses can override this for custom setup session.
75
+
76
+ NOTE: always call super()._setup_session() at the beginning of the
77
+ implementation.
78
+ """
79
+
80
+ def _teardown_session(self) -> None:
81
+ """Subclasses can override this for custom teardown session.
82
+
83
+ NOTE: always call super()._teardown_session() at the end of the
84
+ implementation.
85
+ """
86
+
87
+ def _housekeep(self) -> None:
88
+ """Performs housekeeping for the feature.
89
+
90
+ NOTE: always call super()._housekeep() at the beginning of the
91
+ implementation.
92
+ """
93
+
94
+ #
95
+ # Init and properties
96
+ #
97
+
98
+ def _on_bound(self) -> None:
99
+ """Called when the feature is bound."""
100
+ super()._on_bound()
101
+ self._sandbox = None
102
+ self._housekeep_counter = 0
103
+
104
+ @functools.cached_property
105
+ def name(self) -> str:
106
+ """Returns the name of the feature."""
107
+ assert isinstance(self.sym_parent, dict), 'Feature is not put into a dict.'
108
+ return self.sym_path.key
109
+
110
+ def _on_parent_change(
111
+ self,
112
+ old_parent: pg.Symbolic | None,
113
+ new_parent: pg.Symbolic | None
114
+ ) -> None:
115
+ """Called when the feature is bound."""
116
+ super()._on_parent_change(old_parent, new_parent)
117
+ self.__dict__.pop('name', None)
118
+
119
+ @functools.cached_property
120
+ def environment(self) -> interface.Environment:
121
+ """Returns the environment that the feature is running in."""
122
+ if self._sandbox is not None:
123
+ return self._sandbox.environment
124
+ env = self.sym_ancestor(lambda v: isinstance(v, interface.Environment))
125
+ assert env is not None, 'Feature is not put into an environment.'
126
+ return env
127
+
128
+ @property
129
+ def sandbox(self) -> interface.Sandbox | None:
130
+ """Returns the sandbox that the feature is running in."""
131
+ assert self._sandbox is not None or not self.is_sandbox_based, (
132
+ 'Feature has not been set up yet.'
133
+ )
134
+ return self._sandbox
135
+
136
+ @property
137
+ def working_dir(self) -> str | None:
138
+ """Returns the working directory of the feature."""
139
+ sandbox_workdir = self.sandbox.working_dir
140
+ if sandbox_workdir is None:
141
+ return None
142
+ return os.path.join(sandbox_workdir, self.name)
143
+
144
+ def is_applicable(self, image_id: str) -> bool:
145
+ """Returns True if the feature is applicable to the given image."""
146
+ return any(
147
+ re.fullmatch(regex, image_id) for regex in self.applicable_images
148
+ )
149
+
150
+ #
151
+ # Setup and teardown of the feature.
152
+ #
153
+
154
+ def _do(
155
+ self,
156
+ action: Callable[[], None],
157
+ event_handler: Callable[..., None],
158
+ ) -> None:
159
+ """Triggers an event handler."""
160
+ error = None
161
+ start_time = time.time()
162
+ try:
163
+ action()
164
+ except BaseException as e: # pylint: disable=broad-except
165
+ error = e
166
+ raise
167
+ finally:
168
+ event_handler(duration=time.time() - start_time, error=error)
169
+
170
+ def setup(self, sandbox: interface.Sandbox | None = None) -> None:
171
+ """Sets up the feature."""
172
+ self._sandbox = sandbox
173
+ self._do(self._setup, self.on_setup)
174
+
175
+ def teardown(self) -> None:
176
+ """Tears down the feature."""
177
+ self._do(self._teardown, event_handler=self.on_teardown)
178
+
179
+ def setup_session(self) -> None:
180
+ """Sets up the feature for a user session."""
181
+ self._do(self._setup_session, event_handler=self.on_setup_session)
182
+
183
+ def teardown_session(self) -> None:
184
+ """Teardowns the feature for a user session."""
185
+ self._do(self._teardown_session, self.on_teardown_session)
186
+
187
+ #
188
+ # Housekeeping.
189
+ #
190
+
191
+ def housekeep(self) -> None:
192
+ """Performs housekeeping for the feature."""
193
+ try:
194
+ self._do(self._housekeep, self.on_housekeep)
195
+ finally:
196
+ self._housekeep_counter += 1
197
+
198
+ #
199
+ # Event handlers subclasses can override.
200
+ #
201
+
202
+ def on_setup(
203
+ self,
204
+ duration: float,
205
+ error: BaseException | None = None
206
+ ) -> None:
207
+ """Called when the feature is setup."""
208
+ self.environment.event_handler.on_feature_setup(
209
+ feature=self,
210
+ duration=duration,
211
+ error=error
212
+ )
213
+
214
+ def on_teardown(
215
+ self,
216
+ duration: float,
217
+ error: BaseException | None = None
218
+ ) -> None:
219
+ """Called when the feature is teardown."""
220
+ self.environment.event_handler.on_feature_teardown(
221
+ feature=self,
222
+ duration=duration,
223
+ error=error
224
+ )
225
+
226
+ def on_housekeep(
227
+ self,
228
+ duration: float,
229
+ error: BaseException | None = None,
230
+ **kwargs
231
+ ) -> None:
232
+ """Called when the feature has done housekeeping."""
233
+ self.environment.event_handler.on_feature_housekeep(
234
+ feature=self,
235
+ counter=self._housekeep_counter,
236
+ duration=duration,
237
+ error=error,
238
+ **kwargs
239
+ )
240
+
241
+ def on_setup_session(
242
+ self,
243
+ duration: float,
244
+ error: BaseException | None = None,
245
+ ) -> None:
246
+ """Called when the feature is setup for a user session."""
247
+ self.environment.event_handler.on_feature_setup_session(
248
+ feature=self,
249
+ session_id=self.session_id,
250
+ duration=duration,
251
+ error=error
252
+ )
253
+
254
+ def on_teardown_session(
255
+ self,
256
+ duration: float,
257
+ error: BaseException | None = None,
258
+ ) -> None:
259
+ """Called when the feature is teardown for a user session."""
260
+ self.environment.event_handler.on_feature_teardown_session(
261
+ feature=self,
262
+ session_id=self.session_id,
263
+ duration=duration,
264
+ error=error
265
+ )
266
+
267
+ def on_activity(
268
+ self,
269
+ name: str,
270
+ duration: float,
271
+ error: BaseException | None = None,
272
+ **kwargs
273
+ ) -> None:
274
+ """Called when a sandbox activity is performed."""
275
+ self.environment.event_handler.on_feature_activity(
276
+ name=f'{self.name}.{name}',
277
+ feature=self,
278
+ session_id=self.session_id,
279
+ duration=duration,
280
+ error=error,
281
+ **kwargs
282
+ )
283
+
284
+ @contextlib.contextmanager
285
+ def track_activity(
286
+ self,
287
+ name: str,
288
+ **kwargs: Any
289
+ ) -> Iterator[None]:
290
+ """Context manager that tracks a feature activity."""
291
+ start_time = time.time()
292
+ error = None
293
+ try:
294
+ yield None
295
+ except BaseException as e: # pylint: disable=broad-except
296
+ error = e
297
+ raise
298
+ finally:
299
+ self.on_activity(
300
+ name=name,
301
+ duration=time.time() - start_time,
302
+ error=error,
303
+ **kwargs
304
+ )
@@ -0,0 +1,228 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import unittest
15
+
16
+ from langfun.env import test_utils
17
+
18
+ TestingEnvironment = test_utils.TestingEnvironment
19
+ TestingNonSandboxBasedFeature = test_utils.TestingNonSandboxBasedFeature
20
+ TestingEventHandler = test_utils.TestingEventHandler
21
+
22
+
23
+ class NonSandboxBasedFeatureTests(unittest.TestCase):
24
+
25
+ def test_basics(self):
26
+ feature = TestingNonSandboxBasedFeature()
27
+ event_handler = TestingEventHandler(
28
+ log_session_setup=True,
29
+ log_feature_setup=True,
30
+ log_sandbox_status=True
31
+ )
32
+ env = TestingEnvironment(
33
+ image_ids=[],
34
+ features={'test_feature': feature},
35
+ event_handler=event_handler,
36
+ )
37
+ self.assertFalse(env.is_online)
38
+ self.assertEqual(len(list(env.non_sandbox_based_features())), 1)
39
+ with env:
40
+ self.assertTrue(env.is_online)
41
+ with env.test_feature('session1') as feature:
42
+ self.assertIsNone(feature.sandbox)
43
+ self.assertEqual(feature.session_id, 'session1')
44
+
45
+ self.assertEqual(
46
+ event_handler.logs,
47
+ [
48
+ '[testing-env/test_feature] feature setup',
49
+ '[testing-env] environment started',
50
+ '[testing-env/test_feature@session1] feature setup session',
51
+ '[testing-env/test_feature@session1] feature teardown session',
52
+ '[testing-env/test_feature] feature teardown',
53
+ '[testing-env] environment shutdown'
54
+ ]
55
+ )
56
+
57
+ def test_feature_setup_error(self):
58
+ event_handler = TestingEventHandler(
59
+ log_session_setup=True,
60
+ log_feature_setup=True,
61
+ log_sandbox_status=True
62
+ )
63
+ env = TestingEnvironment(
64
+ image_ids=[],
65
+ features={
66
+ 'test_feature': TestingNonSandboxBasedFeature(
67
+ simulate_setup_error=ValueError
68
+ )
69
+ },
70
+ event_handler=event_handler,
71
+ )
72
+ with self.assertRaises(ValueError):
73
+ with env:
74
+ pass
75
+ self.assertEqual(
76
+ event_handler.logs,
77
+ [
78
+ '[testing-env/test_feature] feature setup with ValueError',
79
+ '[testing-env] environment started with ValueError',
80
+ '[testing-env/test_feature] feature teardown',
81
+ '[testing-env] environment shutdown'
82
+ ]
83
+ )
84
+
85
+ def test_feature_teardown_error(self):
86
+ event_handler = TestingEventHandler(
87
+ log_session_setup=True,
88
+ log_feature_setup=True,
89
+ log_sandbox_status=True
90
+ )
91
+ env = TestingEnvironment(
92
+ image_ids=[],
93
+ features={
94
+ 'test_feature': TestingNonSandboxBasedFeature(
95
+ simulate_teardown_error=ValueError
96
+ )
97
+ },
98
+ event_handler=event_handler,
99
+ )
100
+ with env:
101
+ pass
102
+ self.assertEqual(
103
+ event_handler.logs,
104
+ [
105
+ '[testing-env/test_feature] feature setup',
106
+ '[testing-env] environment started',
107
+ '[testing-env/test_feature] feature teardown with ValueError',
108
+ '[testing-env] environment shutdown'
109
+ ]
110
+ )
111
+
112
+ def test_feature_setup_session_error(self):
113
+ event_handler = TestingEventHandler(
114
+ log_session_setup=True,
115
+ log_feature_setup=True,
116
+ log_sandbox_status=True
117
+ )
118
+ env = TestingEnvironment(
119
+ image_ids=[],
120
+ features={
121
+ 'test_feature': TestingNonSandboxBasedFeature(
122
+ simulate_setup_session_error=ValueError
123
+ )
124
+ },
125
+ event_handler=event_handler,
126
+ )
127
+ with env:
128
+ with self.assertRaises(ValueError):
129
+ with env.test_feature('session1'):
130
+ pass
131
+ self.assertEqual(
132
+ event_handler.logs,
133
+ [
134
+ # pylint: disable=line-too-long
135
+ '[testing-env/test_feature] feature setup',
136
+ '[testing-env] environment started',
137
+ '[testing-env/test_feature@session1] feature setup session with ValueError',
138
+ '[testing-env/test_feature@session1] feature teardown session',
139
+ '[testing-env/test_feature] feature teardown',
140
+ '[testing-env] environment shutdown',
141
+ # pylint: enable=line-too-long
142
+ ]
143
+ )
144
+
145
+ def test_feature_teardown_session_error(self):
146
+ event_handler = TestingEventHandler(
147
+ log_session_setup=True,
148
+ log_feature_setup=True,
149
+ log_sandbox_status=True
150
+ )
151
+ env = TestingEnvironment(
152
+ image_ids=[],
153
+ features={
154
+ 'test_feature': TestingNonSandboxBasedFeature(
155
+ simulate_teardown_session_error=ValueError
156
+ )
157
+ },
158
+ event_handler=event_handler,
159
+ )
160
+ with env:
161
+ with env.test_feature('session1'):
162
+ pass
163
+ self.assertEqual(
164
+ event_handler.logs,
165
+ [
166
+ # pylint: disable=line-too-long
167
+ '[testing-env/test_feature] feature setup',
168
+ '[testing-env] environment started',
169
+ '[testing-env/test_feature@session1] feature setup session',
170
+ '[testing-env/test_feature@session1] feature teardown session with ValueError',
171
+ '[testing-env/test_feature] feature teardown',
172
+ '[testing-env] environment shutdown',
173
+ # pylint: enable=line-too-long
174
+ ]
175
+ )
176
+
177
+ def test_feature_housekeeping(self):
178
+ event_handler = TestingEventHandler(
179
+ log_sandbox_status=False,
180
+ log_feature_setup=False,
181
+ log_housekeep=True
182
+ )
183
+ env = TestingEnvironment(
184
+ image_ids=[],
185
+ features={
186
+ 'test_feature': TestingNonSandboxBasedFeature(
187
+ housekeep_interval=0.1
188
+ )
189
+ },
190
+ event_handler=event_handler,
191
+ housekeep_interval=0.2
192
+ )
193
+ with env:
194
+ env.wait_for_housekeeping()
195
+ self.assertIn(
196
+ '[testing-env/test_feature] feature housekeeping 0',
197
+ event_handler.logs
198
+ )
199
+
200
+ def test_feature_housekeeping_error(self):
201
+ event_handler = TestingEventHandler(
202
+ log_sandbox_status=False,
203
+ log_feature_setup=False,
204
+ log_housekeep=True
205
+ )
206
+ env = TestingEnvironment(
207
+ image_ids=[],
208
+ features={
209
+ 'test_feature': TestingNonSandboxBasedFeature(
210
+ simulate_housekeep_error=ValueError,
211
+ housekeep_interval=0.1
212
+ )
213
+ },
214
+ event_handler=event_handler,
215
+ housekeep_interval=0.2
216
+ )
217
+ with env:
218
+ env.wait_for_housekeeping()
219
+ self.assertFalse(env.is_online)
220
+ self.assertIn(
221
+ '[testing-env/test_feature] feature housekeeping 0 with ValueError',
222
+ event_handler.logs
223
+ )
224
+
225
+
226
+ if __name__ == '__main__':
227
+ unittest.main()
228
+