logxpy 0.1.0__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.
- logxpy/__init__.py +126 -0
- logxpy/_action.py +958 -0
- logxpy/_async.py +186 -0
- logxpy/_base.py +80 -0
- logxpy/_compat.py +71 -0
- logxpy/_config.py +45 -0
- logxpy/_dest.py +88 -0
- logxpy/_errors.py +58 -0
- logxpy/_fmt.py +68 -0
- logxpy/_generators.py +136 -0
- logxpy/_mask.py +23 -0
- logxpy/_message.py +195 -0
- logxpy/_output.py +517 -0
- logxpy/_pool.py +93 -0
- logxpy/_traceback.py +126 -0
- logxpy/_types.py +71 -0
- logxpy/_util.py +56 -0
- logxpy/_validation.py +486 -0
- logxpy/_version.py +21 -0
- logxpy/cli.py +61 -0
- logxpy/dask.py +172 -0
- logxpy/decorators.py +268 -0
- logxpy/filter.py +124 -0
- logxpy/journald.py +88 -0
- logxpy/json.py +149 -0
- logxpy/loggerx.py +253 -0
- logxpy/logwriter.py +84 -0
- logxpy/parse.py +191 -0
- logxpy/prettyprint.py +173 -0
- logxpy/serializers.py +36 -0
- logxpy/stdlib.py +23 -0
- logxpy/tai64n.py +45 -0
- logxpy/testing.py +472 -0
- logxpy/tests/__init__.py +9 -0
- logxpy/tests/common.py +36 -0
- logxpy/tests/strategies.py +231 -0
- logxpy/tests/test_action.py +1751 -0
- logxpy/tests/test_api.py +86 -0
- logxpy/tests/test_async.py +67 -0
- logxpy/tests/test_compat.py +13 -0
- logxpy/tests/test_config.py +21 -0
- logxpy/tests/test_coroutines.py +105 -0
- logxpy/tests/test_dask.py +211 -0
- logxpy/tests/test_decorators.py +54 -0
- logxpy/tests/test_filter.py +122 -0
- logxpy/tests/test_fmt.py +42 -0
- logxpy/tests/test_generators.py +292 -0
- logxpy/tests/test_journald.py +246 -0
- logxpy/tests/test_json.py +208 -0
- logxpy/tests/test_loggerx.py +44 -0
- logxpy/tests/test_logwriter.py +262 -0
- logxpy/tests/test_message.py +334 -0
- logxpy/tests/test_output.py +921 -0
- logxpy/tests/test_parse.py +309 -0
- logxpy/tests/test_pool.py +55 -0
- logxpy/tests/test_prettyprint.py +303 -0
- logxpy/tests/test_pyinstaller.py +35 -0
- logxpy/tests/test_serializers.py +36 -0
- logxpy/tests/test_stdlib.py +73 -0
- logxpy/tests/test_tai64n.py +66 -0
- logxpy/tests/test_testing.py +1051 -0
- logxpy/tests/test_traceback.py +251 -0
- logxpy/tests/test_twisted.py +814 -0
- logxpy/tests/test_util.py +45 -0
- logxpy/tests/test_validation.py +989 -0
- logxpy/twisted.py +265 -0
- logxpy-0.1.0.dist-info/METADATA +100 -0
- logxpy-0.1.0.dist-info/RECORD +72 -0
- logxpy-0.1.0.dist-info/WHEEL +5 -0
- logxpy-0.1.0.dist-info/entry_points.txt +2 -0
- logxpy-0.1.0.dist-info/licenses/LICENSE +201 -0
- logxpy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for L{eliot._action}.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pickle
|
|
6
|
+
import time
|
|
7
|
+
from unittest import TestCase
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
from threading import Thread
|
|
10
|
+
|
|
11
|
+
from hypothesis import assume, given, settings, HealthCheck
|
|
12
|
+
from hypothesis.strategies import integers, lists, just, text
|
|
13
|
+
|
|
14
|
+
from pyrsistent import pvector, v
|
|
15
|
+
|
|
16
|
+
import testtools
|
|
17
|
+
from testtools.matchers import MatchesStructure
|
|
18
|
+
|
|
19
|
+
from .._action import (
|
|
20
|
+
Action,
|
|
21
|
+
current_action,
|
|
22
|
+
startTask,
|
|
23
|
+
start_action,
|
|
24
|
+
ACTION_STATUS_FIELD,
|
|
25
|
+
ACTION_TYPE_FIELD,
|
|
26
|
+
FAILED_STATUS,
|
|
27
|
+
STARTED_STATUS,
|
|
28
|
+
SUCCEEDED_STATUS,
|
|
29
|
+
DuplicateChild,
|
|
30
|
+
InvalidStartMessage,
|
|
31
|
+
InvalidStatus,
|
|
32
|
+
TaskLevel,
|
|
33
|
+
WrittenAction,
|
|
34
|
+
WrongActionType,
|
|
35
|
+
WrongTask,
|
|
36
|
+
WrongTaskLevel,
|
|
37
|
+
TooManyCalls,
|
|
38
|
+
log_call,
|
|
39
|
+
)
|
|
40
|
+
from .._message import (
|
|
41
|
+
EXCEPTION_FIELD,
|
|
42
|
+
REASON_FIELD,
|
|
43
|
+
TASK_LEVEL_FIELD,
|
|
44
|
+
TASK_UUID_FIELD,
|
|
45
|
+
MESSAGE_TYPE_FIELD,
|
|
46
|
+
Message,
|
|
47
|
+
)
|
|
48
|
+
from .._output import MemoryLogger
|
|
49
|
+
from .._validation import ActionType, Field, _ActionSerializers
|
|
50
|
+
from ..testing import assertContainsFields, capture_logging
|
|
51
|
+
from ..parse import Parser
|
|
52
|
+
from .. import (
|
|
53
|
+
add_destination,
|
|
54
|
+
remove_destination,
|
|
55
|
+
register_exception_extractor,
|
|
56
|
+
preserve_context,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
from .strategies import (
|
|
60
|
+
message_dicts,
|
|
61
|
+
start_action_message_dicts,
|
|
62
|
+
start_action_messages,
|
|
63
|
+
task_level_indexes,
|
|
64
|
+
task_level_lists,
|
|
65
|
+
written_actions,
|
|
66
|
+
written_messages,
|
|
67
|
+
reparent_action,
|
|
68
|
+
sibling_task_level,
|
|
69
|
+
union,
|
|
70
|
+
written_from_pmap,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ActionTests(TestCase):
|
|
75
|
+
"""
|
|
76
|
+
Tests for L{Action}.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def test_start(self):
|
|
80
|
+
"""
|
|
81
|
+
L{Action._start} logs an C{action_status="started"} message.
|
|
82
|
+
"""
|
|
83
|
+
logger = MemoryLogger()
|
|
84
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
85
|
+
action._start({"key": "value"})
|
|
86
|
+
assertContainsFields(
|
|
87
|
+
self,
|
|
88
|
+
logger.messages[0],
|
|
89
|
+
{
|
|
90
|
+
"task_uuid": "unique",
|
|
91
|
+
"task_level": [1],
|
|
92
|
+
"action_type": "sys:thename",
|
|
93
|
+
"action_status": "started",
|
|
94
|
+
"key": "value",
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_task_uuid(self):
|
|
99
|
+
"""
|
|
100
|
+
L{Action.task_uuid} return the task's UUID.
|
|
101
|
+
"""
|
|
102
|
+
logger = MemoryLogger()
|
|
103
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
104
|
+
self.assertEqual(action.task_uuid, "unique")
|
|
105
|
+
|
|
106
|
+
def test_startMessageSerialization(self):
|
|
107
|
+
"""
|
|
108
|
+
The start message logged by L{Action._start} is created with the
|
|
109
|
+
appropriate start message L{eliot._validation._MessageSerializer}.
|
|
110
|
+
"""
|
|
111
|
+
serializers = ActionType(
|
|
112
|
+
"sys:thename", [Field("key", lambda x: x, "")], [], ""
|
|
113
|
+
)._serializers
|
|
114
|
+
|
|
115
|
+
class Logger(list):
|
|
116
|
+
def write(self, msg, serializer):
|
|
117
|
+
self.append(serializer)
|
|
118
|
+
|
|
119
|
+
logger = Logger()
|
|
120
|
+
action = Action(
|
|
121
|
+
logger, "unique", TaskLevel(level=[]), "sys:thename", serializers
|
|
122
|
+
)
|
|
123
|
+
action._start({"key": "value"})
|
|
124
|
+
self.assertIs(logger[0], serializers.start)
|
|
125
|
+
|
|
126
|
+
def test_child(self):
|
|
127
|
+
"""
|
|
128
|
+
L{Action.child} returns a new L{Action} with the given logger, system
|
|
129
|
+
and name, and a task_uuid taken from the parent L{Action}.
|
|
130
|
+
"""
|
|
131
|
+
logger = MemoryLogger()
|
|
132
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
133
|
+
logger2 = MemoryLogger()
|
|
134
|
+
child = action.child(logger2, "newsystem:newname")
|
|
135
|
+
self.assertEqual(
|
|
136
|
+
[child._logger, child._identification, child._task_level],
|
|
137
|
+
[
|
|
138
|
+
logger2,
|
|
139
|
+
{"task_uuid": "unique", "action_type": "newsystem:newname"},
|
|
140
|
+
TaskLevel(level=[1]),
|
|
141
|
+
],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def test_childLevel(self):
|
|
145
|
+
"""
|
|
146
|
+
Each call to L{Action.child} increments the new sub-level set on the
|
|
147
|
+
child.
|
|
148
|
+
"""
|
|
149
|
+
logger = MemoryLogger()
|
|
150
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
151
|
+
child1 = action.child(logger, "newsystem:newname")
|
|
152
|
+
child2 = action.child(logger, "newsystem:newname")
|
|
153
|
+
child1_1 = child1.child(logger, "newsystem:other")
|
|
154
|
+
self.assertEqual(child1._task_level, TaskLevel(level=[1]))
|
|
155
|
+
self.assertEqual(child2._task_level, TaskLevel(level=[2]))
|
|
156
|
+
self.assertEqual(child1_1._task_level, TaskLevel(level=[1, 1]))
|
|
157
|
+
|
|
158
|
+
def test_childSerializers(self):
|
|
159
|
+
"""
|
|
160
|
+
L{Action.child} returns a new L{Action} with the serializers passed to
|
|
161
|
+
it, rather than the parent's.
|
|
162
|
+
"""
|
|
163
|
+
logger = MemoryLogger()
|
|
164
|
+
serializers = object()
|
|
165
|
+
action = Action(
|
|
166
|
+
logger, "unique", TaskLevel(level=[]), "sys:thename", serializers
|
|
167
|
+
)
|
|
168
|
+
childSerializers = object()
|
|
169
|
+
child = action.child(logger, "newsystem:newname", childSerializers)
|
|
170
|
+
self.assertIs(child._serializers, childSerializers)
|
|
171
|
+
|
|
172
|
+
def test_run(self):
|
|
173
|
+
"""
|
|
174
|
+
L{Action.run} runs the given function with given arguments, returning
|
|
175
|
+
its result.
|
|
176
|
+
"""
|
|
177
|
+
action = Action(None, "", TaskLevel(level=[]), "")
|
|
178
|
+
|
|
179
|
+
def f(*args, **kwargs):
|
|
180
|
+
return args, kwargs
|
|
181
|
+
|
|
182
|
+
result = action.run(f, 1, 2, x=3)
|
|
183
|
+
self.assertEqual(result, ((1, 2), {"x": 3}))
|
|
184
|
+
|
|
185
|
+
def test_runContext(self):
|
|
186
|
+
"""
|
|
187
|
+
L{Action.run} runs the given function with the action set as the
|
|
188
|
+
current action.
|
|
189
|
+
"""
|
|
190
|
+
result = []
|
|
191
|
+
action = Action(None, "", TaskLevel(level=[]), "")
|
|
192
|
+
action.run(lambda: result.append(current_action()))
|
|
193
|
+
self.assertEqual(result, [action])
|
|
194
|
+
|
|
195
|
+
def test_per_thread_context(self):
|
|
196
|
+
"""Different threads have different contexts."""
|
|
197
|
+
in_thread = []
|
|
198
|
+
|
|
199
|
+
def run_in_thread():
|
|
200
|
+
action = Action(None, "", TaskLevel(level=[]), "")
|
|
201
|
+
with action.context():
|
|
202
|
+
time.sleep(0.5)
|
|
203
|
+
in_thread.append(current_action())
|
|
204
|
+
|
|
205
|
+
thread = Thread(target=run_in_thread)
|
|
206
|
+
thread.start()
|
|
207
|
+
time.sleep(0.2)
|
|
208
|
+
self.assertEqual(current_action(), None)
|
|
209
|
+
thread.join()
|
|
210
|
+
self.assertIsInstance(in_thread[0], Action)
|
|
211
|
+
|
|
212
|
+
def test_runContextUnsetOnReturn(self):
|
|
213
|
+
"""
|
|
214
|
+
L{Action.run} unsets the action once the given function returns.
|
|
215
|
+
"""
|
|
216
|
+
action = Action(None, "", TaskLevel(level=[]), "")
|
|
217
|
+
action.run(lambda: None)
|
|
218
|
+
self.assertIs(current_action(), None)
|
|
219
|
+
|
|
220
|
+
def test_runContextUnsetOnRaise(self):
|
|
221
|
+
"""
|
|
222
|
+
L{Action.run} unsets the action once the given function raises an
|
|
223
|
+
exception.
|
|
224
|
+
"""
|
|
225
|
+
action = Action(None, "", TaskLevel(level=[]), "")
|
|
226
|
+
self.assertRaises(ZeroDivisionError, action.run, lambda: 1 / 0)
|
|
227
|
+
self.assertIs(current_action(), None)
|
|
228
|
+
|
|
229
|
+
def test_withSetsContext(self):
|
|
230
|
+
"""
|
|
231
|
+
L{Action.__enter__} sets the action as the current action.
|
|
232
|
+
"""
|
|
233
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
234
|
+
with action:
|
|
235
|
+
self.assertIs(current_action(), action)
|
|
236
|
+
|
|
237
|
+
def test_withUnsetOnReturn(self):
|
|
238
|
+
"""
|
|
239
|
+
L{Action.__exit__} unsets the action on successful block finish.
|
|
240
|
+
"""
|
|
241
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
242
|
+
with action:
|
|
243
|
+
pass
|
|
244
|
+
self.assertIs(current_action(), None)
|
|
245
|
+
|
|
246
|
+
def test_withUnsetOnRaise(self):
|
|
247
|
+
"""
|
|
248
|
+
L{Action.__exit__} unsets the action if the block raises an exception.
|
|
249
|
+
"""
|
|
250
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
251
|
+
try:
|
|
252
|
+
with action:
|
|
253
|
+
1 / 0
|
|
254
|
+
except ZeroDivisionError:
|
|
255
|
+
pass
|
|
256
|
+
else:
|
|
257
|
+
self.fail("no exception")
|
|
258
|
+
self.assertIs(current_action(), None)
|
|
259
|
+
|
|
260
|
+
def test_withContextSetsContext(self):
|
|
261
|
+
"""
|
|
262
|
+
L{Action.context().__enter__} sets the action as the current action.
|
|
263
|
+
"""
|
|
264
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
265
|
+
with action.context():
|
|
266
|
+
self.assertIs(current_action(), action)
|
|
267
|
+
|
|
268
|
+
def test_withContextReturnsaction(self):
|
|
269
|
+
"""
|
|
270
|
+
L{Action.context().__enter__} returns the action.
|
|
271
|
+
"""
|
|
272
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
273
|
+
with action.context() as action2:
|
|
274
|
+
self.assertIs(action, action2)
|
|
275
|
+
|
|
276
|
+
def test_withContextUnsetOnReturn(self):
|
|
277
|
+
"""
|
|
278
|
+
L{Action.context().__exit__} unsets the action on successful block
|
|
279
|
+
finish.
|
|
280
|
+
"""
|
|
281
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
282
|
+
with action.context():
|
|
283
|
+
pass
|
|
284
|
+
self.assertIs(current_action(), None)
|
|
285
|
+
|
|
286
|
+
def test_withContextNoLogging(self):
|
|
287
|
+
"""
|
|
288
|
+
L{Action.context().__exit__} does not log any messages.
|
|
289
|
+
"""
|
|
290
|
+
logger = MemoryLogger()
|
|
291
|
+
action = Action(logger, "", TaskLevel(level=[]), "")
|
|
292
|
+
with action.context():
|
|
293
|
+
pass
|
|
294
|
+
self.assertFalse(logger.messages)
|
|
295
|
+
|
|
296
|
+
def test_withContextUnsetOnRaise(self):
|
|
297
|
+
"""
|
|
298
|
+
L{Action.conext().__exit__} unsets the action if the block raises an
|
|
299
|
+
exception.
|
|
300
|
+
"""
|
|
301
|
+
action = Action(MemoryLogger(), "", TaskLevel(level=[]), "")
|
|
302
|
+
try:
|
|
303
|
+
with action.context():
|
|
304
|
+
1 / 0
|
|
305
|
+
except ZeroDivisionError:
|
|
306
|
+
pass
|
|
307
|
+
else:
|
|
308
|
+
self.fail("no exception")
|
|
309
|
+
self.assertIs(current_action(), None)
|
|
310
|
+
|
|
311
|
+
def test_finish(self):
|
|
312
|
+
"""
|
|
313
|
+
L{Action.finish} with no exception logs an C{action_status="succeeded"}
|
|
314
|
+
message.
|
|
315
|
+
"""
|
|
316
|
+
logger = MemoryLogger()
|
|
317
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
318
|
+
action.finish()
|
|
319
|
+
assertContainsFields(
|
|
320
|
+
self,
|
|
321
|
+
logger.messages[0],
|
|
322
|
+
{
|
|
323
|
+
"task_uuid": "unique",
|
|
324
|
+
"task_level": [1],
|
|
325
|
+
"action_type": "sys:thename",
|
|
326
|
+
"action_status": "succeeded",
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def test_successfulFinishSerializer(self):
|
|
331
|
+
"""
|
|
332
|
+
L{Action.finish} with no exception passes the success
|
|
333
|
+
L{eliot._validation._MessageSerializer} to the message it creates.
|
|
334
|
+
"""
|
|
335
|
+
serializers = ActionType(
|
|
336
|
+
"sys:thename", [], [Field("key", lambda x: x, "")], ""
|
|
337
|
+
)._serializers
|
|
338
|
+
|
|
339
|
+
class Logger(list):
|
|
340
|
+
def write(self, msg, serializer):
|
|
341
|
+
self.append(serializer)
|
|
342
|
+
|
|
343
|
+
logger = Logger()
|
|
344
|
+
action = Action(
|
|
345
|
+
logger, "unique", TaskLevel(level=[]), "sys:thename", serializers
|
|
346
|
+
)
|
|
347
|
+
action.finish()
|
|
348
|
+
self.assertIs(logger[0], serializers.success)
|
|
349
|
+
|
|
350
|
+
def test_failureFinishSerializer(self):
|
|
351
|
+
"""
|
|
352
|
+
L{Action.finish} with an exception passes the failure
|
|
353
|
+
L{eliot._validation._MessageSerializer} to the message it creates.
|
|
354
|
+
"""
|
|
355
|
+
serializers = ActionType(
|
|
356
|
+
"sys:thename", [], [Field("key", lambda x: x, "")], ""
|
|
357
|
+
)._serializers
|
|
358
|
+
|
|
359
|
+
class Logger(list):
|
|
360
|
+
def write(self, msg, serializer):
|
|
361
|
+
self.append(serializer)
|
|
362
|
+
|
|
363
|
+
logger = Logger()
|
|
364
|
+
action = Action(
|
|
365
|
+
logger, "unique", TaskLevel(level=[]), "sys:thename", serializers
|
|
366
|
+
)
|
|
367
|
+
action.finish(Exception())
|
|
368
|
+
self.assertIs(logger[0], serializers.failure)
|
|
369
|
+
|
|
370
|
+
def test_startFieldsNotInFinish(self):
|
|
371
|
+
"""
|
|
372
|
+
L{Action.finish} logs a message without the fields from
|
|
373
|
+
L{Action._start}.
|
|
374
|
+
"""
|
|
375
|
+
logger = MemoryLogger()
|
|
376
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
377
|
+
action._start({"key": "value"})
|
|
378
|
+
action.finish()
|
|
379
|
+
self.assertNotIn("key", logger.messages[1])
|
|
380
|
+
|
|
381
|
+
def test_finishWithBadException(self):
|
|
382
|
+
"""
|
|
383
|
+
L{Action.finish} still logs a message if the given exception raises
|
|
384
|
+
another exception when called with C{str()}.
|
|
385
|
+
"""
|
|
386
|
+
logger = MemoryLogger()
|
|
387
|
+
action = Action(logger, "unique", TaskLevel(level=[]), "sys:thename")
|
|
388
|
+
|
|
389
|
+
class BadException(Exception):
|
|
390
|
+
def __str__(self):
|
|
391
|
+
raise TypeError()
|
|
392
|
+
|
|
393
|
+
action.finish(BadException())
|
|
394
|
+
self.assertEqual(
|
|
395
|
+
logger.messages[0]["reason"], "eliot: unknown, str() raised exception"
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def test_withLogsSuccessfulFinishMessage(self):
|
|
399
|
+
"""
|
|
400
|
+
L{Action.__exit__} logs an action finish message on a successful block
|
|
401
|
+
finish.
|
|
402
|
+
"""
|
|
403
|
+
logger = MemoryLogger()
|
|
404
|
+
action = Action(logger, "uuid", TaskLevel(level=[1]), "sys:me")
|
|
405
|
+
with action:
|
|
406
|
+
pass
|
|
407
|
+
# Start message is only created if we use the action()/task() utility
|
|
408
|
+
# functions, the intended public APIs.
|
|
409
|
+
self.assertEqual(len(logger.messages), 1)
|
|
410
|
+
assertContainsFields(
|
|
411
|
+
self,
|
|
412
|
+
logger.messages[0],
|
|
413
|
+
{
|
|
414
|
+
"task_uuid": "uuid",
|
|
415
|
+
"task_level": [1, 1],
|
|
416
|
+
"action_type": "sys:me",
|
|
417
|
+
"action_status": "succeeded",
|
|
418
|
+
},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def test_withLogsExceptionMessage(self):
|
|
422
|
+
"""
|
|
423
|
+
L{Action.__exit__} logs an action finish message on an exception
|
|
424
|
+
raised from the block.
|
|
425
|
+
"""
|
|
426
|
+
logger = MemoryLogger()
|
|
427
|
+
action = Action(logger, "uuid", TaskLevel(level=[1]), "sys:me")
|
|
428
|
+
exception = RuntimeError("because")
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
with action:
|
|
432
|
+
raise exception
|
|
433
|
+
except RuntimeError:
|
|
434
|
+
pass
|
|
435
|
+
else:
|
|
436
|
+
self.fail("no exception")
|
|
437
|
+
|
|
438
|
+
self.assertEqual(len(logger.messages), 1)
|
|
439
|
+
assertContainsFields(
|
|
440
|
+
self,
|
|
441
|
+
logger.messages[0],
|
|
442
|
+
{
|
|
443
|
+
"task_uuid": "uuid",
|
|
444
|
+
"task_level": [1, 1],
|
|
445
|
+
"action_type": "sys:me",
|
|
446
|
+
"action_status": "failed",
|
|
447
|
+
"reason": "because",
|
|
448
|
+
"exception": "%s.RuntimeError" % (RuntimeError.__module__,),
|
|
449
|
+
},
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def test_withReturnValue(self):
|
|
453
|
+
"""
|
|
454
|
+
L{Action.__enter__} returns the action itself.
|
|
455
|
+
"""
|
|
456
|
+
logger = MemoryLogger()
|
|
457
|
+
action = Action(logger, "uuid", TaskLevel(level=[1]), "sys:me")
|
|
458
|
+
with action as act:
|
|
459
|
+
self.assertIs(action, act)
|
|
460
|
+
|
|
461
|
+
def test_addSuccessFields(self):
|
|
462
|
+
"""
|
|
463
|
+
On a successful finish, L{Action.__exit__} adds fields from
|
|
464
|
+
L{Action.addSuccessFields} to the result message.
|
|
465
|
+
"""
|
|
466
|
+
logger = MemoryLogger()
|
|
467
|
+
action = Action(logger, "uuid", TaskLevel(level=[1]), "sys:me")
|
|
468
|
+
with action as act:
|
|
469
|
+
act.addSuccessFields(x=1, y=2)
|
|
470
|
+
act.addSuccessFields(z=3)
|
|
471
|
+
assertContainsFields(self, logger.messages[0], {"x": 1, "y": 2, "z": 3})
|
|
472
|
+
|
|
473
|
+
def test_nextTaskLevel(self):
|
|
474
|
+
"""
|
|
475
|
+
Each call to L{Action._nextTaskLevel()} increments a counter.
|
|
476
|
+
"""
|
|
477
|
+
action = Action(MemoryLogger(), "uuid", TaskLevel(level=[1]), "sys:me")
|
|
478
|
+
self.assertEqual(
|
|
479
|
+
[action._nextTaskLevel() for i in range(5)],
|
|
480
|
+
[
|
|
481
|
+
TaskLevel(level=level)
|
|
482
|
+
for level in ([1, 1], [1, 2], [1, 3], [1, 4], [1, 5])
|
|
483
|
+
],
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def test_multipleFinishCalls(self):
|
|
487
|
+
"""
|
|
488
|
+
If L{Action.finish} is called, subsequent calls to L{Action.finish}
|
|
489
|
+
have no effect.
|
|
490
|
+
"""
|
|
491
|
+
logger = MemoryLogger()
|
|
492
|
+
action = Action(logger, "uuid", TaskLevel(level=[1]), "sys:me")
|
|
493
|
+
with action as act:
|
|
494
|
+
act.finish()
|
|
495
|
+
act.finish(Exception())
|
|
496
|
+
act.finish()
|
|
497
|
+
# Only initial finish message is logged:
|
|
498
|
+
self.assertEqual(len(logger.messages), 1)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class StartActionAndTaskTests(TestCase):
|
|
502
|
+
"""
|
|
503
|
+
Tests for L{start_action} and L{startTask}.
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
def test_startTaskNewAction(self):
|
|
507
|
+
"""
|
|
508
|
+
L{startTask} creates a new top-level L{Action}.
|
|
509
|
+
"""
|
|
510
|
+
logger = MemoryLogger()
|
|
511
|
+
action = startTask(logger, "sys:do")
|
|
512
|
+
self.assertIsInstance(action, Action)
|
|
513
|
+
self.assertEqual(action._task_level, TaskLevel(level=[]))
|
|
514
|
+
|
|
515
|
+
def test_start_task_default_action_type(self):
|
|
516
|
+
"""
|
|
517
|
+
L{start_task} sets a default C{action_type} if none is set.
|
|
518
|
+
"""
|
|
519
|
+
logger = MemoryLogger()
|
|
520
|
+
startTask(logger)
|
|
521
|
+
assertContainsFields(self, logger.messages[0], {"action_type": ""})
|
|
522
|
+
|
|
523
|
+
def test_startTaskSerializers(self):
|
|
524
|
+
"""
|
|
525
|
+
If serializers are passed to L{startTask} they are attached to the
|
|
526
|
+
resulting L{Action}.
|
|
527
|
+
"""
|
|
528
|
+
logger = MemoryLogger()
|
|
529
|
+
serializers = _ActionSerializers(start=None, success=None, failure=None)
|
|
530
|
+
action = startTask(logger, "sys:do", serializers)
|
|
531
|
+
self.assertIs(action._serializers, serializers)
|
|
532
|
+
|
|
533
|
+
def test_startActionSerializers(self):
|
|
534
|
+
"""
|
|
535
|
+
If serializers are passed to L{start_action} they are attached to the
|
|
536
|
+
resulting L{Action}.
|
|
537
|
+
"""
|
|
538
|
+
logger = MemoryLogger()
|
|
539
|
+
serializers = _ActionSerializers(start=None, success=None, failure=None)
|
|
540
|
+
action = start_action(logger, "sys:do", serializers)
|
|
541
|
+
self.assertIs(action._serializers, serializers)
|
|
542
|
+
|
|
543
|
+
def test_startTaskNewUUID(self):
|
|
544
|
+
"""
|
|
545
|
+
L{startTask} creates an L{Action} with its own C{task_uuid}.
|
|
546
|
+
"""
|
|
547
|
+
logger = MemoryLogger()
|
|
548
|
+
action = startTask(logger, "sys:do")
|
|
549
|
+
action2 = startTask(logger, "sys:do")
|
|
550
|
+
self.assertNotEqual(
|
|
551
|
+
action._identification["task_uuid"], action2._identification["task_uuid"]
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def test_startTaskLogsStart(self):
|
|
555
|
+
"""
|
|
556
|
+
L{startTask} logs a start message for the newly created L{Action}.
|
|
557
|
+
"""
|
|
558
|
+
logger = MemoryLogger()
|
|
559
|
+
action = startTask(logger, "sys:do", key="value")
|
|
560
|
+
assertContainsFields(
|
|
561
|
+
self,
|
|
562
|
+
logger.messages[0],
|
|
563
|
+
{
|
|
564
|
+
"task_uuid": action._identification["task_uuid"],
|
|
565
|
+
"task_level": [1],
|
|
566
|
+
"action_type": "sys:do",
|
|
567
|
+
"action_status": "started",
|
|
568
|
+
"key": "value",
|
|
569
|
+
},
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def test_start_action_default_action_type(self):
|
|
573
|
+
"""
|
|
574
|
+
L{start_action} sets a default C{action_type} if none is set.
|
|
575
|
+
"""
|
|
576
|
+
logger = MemoryLogger()
|
|
577
|
+
start_action(logger)
|
|
578
|
+
assertContainsFields(self, logger.messages[0], {"action_type": ""})
|
|
579
|
+
|
|
580
|
+
def test_startActionNoParent(self):
|
|
581
|
+
"""
|
|
582
|
+
L{start_action} when C{current_action()} is C{None} creates a top-level
|
|
583
|
+
L{Action}.
|
|
584
|
+
"""
|
|
585
|
+
logger = MemoryLogger()
|
|
586
|
+
action = start_action(logger, "sys:do")
|
|
587
|
+
self.assertIsInstance(action, Action)
|
|
588
|
+
self.assertEqual(action._task_level, TaskLevel(level=[]))
|
|
589
|
+
|
|
590
|
+
def test_startActionNoParentLogStart(self):
|
|
591
|
+
"""
|
|
592
|
+
L{start_action} when C{current_action()} is C{None} logs a start
|
|
593
|
+
message.
|
|
594
|
+
"""
|
|
595
|
+
logger = MemoryLogger()
|
|
596
|
+
action = start_action(logger, "sys:do", key="value")
|
|
597
|
+
assertContainsFields(
|
|
598
|
+
self,
|
|
599
|
+
logger.messages[0],
|
|
600
|
+
{
|
|
601
|
+
"task_uuid": action._identification["task_uuid"],
|
|
602
|
+
"task_level": [1],
|
|
603
|
+
"action_type": "sys:do",
|
|
604
|
+
"action_status": "started",
|
|
605
|
+
"key": "value",
|
|
606
|
+
},
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
def test_startActionWithParent(self):
|
|
610
|
+
"""
|
|
611
|
+
L{start_action} uses the C{current_action()} as parent for a new
|
|
612
|
+
L{Action}.
|
|
613
|
+
"""
|
|
614
|
+
logger = MemoryLogger()
|
|
615
|
+
parent = Action(logger, "uuid", TaskLevel(level=[2]), "other:thing")
|
|
616
|
+
with parent:
|
|
617
|
+
action = start_action(logger, "sys:do")
|
|
618
|
+
self.assertIsInstance(action, Action)
|
|
619
|
+
self.assertEqual(action._identification["task_uuid"], "uuid")
|
|
620
|
+
self.assertEqual(action._task_level, TaskLevel(level=[2, 1]))
|
|
621
|
+
|
|
622
|
+
def test_startActionWithParentLogStart(self):
|
|
623
|
+
"""
|
|
624
|
+
L{start_action} when C{current_action()} is an L{Action} logs a start
|
|
625
|
+
message.
|
|
626
|
+
"""
|
|
627
|
+
logger = MemoryLogger()
|
|
628
|
+
parent = Action(logger, "uuid", TaskLevel(level=[]), "other:thing")
|
|
629
|
+
with parent:
|
|
630
|
+
start_action(logger, "sys:do", key="value")
|
|
631
|
+
assertContainsFields(
|
|
632
|
+
self,
|
|
633
|
+
logger.messages[0],
|
|
634
|
+
{
|
|
635
|
+
"task_uuid": "uuid",
|
|
636
|
+
"task_level": [1, 1],
|
|
637
|
+
"action_type": "sys:do",
|
|
638
|
+
"action_status": "started",
|
|
639
|
+
"key": "value",
|
|
640
|
+
},
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def test_startTaskNoLogger(self):
|
|
644
|
+
"""
|
|
645
|
+
When no logger is given L{startTask} logs to the default ``Logger``.
|
|
646
|
+
"""
|
|
647
|
+
messages = []
|
|
648
|
+
add_destination(messages.append)
|
|
649
|
+
self.addCleanup(remove_destination, messages.append)
|
|
650
|
+
action = startTask(action_type="sys:do", key="value")
|
|
651
|
+
assertContainsFields(
|
|
652
|
+
self,
|
|
653
|
+
messages[0],
|
|
654
|
+
{
|
|
655
|
+
"task_uuid": action._identification["task_uuid"],
|
|
656
|
+
"task_level": [1],
|
|
657
|
+
"action_type": "sys:do",
|
|
658
|
+
"action_status": "started",
|
|
659
|
+
"key": "value",
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
def test_startActionNoLogger(self):
|
|
664
|
+
"""
|
|
665
|
+
When no logger is given L{start_action} logs to the default ``Logger``.
|
|
666
|
+
"""
|
|
667
|
+
messages = []
|
|
668
|
+
add_destination(messages.append)
|
|
669
|
+
self.addCleanup(remove_destination, messages.append)
|
|
670
|
+
action = start_action(action_type="sys:do", key="value")
|
|
671
|
+
assertContainsFields(
|
|
672
|
+
self,
|
|
673
|
+
messages[0],
|
|
674
|
+
{
|
|
675
|
+
"task_uuid": action._identification["task_uuid"],
|
|
676
|
+
"task_level": [1],
|
|
677
|
+
"action_type": "sys:do",
|
|
678
|
+
"action_status": "started",
|
|
679
|
+
"key": "value",
|
|
680
|
+
},
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class PEP8Tests(TestCase):
|
|
685
|
+
"""
|
|
686
|
+
Tests for PEP 8 method compatibility.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
def test_add_success_fields(self):
|
|
690
|
+
"""
|
|
691
|
+
L{Action.addSuccessFields} is the same as L{Action.add_success_fields}.
|
|
692
|
+
"""
|
|
693
|
+
self.assertEqual(Action.addSuccessFields, Action.add_success_fields)
|
|
694
|
+
|
|
695
|
+
def test_serialize_task_id(self):
|
|
696
|
+
"""
|
|
697
|
+
L{Action.serialize_task_id} is the same as L{Action.serializeTaskId}.
|
|
698
|
+
"""
|
|
699
|
+
self.assertEqual(Action.serialize_task_id, Action.serializeTaskId)
|
|
700
|
+
|
|
701
|
+
def test_continue_task(self):
|
|
702
|
+
"""
|
|
703
|
+
L{Action.continue_task} is the same as L{Action.continueTask}.
|
|
704
|
+
"""
|
|
705
|
+
self.assertEqual(Action.continue_task, Action.continueTask)
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
class SerializationTests(TestCase):
|
|
709
|
+
"""
|
|
710
|
+
Tests for L{Action} serialization and deserialization.
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
def test_serializeTaskId(self):
|
|
714
|
+
"""
|
|
715
|
+
L{Action.serialize_task_id} result is composed of the task UUID and an
|
|
716
|
+
incremented task level.
|
|
717
|
+
"""
|
|
718
|
+
action = Action(None, "uniq123", TaskLevel(level=[1, 2]), "mytype")
|
|
719
|
+
self.assertEqual(
|
|
720
|
+
[
|
|
721
|
+
action._nextTaskLevel(),
|
|
722
|
+
action.serialize_task_id(),
|
|
723
|
+
action._nextTaskLevel(),
|
|
724
|
+
],
|
|
725
|
+
[TaskLevel(level=[1, 2, 1]), b"uniq123@/1/2/2", TaskLevel(level=[1, 2, 3])],
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def test_continueTaskReturnsAction(self):
|
|
729
|
+
"""
|
|
730
|
+
L{Action.continue_task} returns an L{Action} whose C{task_level} and
|
|
731
|
+
C{task_uuid} are derived from those in the given serialized task
|
|
732
|
+
identifier.
|
|
733
|
+
"""
|
|
734
|
+
originalAction = Action(None, "uniq456", TaskLevel(level=[3, 4]), "mytype")
|
|
735
|
+
taskId = originalAction.serializeTaskId()
|
|
736
|
+
|
|
737
|
+
newAction = Action.continue_task(MemoryLogger(), taskId)
|
|
738
|
+
self.assertEqual(
|
|
739
|
+
[newAction.__class__, newAction._identification, newAction._task_level],
|
|
740
|
+
[
|
|
741
|
+
Action,
|
|
742
|
+
{"task_uuid": "uniq456", "action_type": "eliot:remote_task"},
|
|
743
|
+
TaskLevel(level=[3, 4, 1]),
|
|
744
|
+
],
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
def test_continueTaskUnicode(self):
|
|
748
|
+
"""
|
|
749
|
+
L{Action.continue_task} can take a Unicode task identifier.
|
|
750
|
+
"""
|
|
751
|
+
original_action = Action(None, "uniq790", TaskLevel(level=[3, 4]), "mytype")
|
|
752
|
+
task_id = str(original_action.serialize_task_id(), "utf-8")
|
|
753
|
+
|
|
754
|
+
new_action = Action.continue_task(MemoryLogger(), task_id)
|
|
755
|
+
self.assertEqual(new_action._identification["task_uuid"], "uniq790")
|
|
756
|
+
|
|
757
|
+
def test_continueTaskStartsAction(self):
|
|
758
|
+
"""
|
|
759
|
+
L{Action.continue_task} starts the L{Action} it creates.
|
|
760
|
+
"""
|
|
761
|
+
originalAction = Action(None, "uniq456", TaskLevel(level=[3, 4]), "mytype")
|
|
762
|
+
taskId = originalAction.serializeTaskId()
|
|
763
|
+
logger = MemoryLogger()
|
|
764
|
+
|
|
765
|
+
Action.continue_task(logger, taskId)
|
|
766
|
+
assertContainsFields(
|
|
767
|
+
self,
|
|
768
|
+
logger.messages[0],
|
|
769
|
+
{
|
|
770
|
+
"task_uuid": "uniq456",
|
|
771
|
+
"task_level": [3, 4, 1, 1],
|
|
772
|
+
"action_type": "eliot:remote_task",
|
|
773
|
+
"action_status": "started",
|
|
774
|
+
},
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
def test_continueTaskCustomType(self):
|
|
778
|
+
"""
|
|
779
|
+
L{Action.continue_task} uses the provided action type and extra fields.
|
|
780
|
+
"""
|
|
781
|
+
originalAction = Action(None, "uniq456", TaskLevel(level=[3, 4]), "mytype")
|
|
782
|
+
taskId = originalAction.serializeTaskId()
|
|
783
|
+
logger = MemoryLogger()
|
|
784
|
+
|
|
785
|
+
Action.continue_task(logger, taskId, action_type="custom:action", field="value")
|
|
786
|
+
assertContainsFields(
|
|
787
|
+
self,
|
|
788
|
+
logger.messages[0],
|
|
789
|
+
{
|
|
790
|
+
"task_uuid": "uniq456",
|
|
791
|
+
"task_level": [3, 4, 1, 1],
|
|
792
|
+
"action_type": "custom:action",
|
|
793
|
+
"action_status": "started",
|
|
794
|
+
"field": "value",
|
|
795
|
+
},
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def test_continueTaskNoLogger(self):
|
|
799
|
+
"""
|
|
800
|
+
L{Action.continue_task} can be called without a logger.
|
|
801
|
+
"""
|
|
802
|
+
originalAction = Action(None, "uniq456", TaskLevel(level=[3, 4]), "mytype")
|
|
803
|
+
taskId = originalAction.serializeTaskId()
|
|
804
|
+
|
|
805
|
+
messages = []
|
|
806
|
+
add_destination(messages.append)
|
|
807
|
+
self.addCleanup(remove_destination, messages.append)
|
|
808
|
+
Action.continue_task(task_id=taskId)
|
|
809
|
+
assertContainsFields(
|
|
810
|
+
self,
|
|
811
|
+
messages[-1],
|
|
812
|
+
{
|
|
813
|
+
"task_uuid": "uniq456",
|
|
814
|
+
"task_level": [3, 4, 1, 1],
|
|
815
|
+
"action_type": "eliot:remote_task",
|
|
816
|
+
"action_status": "started",
|
|
817
|
+
},
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def test_continueTaskRequiredTaskId(self):
|
|
821
|
+
"""
|
|
822
|
+
L{Action.continue_task} requires a C{task_id} to be passed in.
|
|
823
|
+
"""
|
|
824
|
+
self.assertRaises(RuntimeError, Action.continue_task)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
class TaskLevelTests(TestCase):
|
|
828
|
+
"""
|
|
829
|
+
Tests for L{TaskLevel}.
|
|
830
|
+
"""
|
|
831
|
+
|
|
832
|
+
def assert_fully_less_than(self, x, y):
|
|
833
|
+
"""
|
|
834
|
+
Assert that x < y according to all the comparison operators.
|
|
835
|
+
"""
|
|
836
|
+
self.assertTrue(
|
|
837
|
+
all(
|
|
838
|
+
[
|
|
839
|
+
# lt
|
|
840
|
+
x < y,
|
|
841
|
+
not y < x,
|
|
842
|
+
# le
|
|
843
|
+
x <= y,
|
|
844
|
+
not y <= x,
|
|
845
|
+
# gt
|
|
846
|
+
y > x,
|
|
847
|
+
not x > y,
|
|
848
|
+
# ge
|
|
849
|
+
y >= x,
|
|
850
|
+
not x >= y,
|
|
851
|
+
# eq
|
|
852
|
+
not x == y,
|
|
853
|
+
not y == x,
|
|
854
|
+
# ne
|
|
855
|
+
x != y,
|
|
856
|
+
y != x,
|
|
857
|
+
]
|
|
858
|
+
)
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
def test_equality(self):
|
|
862
|
+
"""
|
|
863
|
+
L{TaskChild} correctly implements equality and hashing.
|
|
864
|
+
"""
|
|
865
|
+
a = TaskLevel(level=[1, 2])
|
|
866
|
+
a2 = TaskLevel(level=[1, 2])
|
|
867
|
+
b = TaskLevel(level=[2, 999])
|
|
868
|
+
self.assertTrue(
|
|
869
|
+
all(
|
|
870
|
+
[
|
|
871
|
+
a == a2,
|
|
872
|
+
a2 == a,
|
|
873
|
+
a != b,
|
|
874
|
+
b != a,
|
|
875
|
+
not b == a,
|
|
876
|
+
not a == b,
|
|
877
|
+
not a == 1,
|
|
878
|
+
a != 1,
|
|
879
|
+
hash(a) == hash(a2),
|
|
880
|
+
hash(b) != hash(a),
|
|
881
|
+
]
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def test_as_list(self):
|
|
886
|
+
"""
|
|
887
|
+
L{TaskChild.as_list} returns the level.
|
|
888
|
+
"""
|
|
889
|
+
self.assertEqual(TaskLevel(level=[1, 2, 3]).as_list(), [1, 2, 3])
|
|
890
|
+
|
|
891
|
+
@given(lists(task_level_indexes))
|
|
892
|
+
def test_parent_of_child(self, base_task_level):
|
|
893
|
+
"""
|
|
894
|
+
L{TaskLevel.child} returns the first child of the task.
|
|
895
|
+
"""
|
|
896
|
+
base_task = TaskLevel(level=base_task_level)
|
|
897
|
+
child_task = base_task.child()
|
|
898
|
+
self.assertEqual(base_task, child_task.parent())
|
|
899
|
+
|
|
900
|
+
@given(task_level_lists)
|
|
901
|
+
def test_child_greater_than_parent(self, task_level):
|
|
902
|
+
"""
|
|
903
|
+
L{TaskLevel.child} returns a child that is greater than its parent.
|
|
904
|
+
"""
|
|
905
|
+
task = TaskLevel(level=task_level)
|
|
906
|
+
self.assert_fully_less_than(task, task.child())
|
|
907
|
+
|
|
908
|
+
@given(task_level_lists)
|
|
909
|
+
def test_next_sibling_greater(self, task_level):
|
|
910
|
+
"""
|
|
911
|
+
L{TaskLevel.next_sibling} returns a greater task level.
|
|
912
|
+
"""
|
|
913
|
+
task = TaskLevel(level=task_level)
|
|
914
|
+
self.assert_fully_less_than(task, task.next_sibling())
|
|
915
|
+
|
|
916
|
+
@given(task_level_lists)
|
|
917
|
+
def test_next_sibling(self, task_level):
|
|
918
|
+
"""
|
|
919
|
+
L{TaskLevel.next_sibling} returns the next sibling of a task.
|
|
920
|
+
"""
|
|
921
|
+
task = TaskLevel(level=task_level)
|
|
922
|
+
sibling = task.next_sibling()
|
|
923
|
+
self.assertEqual(
|
|
924
|
+
sibling, TaskLevel(level=task_level[:-1] + [task_level[-1] + 1])
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
def test_parent_of_root(self):
|
|
928
|
+
"""
|
|
929
|
+
L{TaskLevel.parent} of the root task level is C{None}.
|
|
930
|
+
"""
|
|
931
|
+
self.assertIs(TaskLevel(level=[]).parent(), None)
|
|
932
|
+
|
|
933
|
+
def test_toString(self):
|
|
934
|
+
"""
|
|
935
|
+
L{TaskLevel.toString} serializes the object to a Unicode string.
|
|
936
|
+
"""
|
|
937
|
+
root = TaskLevel(level=[])
|
|
938
|
+
child2_1 = root.child().next_sibling().child()
|
|
939
|
+
self.assertEqual([root.toString(), child2_1.toString()], ["/", "/2/1"])
|
|
940
|
+
|
|
941
|
+
def test_fromString(self):
|
|
942
|
+
"""
|
|
943
|
+
L{TaskLevel.fromString} deserializes the output of
|
|
944
|
+
L{TaskLevel.toString}.
|
|
945
|
+
"""
|
|
946
|
+
self.assertEqual(
|
|
947
|
+
[TaskLevel.fromString("/"), TaskLevel.fromString("/2/1")],
|
|
948
|
+
[TaskLevel(level=[]), TaskLevel(level=[2, 1])],
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def test_from_string(self):
|
|
952
|
+
"""
|
|
953
|
+
L{TaskLevel.from_string} is the same as as L{TaskLevel.fromString}.
|
|
954
|
+
"""
|
|
955
|
+
self.assertEqual(TaskLevel.from_string, TaskLevel.fromString)
|
|
956
|
+
|
|
957
|
+
def test_to_string(self):
|
|
958
|
+
"""
|
|
959
|
+
L{TaskLevel.to_string} is the same as as L{TaskLevel.toString}.
|
|
960
|
+
"""
|
|
961
|
+
self.assertEqual(TaskLevel.to_string, TaskLevel.toString)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
class WrittenActionTests(testtools.TestCase):
|
|
965
|
+
"""
|
|
966
|
+
Tests for L{WrittenAction}.
|
|
967
|
+
"""
|
|
968
|
+
|
|
969
|
+
@given(start_action_messages)
|
|
970
|
+
def test_from_single_start_message(self, message):
|
|
971
|
+
"""
|
|
972
|
+
A L{WrittenAction} can be constructed from a single "start" message.
|
|
973
|
+
Such an action inherits the C{action_type} of the start message, has no
|
|
974
|
+
C{end_time}, and has a C{status} of C{STARTED_STATUS}.
|
|
975
|
+
"""
|
|
976
|
+
action = WrittenAction.from_messages(message)
|
|
977
|
+
self.assertThat(
|
|
978
|
+
action,
|
|
979
|
+
MatchesStructure.byEquality(
|
|
980
|
+
status=STARTED_STATUS,
|
|
981
|
+
action_type=message.contents[ACTION_TYPE_FIELD],
|
|
982
|
+
task_uuid=message.task_uuid,
|
|
983
|
+
task_level=message.task_level.parent(),
|
|
984
|
+
start_time=message.timestamp,
|
|
985
|
+
children=pvector([]),
|
|
986
|
+
end_time=None,
|
|
987
|
+
reason=None,
|
|
988
|
+
exception=None,
|
|
989
|
+
),
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
@given(start_action_messages, message_dicts, integers(min_value=2))
|
|
993
|
+
def test_from_single_end_message(self, start_message, end_message_dict, n):
|
|
994
|
+
"""
|
|
995
|
+
A L{WrittenAction} can be constructed from a single "end"
|
|
996
|
+
message. Such an action inherits the C{action_type} and
|
|
997
|
+
C{task_level} of the end message, has no C{start_time}, and has a
|
|
998
|
+
C{status} matching that of the end message.
|
|
999
|
+
"""
|
|
1000
|
+
end_message = written_from_pmap(
|
|
1001
|
+
union(
|
|
1002
|
+
end_message_dict,
|
|
1003
|
+
{
|
|
1004
|
+
ACTION_STATUS_FIELD: SUCCEEDED_STATUS,
|
|
1005
|
+
ACTION_TYPE_FIELD: start_message.contents[ACTION_TYPE_FIELD],
|
|
1006
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1007
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1008
|
+
},
|
|
1009
|
+
)
|
|
1010
|
+
)
|
|
1011
|
+
action = WrittenAction.from_messages(end_message=end_message)
|
|
1012
|
+
self.assertThat(
|
|
1013
|
+
action,
|
|
1014
|
+
MatchesStructure.byEquality(
|
|
1015
|
+
status=SUCCEEDED_STATUS,
|
|
1016
|
+
action_type=end_message.contents[ACTION_TYPE_FIELD],
|
|
1017
|
+
task_uuid=end_message.task_uuid,
|
|
1018
|
+
task_level=end_message.task_level.parent(),
|
|
1019
|
+
start_time=None,
|
|
1020
|
+
children=pvector([]),
|
|
1021
|
+
end_time=end_message.timestamp,
|
|
1022
|
+
reason=None,
|
|
1023
|
+
exception=None,
|
|
1024
|
+
),
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
@given(message_dicts)
|
|
1028
|
+
def test_from_single_child_message(self, message_dict):
|
|
1029
|
+
"""
|
|
1030
|
+
A L{WrittenAction} can be constructed from a single child
|
|
1031
|
+
message. Such an action inherits the C{task_level} of the message,
|
|
1032
|
+
has no C{start_time}, C{status}, C{task_type} or C{end_time}.
|
|
1033
|
+
"""
|
|
1034
|
+
message = written_from_pmap(message_dict)
|
|
1035
|
+
action = WrittenAction.from_messages(children=[message])
|
|
1036
|
+
self.assertThat(
|
|
1037
|
+
action,
|
|
1038
|
+
MatchesStructure.byEquality(
|
|
1039
|
+
status=None,
|
|
1040
|
+
action_type=None,
|
|
1041
|
+
task_uuid=message.task_uuid,
|
|
1042
|
+
task_level=message.task_level.parent(),
|
|
1043
|
+
start_time=None,
|
|
1044
|
+
children=pvector([message]),
|
|
1045
|
+
end_time=None,
|
|
1046
|
+
reason=None,
|
|
1047
|
+
exception=None,
|
|
1048
|
+
),
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
@given(start_action_messages, message_dicts, integers(min_value=2))
|
|
1052
|
+
def test_different_task_uuid(self, start_message, end_message_dict, n):
|
|
1053
|
+
"""
|
|
1054
|
+
By definition, an action is either a top-level task or takes place
|
|
1055
|
+
within such a task. If we try to assemble actions from messages with
|
|
1056
|
+
differing task UUIDs, we raise an error.
|
|
1057
|
+
"""
|
|
1058
|
+
assume(start_message.task_uuid != end_message_dict["task_uuid"])
|
|
1059
|
+
action_type = start_message.as_dict()[ACTION_TYPE_FIELD]
|
|
1060
|
+
end_message = written_from_pmap(
|
|
1061
|
+
union(
|
|
1062
|
+
end_message_dict.set(ACTION_TYPE_FIELD, action_type),
|
|
1063
|
+
{
|
|
1064
|
+
ACTION_STATUS_FIELD: SUCCEEDED_STATUS,
|
|
1065
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1066
|
+
},
|
|
1067
|
+
)
|
|
1068
|
+
)
|
|
1069
|
+
self.assertRaises(
|
|
1070
|
+
WrongTask,
|
|
1071
|
+
WrittenAction.from_messages,
|
|
1072
|
+
start_message,
|
|
1073
|
+
end_message=end_message,
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
@given(message_dicts)
|
|
1077
|
+
def test_invalid_start_message_missing_status(self, message_dict):
|
|
1078
|
+
"""
|
|
1079
|
+
A start message must have an C{ACTION_STATUS_FIELD} with the value
|
|
1080
|
+
C{STARTED_STATUS}, otherwise it's not a start message. If we receive
|
|
1081
|
+
such a message, raise an error.
|
|
1082
|
+
|
|
1083
|
+
This test handles the case where the status field is not present.
|
|
1084
|
+
"""
|
|
1085
|
+
assume(ACTION_STATUS_FIELD not in message_dict)
|
|
1086
|
+
message = written_from_pmap(message_dict)
|
|
1087
|
+
self.assertRaises(InvalidStartMessage, WrittenAction.from_messages, message)
|
|
1088
|
+
|
|
1089
|
+
@given(
|
|
1090
|
+
message_dict=start_action_message_dicts,
|
|
1091
|
+
status=(just(FAILED_STATUS) | just(SUCCEEDED_STATUS) | text()),
|
|
1092
|
+
)
|
|
1093
|
+
def test_invalid_start_message_wrong_status(self, message_dict, status):
|
|
1094
|
+
"""
|
|
1095
|
+
A start message must have an C{ACTION_STATUS_FIELD} with the value
|
|
1096
|
+
C{STARTED_STATUS}, otherwise it's not a start message. If we receive
|
|
1097
|
+
such a message, raise an error.
|
|
1098
|
+
|
|
1099
|
+
This test handles the case where the status field is present, but is
|
|
1100
|
+
not C{STARTED_STATUS}.
|
|
1101
|
+
"""
|
|
1102
|
+
message = written_from_pmap(message_dict.update({ACTION_STATUS_FIELD: status}))
|
|
1103
|
+
self.assertRaises(InvalidStartMessage, WrittenAction.from_messages, message)
|
|
1104
|
+
|
|
1105
|
+
@given(start_action_message_dicts, integers(min_value=2))
|
|
1106
|
+
def test_invalid_task_level_in_start_message(self, start_message_dict, i):
|
|
1107
|
+
"""
|
|
1108
|
+
All messages in an action have a task level. The first message in an
|
|
1109
|
+
action must have a task level ending in C{1}, indicating that it's the
|
|
1110
|
+
first message.
|
|
1111
|
+
|
|
1112
|
+
If we try to start an action with a message that has a task level that
|
|
1113
|
+
does not end in C{1}, raise an error.
|
|
1114
|
+
"""
|
|
1115
|
+
new_level = start_message_dict[TASK_LEVEL_FIELD].append(i)
|
|
1116
|
+
message_dict = start_message_dict.set(TASK_LEVEL_FIELD, new_level)
|
|
1117
|
+
message = written_from_pmap(message_dict)
|
|
1118
|
+
self.assertRaises(InvalidStartMessage, WrittenAction.from_messages, message)
|
|
1119
|
+
|
|
1120
|
+
@given(start_action_messages, message_dicts, text(), integers(min_value=1))
|
|
1121
|
+
def test_action_type_mismatch(self, start_message, end_message_dict, end_type, n):
|
|
1122
|
+
"""
|
|
1123
|
+
The end message of an action must have the same C{ACTION_TYPE_FIELD} as
|
|
1124
|
+
the start message of an action. If we try to end an action with a
|
|
1125
|
+
message that has a different type, we raise an error.
|
|
1126
|
+
"""
|
|
1127
|
+
assume(end_type != start_message.contents[ACTION_TYPE_FIELD])
|
|
1128
|
+
end_message = written_from_pmap(
|
|
1129
|
+
union(
|
|
1130
|
+
end_message_dict,
|
|
1131
|
+
{
|
|
1132
|
+
ACTION_STATUS_FIELD: SUCCEEDED_STATUS,
|
|
1133
|
+
ACTION_TYPE_FIELD: end_type,
|
|
1134
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1135
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1136
|
+
},
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
self.assertRaises(
|
|
1140
|
+
WrongActionType,
|
|
1141
|
+
WrittenAction.from_messages,
|
|
1142
|
+
start_message,
|
|
1143
|
+
end_message=end_message,
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
@given(start_action_messages, message_dicts, integers(min_value=2))
|
|
1147
|
+
def test_successful_end(self, start_message, end_message_dict, n):
|
|
1148
|
+
"""
|
|
1149
|
+
A L{WrittenAction} can be constructed with just a start message and an
|
|
1150
|
+
end message: in this case, an end message that indicates the action was
|
|
1151
|
+
successful.
|
|
1152
|
+
|
|
1153
|
+
Such an action inherits the C{end_time} from the end message, and has
|
|
1154
|
+
a C{status} of C{SUCCEEDED_STATUS}.
|
|
1155
|
+
"""
|
|
1156
|
+
end_message = written_from_pmap(
|
|
1157
|
+
union(
|
|
1158
|
+
end_message_dict,
|
|
1159
|
+
{
|
|
1160
|
+
ACTION_STATUS_FIELD: SUCCEEDED_STATUS,
|
|
1161
|
+
ACTION_TYPE_FIELD: start_message.contents[ACTION_TYPE_FIELD],
|
|
1162
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1163
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1164
|
+
},
|
|
1165
|
+
)
|
|
1166
|
+
)
|
|
1167
|
+
action = WrittenAction.from_messages(start_message, end_message=end_message)
|
|
1168
|
+
self.assertThat(
|
|
1169
|
+
action,
|
|
1170
|
+
MatchesStructure.byEquality(
|
|
1171
|
+
action_type=start_message.contents[ACTION_TYPE_FIELD],
|
|
1172
|
+
status=SUCCEEDED_STATUS,
|
|
1173
|
+
task_uuid=start_message.task_uuid,
|
|
1174
|
+
task_level=start_message.task_level.parent(),
|
|
1175
|
+
start_time=start_message.timestamp,
|
|
1176
|
+
children=pvector([]),
|
|
1177
|
+
end_time=end_message.timestamp,
|
|
1178
|
+
reason=None,
|
|
1179
|
+
exception=None,
|
|
1180
|
+
),
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
@given(start_action_messages, message_dicts, text(), text(), integers(min_value=2))
|
|
1184
|
+
def test_failed_end(self, start_message, end_message_dict, exception, reason, n):
|
|
1185
|
+
"""
|
|
1186
|
+
A L{WrittenAction} can be constructed with just a start message and an
|
|
1187
|
+
end message: in this case, an end message that indicates that the
|
|
1188
|
+
action failed.
|
|
1189
|
+
|
|
1190
|
+
Such an action inherits the C{end_time} from the end message, has a
|
|
1191
|
+
C{status} of C{FAILED_STATUS}, and an C{exception} and C{reason} that
|
|
1192
|
+
match the raised exception.
|
|
1193
|
+
"""
|
|
1194
|
+
end_message = written_from_pmap(
|
|
1195
|
+
union(
|
|
1196
|
+
end_message_dict,
|
|
1197
|
+
{
|
|
1198
|
+
ACTION_STATUS_FIELD: FAILED_STATUS,
|
|
1199
|
+
ACTION_TYPE_FIELD: start_message.contents[ACTION_TYPE_FIELD],
|
|
1200
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1201
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1202
|
+
EXCEPTION_FIELD: exception,
|
|
1203
|
+
REASON_FIELD: reason,
|
|
1204
|
+
},
|
|
1205
|
+
)
|
|
1206
|
+
)
|
|
1207
|
+
action = WrittenAction.from_messages(start_message, end_message=end_message)
|
|
1208
|
+
self.assertThat(
|
|
1209
|
+
action,
|
|
1210
|
+
MatchesStructure.byEquality(
|
|
1211
|
+
action_type=start_message.contents[ACTION_TYPE_FIELD],
|
|
1212
|
+
status=FAILED_STATUS,
|
|
1213
|
+
task_uuid=start_message.task_uuid,
|
|
1214
|
+
task_level=start_message.task_level.parent(),
|
|
1215
|
+
start_time=start_message.timestamp,
|
|
1216
|
+
children=pvector([]),
|
|
1217
|
+
end_time=end_message.timestamp,
|
|
1218
|
+
reason=reason,
|
|
1219
|
+
exception=exception,
|
|
1220
|
+
),
|
|
1221
|
+
)
|
|
1222
|
+
|
|
1223
|
+
@given(start_action_messages, message_dicts, integers(min_value=2))
|
|
1224
|
+
def test_end_has_no_status(self, start_message, end_message_dict, n):
|
|
1225
|
+
"""
|
|
1226
|
+
If we try to end a L{WrittenAction} with a message that lacks an
|
|
1227
|
+
C{ACTION_STATUS_FIELD}, we raise an error, because it's not a valid
|
|
1228
|
+
end message.
|
|
1229
|
+
"""
|
|
1230
|
+
assume(ACTION_STATUS_FIELD not in end_message_dict)
|
|
1231
|
+
end_message = written_from_pmap(
|
|
1232
|
+
union(
|
|
1233
|
+
end_message_dict,
|
|
1234
|
+
{
|
|
1235
|
+
ACTION_TYPE_FIELD: start_message.contents[ACTION_TYPE_FIELD],
|
|
1236
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1237
|
+
TASK_LEVEL_FIELD: sibling_task_level(start_message, n),
|
|
1238
|
+
},
|
|
1239
|
+
)
|
|
1240
|
+
)
|
|
1241
|
+
self.assertRaises(
|
|
1242
|
+
InvalidStatus,
|
|
1243
|
+
WrittenAction.from_messages,
|
|
1244
|
+
start_message,
|
|
1245
|
+
end_message=end_message,
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
# This test is slow, and when run under coverage on pypy on Travis won't
|
|
1249
|
+
# make the default of 5 examples. 1 is enough.
|
|
1250
|
+
@given(start_action_messages, lists(written_messages | written_actions))
|
|
1251
|
+
@settings(suppress_health_check=[HealthCheck.too_slow])
|
|
1252
|
+
def test_children(self, start_message, child_messages):
|
|
1253
|
+
"""
|
|
1254
|
+
We can construct a L{WrittenAction} with child messages. These messages
|
|
1255
|
+
can be either L{WrittenAction}s or L{WrittenMessage}s. They are
|
|
1256
|
+
available in the C{children} field.
|
|
1257
|
+
"""
|
|
1258
|
+
messages = [
|
|
1259
|
+
reparent_action(
|
|
1260
|
+
start_message.task_uuid,
|
|
1261
|
+
TaskLevel(level=sibling_task_level(start_message, i)),
|
|
1262
|
+
message,
|
|
1263
|
+
)
|
|
1264
|
+
for (i, message) in enumerate(child_messages, 2)
|
|
1265
|
+
]
|
|
1266
|
+
action = WrittenAction.from_messages(start_message, messages)
|
|
1267
|
+
|
|
1268
|
+
def task_level(m):
|
|
1269
|
+
return m.task_level
|
|
1270
|
+
|
|
1271
|
+
self.assertEqual(sorted(messages, key=task_level), action.children)
|
|
1272
|
+
|
|
1273
|
+
@given(start_action_messages, message_dicts)
|
|
1274
|
+
def test_wrong_task_uuid(self, start_message, child_message):
|
|
1275
|
+
"""
|
|
1276
|
+
All child messages of an action must have the same C{task_uuid} as the
|
|
1277
|
+
action.
|
|
1278
|
+
"""
|
|
1279
|
+
assume(child_message[TASK_UUID_FIELD] != start_message.task_uuid)
|
|
1280
|
+
message = written_from_pmap(child_message)
|
|
1281
|
+
self.assertRaises(
|
|
1282
|
+
WrongTask, WrittenAction.from_messages, start_message, v(message)
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
@given(start_action_messages, message_dicts)
|
|
1286
|
+
def test_wrong_task_level(self, start_message, child_message):
|
|
1287
|
+
"""
|
|
1288
|
+
All child messages of an action must have a task level that is a direct
|
|
1289
|
+
child of the action's task level.
|
|
1290
|
+
"""
|
|
1291
|
+
assume(
|
|
1292
|
+
not start_message.task_level.is_sibling_of(
|
|
1293
|
+
TaskLevel(level=child_message[TASK_LEVEL_FIELD])
|
|
1294
|
+
)
|
|
1295
|
+
)
|
|
1296
|
+
message = written_from_pmap(
|
|
1297
|
+
child_message.update({TASK_UUID_FIELD: start_message.task_uuid})
|
|
1298
|
+
)
|
|
1299
|
+
self.assertRaises(
|
|
1300
|
+
WrongTaskLevel, WrittenAction.from_messages, start_message, v(message)
|
|
1301
|
+
)
|
|
1302
|
+
|
|
1303
|
+
@given(start_action_messages, message_dicts, message_dicts, integers(min_value=2))
|
|
1304
|
+
def test_duplicate_task_level(self, start_message, child1, child2, index):
|
|
1305
|
+
"""
|
|
1306
|
+
If we try to add a child to an action that has a task level that's the
|
|
1307
|
+
same as the task level of an existing child, we raise an error.
|
|
1308
|
+
"""
|
|
1309
|
+
parent_level = start_message.task_level.parent().level
|
|
1310
|
+
messages = [
|
|
1311
|
+
written_from_pmap(
|
|
1312
|
+
union(
|
|
1313
|
+
child_message,
|
|
1314
|
+
{
|
|
1315
|
+
TASK_UUID_FIELD: start_message.task_uuid,
|
|
1316
|
+
TASK_LEVEL_FIELD: parent_level.append(index),
|
|
1317
|
+
},
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1320
|
+
for child_message in [child1, child2]
|
|
1321
|
+
]
|
|
1322
|
+
assume(messages[0] != messages[1])
|
|
1323
|
+
self.assertRaises(
|
|
1324
|
+
DuplicateChild, WrittenAction.from_messages, start_message, messages
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def make_error_extraction_tests(get_messages):
|
|
1329
|
+
"""
|
|
1330
|
+
Create a test case class for testing extraction of fields from exceptions.
|
|
1331
|
+
|
|
1332
|
+
@param get_messages: Callable that takes an exception instance, returns
|
|
1333
|
+
all message dictionaries generated by logging it.
|
|
1334
|
+
|
|
1335
|
+
@return: ``TestCase`` subclass.
|
|
1336
|
+
"""
|
|
1337
|
+
|
|
1338
|
+
class ErrorFieldExtraction(TestCase):
|
|
1339
|
+
"""
|
|
1340
|
+
Tests for extracting fields from exceptions in failed actions.
|
|
1341
|
+
"""
|
|
1342
|
+
|
|
1343
|
+
def test_matching_class(self):
|
|
1344
|
+
"""
|
|
1345
|
+
If an exception fails an action and the exact type has registered
|
|
1346
|
+
extractor, extract errors using it.
|
|
1347
|
+
"""
|
|
1348
|
+
|
|
1349
|
+
class MyException(Exception):
|
|
1350
|
+
pass
|
|
1351
|
+
|
|
1352
|
+
register_exception_extractor(MyException, lambda e: {"key": e.args[0]})
|
|
1353
|
+
exception = MyException("a value")
|
|
1354
|
+
[message] = get_messages(exception)
|
|
1355
|
+
assertContainsFields(self, message, {"key": "a value"})
|
|
1356
|
+
|
|
1357
|
+
def test_subclass_falls_back_to_parent(self):
|
|
1358
|
+
"""
|
|
1359
|
+
If an exception fails an action and the exact type has not been
|
|
1360
|
+
registered but the error is a subclass of a registered class,
|
|
1361
|
+
extract errors using it.
|
|
1362
|
+
"""
|
|
1363
|
+
|
|
1364
|
+
class MyException(Exception):
|
|
1365
|
+
pass
|
|
1366
|
+
|
|
1367
|
+
class SubException(MyException):
|
|
1368
|
+
pass
|
|
1369
|
+
|
|
1370
|
+
register_exception_extractor(MyException, lambda e: {"key": e.args[0]})
|
|
1371
|
+
[message] = get_messages(SubException("the value"))
|
|
1372
|
+
assertContainsFields(self, message, {"key": "the value"})
|
|
1373
|
+
|
|
1374
|
+
def test_subclass_matches_first(self):
|
|
1375
|
+
"""
|
|
1376
|
+
If both a superclass and base class have registered extractors, the
|
|
1377
|
+
more specific one is used.
|
|
1378
|
+
"""
|
|
1379
|
+
|
|
1380
|
+
class MyException(Exception):
|
|
1381
|
+
pass
|
|
1382
|
+
|
|
1383
|
+
class SubException(MyException):
|
|
1384
|
+
pass
|
|
1385
|
+
|
|
1386
|
+
class SubSubException(SubException):
|
|
1387
|
+
pass
|
|
1388
|
+
|
|
1389
|
+
register_exception_extractor(MyException, lambda e: {"parent": e.args[0]})
|
|
1390
|
+
register_exception_extractor(SubException, lambda e: {"child": e.args[0]})
|
|
1391
|
+
[message] = get_messages(SubSubException("the value"))
|
|
1392
|
+
assertContainsFields(self, message, {"child": "the value"})
|
|
1393
|
+
|
|
1394
|
+
def test_error_in_extracter(self):
|
|
1395
|
+
"""
|
|
1396
|
+
If an error occurs in extraction, log the message as usual just
|
|
1397
|
+
without the extra fields, and an additional traceback.
|
|
1398
|
+
"""
|
|
1399
|
+
|
|
1400
|
+
class MyException(Exception):
|
|
1401
|
+
pass
|
|
1402
|
+
|
|
1403
|
+
def extract(e):
|
|
1404
|
+
return e.nosuchattribute
|
|
1405
|
+
|
|
1406
|
+
register_exception_extractor(MyException, extract)
|
|
1407
|
+
|
|
1408
|
+
messages = get_failed_action_messages(MyException())
|
|
1409
|
+
assertContainsFields(
|
|
1410
|
+
self, messages[1], {"action_type": "sys:me", "action_status": "failed"}
|
|
1411
|
+
)
|
|
1412
|
+
assertContainsFields(self, messages[0], {"message_type": "eliot:traceback"})
|
|
1413
|
+
self.assertIn("nosuchattribute", str(messages[0]["reason"]))
|
|
1414
|
+
|
|
1415
|
+
def test_environmenterror(self):
|
|
1416
|
+
"""
|
|
1417
|
+
``EnvironmentError`` has a registered extractor that extracts the
|
|
1418
|
+
errno.
|
|
1419
|
+
"""
|
|
1420
|
+
[message] = get_messages(EnvironmentError(12, "oh noes"))
|
|
1421
|
+
assertContainsFields(self, message, {"errno": 12})
|
|
1422
|
+
|
|
1423
|
+
return ErrorFieldExtraction
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def get_failed_action_messages(exception):
|
|
1427
|
+
"""
|
|
1428
|
+
Fail an action using the given exception.
|
|
1429
|
+
|
|
1430
|
+
:return: Logged dictionaries from the exception failing an action.
|
|
1431
|
+
"""
|
|
1432
|
+
action_type = ActionType("sys:me", [], [])
|
|
1433
|
+
logger = MemoryLogger()
|
|
1434
|
+
action = action_type.as_task(logger=logger)
|
|
1435
|
+
try:
|
|
1436
|
+
with action:
|
|
1437
|
+
raise exception
|
|
1438
|
+
except exception.__class__:
|
|
1439
|
+
pass
|
|
1440
|
+
logger.validate()
|
|
1441
|
+
return logger.messages[1:]
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class FailedActionExtractionTests(
|
|
1445
|
+
make_error_extraction_tests(get_failed_action_messages)
|
|
1446
|
+
):
|
|
1447
|
+
"""
|
|
1448
|
+
Tests for error extraction in failed actions.
|
|
1449
|
+
"""
|
|
1450
|
+
|
|
1451
|
+
def test_regular_fields(self):
|
|
1452
|
+
"""
|
|
1453
|
+
The normal failed action fields are still present when error
|
|
1454
|
+
extraction is used.
|
|
1455
|
+
"""
|
|
1456
|
+
|
|
1457
|
+
class MyException(Exception):
|
|
1458
|
+
pass
|
|
1459
|
+
|
|
1460
|
+
register_exception_extractor(MyException, lambda e: {"key": e.args[0]})
|
|
1461
|
+
|
|
1462
|
+
exception = MyException("because")
|
|
1463
|
+
messages = get_failed_action_messages(exception)
|
|
1464
|
+
assertContainsFields(
|
|
1465
|
+
self,
|
|
1466
|
+
messages[0],
|
|
1467
|
+
{
|
|
1468
|
+
"task_level": [2],
|
|
1469
|
+
"action_type": "sys:me",
|
|
1470
|
+
"action_status": "failed",
|
|
1471
|
+
"reason": "because",
|
|
1472
|
+
"exception": "eliot.tests.test_action.MyException",
|
|
1473
|
+
},
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
class PreserveContextTests(TestCase):
|
|
1478
|
+
"""
|
|
1479
|
+
Tests for L{preserve_context}.
|
|
1480
|
+
"""
|
|
1481
|
+
|
|
1482
|
+
def add(self, x, y):
|
|
1483
|
+
"""
|
|
1484
|
+
Add two inputs.
|
|
1485
|
+
"""
|
|
1486
|
+
Message.log(message_type="child")
|
|
1487
|
+
return x + y
|
|
1488
|
+
|
|
1489
|
+
def test_no_context(self):
|
|
1490
|
+
"""
|
|
1491
|
+
If C{preserve_context} is run outside an action context it just
|
|
1492
|
+
returns the same function.
|
|
1493
|
+
"""
|
|
1494
|
+
wrapped = preserve_context(self.add)
|
|
1495
|
+
self.assertEqual(wrapped(2, 3), 5)
|
|
1496
|
+
|
|
1497
|
+
def test_with_context_calls_underlying(self):
|
|
1498
|
+
"""
|
|
1499
|
+
If run inside an Eliot context, the result of C{preserve_context} is
|
|
1500
|
+
the result of calling the underlying function.
|
|
1501
|
+
"""
|
|
1502
|
+
with start_action(action_type="parent"):
|
|
1503
|
+
wrapped = preserve_context(self.add)
|
|
1504
|
+
self.assertEqual(wrapped(3, y=4), 7)
|
|
1505
|
+
|
|
1506
|
+
@capture_logging(None)
|
|
1507
|
+
def test_with_context_preserves_context(self, logger):
|
|
1508
|
+
"""
|
|
1509
|
+
If run inside an Eliot context, the result of C{preserve_context} runs
|
|
1510
|
+
the wrapped function within a C{eliot:task} which is a child of
|
|
1511
|
+
the original action.
|
|
1512
|
+
"""
|
|
1513
|
+
with start_action(action_type="parent"):
|
|
1514
|
+
wrapped = preserve_context(lambda: self.add(3, 4))
|
|
1515
|
+
thread = Thread(target=wrapped)
|
|
1516
|
+
thread.start()
|
|
1517
|
+
thread.join()
|
|
1518
|
+
[tree] = Parser.parse_stream(logger.messages)
|
|
1519
|
+
root = tree.root()
|
|
1520
|
+
self.assertEqual(
|
|
1521
|
+
(
|
|
1522
|
+
root.action_type,
|
|
1523
|
+
root.children[0].action_type,
|
|
1524
|
+
root.children[0].children[0].contents[MESSAGE_TYPE_FIELD],
|
|
1525
|
+
),
|
|
1526
|
+
("parent", "eliot:remote_task", "child"),
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
def test_callable_only_once(self):
|
|
1530
|
+
"""
|
|
1531
|
+
The result of C{preserve_context} can only be called once.
|
|
1532
|
+
"""
|
|
1533
|
+
with start_action(action_type="parent"):
|
|
1534
|
+
wrapped = preserve_context(self.add)
|
|
1535
|
+
wrapped(1, 2)
|
|
1536
|
+
self.assertRaises(TooManyCalls, wrapped, 3, 4)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
@log_call
|
|
1540
|
+
def for_pickling():
|
|
1541
|
+
pass
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
class LogCallTests(TestCase):
|
|
1545
|
+
"""Tests for log_call decorator."""
|
|
1546
|
+
|
|
1547
|
+
def assert_logged(self, logger, action_type, expected_params, expected_result):
|
|
1548
|
+
"""Assert that an action of given structure was logged."""
|
|
1549
|
+
[tree] = Parser.parse_stream(logger.messages)
|
|
1550
|
+
root = tree.root()
|
|
1551
|
+
self.assertEqual(root.action_type, action_type)
|
|
1552
|
+
message = dict(root.start_message.contents)
|
|
1553
|
+
for field in [ACTION_STATUS_FIELD, ACTION_TYPE_FIELD]:
|
|
1554
|
+
message.pop(field)
|
|
1555
|
+
self.assertEqual(message, expected_params)
|
|
1556
|
+
self.assertEqual(root.end_message.contents["result"], expected_result)
|
|
1557
|
+
self.assertEqual(root.status, SUCCEEDED_STATUS)
|
|
1558
|
+
|
|
1559
|
+
@capture_logging(None)
|
|
1560
|
+
def test_no_args_return(self, logger):
|
|
1561
|
+
"""
|
|
1562
|
+
C{@log_call} with no arguments logs return result, arguments, and has
|
|
1563
|
+
action type based on the action name.
|
|
1564
|
+
"""
|
|
1565
|
+
|
|
1566
|
+
@log_call
|
|
1567
|
+
def myfunc(x, y):
|
|
1568
|
+
return 4
|
|
1569
|
+
|
|
1570
|
+
myfunc(2, 3)
|
|
1571
|
+
self.assert_logged(logger, self.id() + ".<locals>.myfunc", {"x": 2, "y": 3}, 4)
|
|
1572
|
+
|
|
1573
|
+
@capture_logging(None)
|
|
1574
|
+
def test_exception(self, logger):
|
|
1575
|
+
"""C{@log_call} with an exception logs a failed action."""
|
|
1576
|
+
|
|
1577
|
+
@log_call
|
|
1578
|
+
def myfunc(x, y):
|
|
1579
|
+
1 / 0
|
|
1580
|
+
|
|
1581
|
+
with self.assertRaises(ZeroDivisionError):
|
|
1582
|
+
myfunc(2, 4)
|
|
1583
|
+
|
|
1584
|
+
[tree] = Parser.parse_stream(logger.messages)
|
|
1585
|
+
root = tree.root()
|
|
1586
|
+
self.assertIn("ZeroDivisionError", root.end_message.contents["exception"])
|
|
1587
|
+
self.assertEqual(root.status, FAILED_STATUS)
|
|
1588
|
+
|
|
1589
|
+
@capture_logging(None)
|
|
1590
|
+
def test_action_type(self, logger):
|
|
1591
|
+
"""C{@log_call} can take an action type."""
|
|
1592
|
+
|
|
1593
|
+
@log_call(action_type="myaction")
|
|
1594
|
+
def myfunc(x, y):
|
|
1595
|
+
return 4
|
|
1596
|
+
|
|
1597
|
+
myfunc(2, 3)
|
|
1598
|
+
self.assert_logged(logger, "myaction", {"x": 2, "y": 3}, 4)
|
|
1599
|
+
|
|
1600
|
+
@capture_logging(None)
|
|
1601
|
+
def test_default_argument_given(self, logger):
|
|
1602
|
+
"""C{@log_call} logs default arguments that were passed in."""
|
|
1603
|
+
|
|
1604
|
+
@log_call
|
|
1605
|
+
def myfunc(x, y=1):
|
|
1606
|
+
return 4
|
|
1607
|
+
|
|
1608
|
+
myfunc(2, y=5)
|
|
1609
|
+
self.assert_logged(logger, self.id() + ".<locals>.myfunc", {"x": 2, "y": 5}, 4)
|
|
1610
|
+
|
|
1611
|
+
@capture_logging(None)
|
|
1612
|
+
def test_default_argument_missing(self, logger):
|
|
1613
|
+
"""C{@log_call} logs default arguments that weren't passed in."""
|
|
1614
|
+
|
|
1615
|
+
@log_call
|
|
1616
|
+
def myfunc(x, y=1):
|
|
1617
|
+
return 6
|
|
1618
|
+
|
|
1619
|
+
myfunc(2)
|
|
1620
|
+
self.assert_logged(logger, self.id() + ".<locals>.myfunc", {"x": 2, "y": 1}, 6)
|
|
1621
|
+
|
|
1622
|
+
@capture_logging(None)
|
|
1623
|
+
def test_star_args_kwargs(self, logger):
|
|
1624
|
+
"""C{@log_call} logs star args and kwargs."""
|
|
1625
|
+
|
|
1626
|
+
@log_call
|
|
1627
|
+
def myfunc(x, *y, **z):
|
|
1628
|
+
return 6
|
|
1629
|
+
|
|
1630
|
+
myfunc(2, 3, 4, a=1, b=2)
|
|
1631
|
+
self.assert_logged(
|
|
1632
|
+
logger,
|
|
1633
|
+
self.id() + ".<locals>.myfunc",
|
|
1634
|
+
{"x": 2, "y": (3, 4), "z": {"a": 1, "b": 2}},
|
|
1635
|
+
6,
|
|
1636
|
+
)
|
|
1637
|
+
|
|
1638
|
+
@capture_logging(None)
|
|
1639
|
+
def test_whitelist_args(self, logger):
|
|
1640
|
+
"""C{@log_call} only includes whitelisted arguments."""
|
|
1641
|
+
|
|
1642
|
+
@log_call(include_args=("x", "z"))
|
|
1643
|
+
def myfunc(x, y, z):
|
|
1644
|
+
return 6
|
|
1645
|
+
|
|
1646
|
+
myfunc(2, 3, 4)
|
|
1647
|
+
self.assert_logged(logger, self.id() + ".<locals>.myfunc", {"x": 2, "z": 4}, 6)
|
|
1648
|
+
|
|
1649
|
+
def test_wrong_whitelist_args(self):
|
|
1650
|
+
"""If C{include_args} doesn't match function, raise an exception."""
|
|
1651
|
+
with self.assertRaises(ValueError):
|
|
1652
|
+
|
|
1653
|
+
@log_call(include_args=["a", "x", "y"])
|
|
1654
|
+
def f(x, y):
|
|
1655
|
+
pass
|
|
1656
|
+
|
|
1657
|
+
@capture_logging(None)
|
|
1658
|
+
def test_no_result(self, logger):
|
|
1659
|
+
"""C{@log_call} can omit logging the result."""
|
|
1660
|
+
|
|
1661
|
+
@log_call(include_result=False)
|
|
1662
|
+
def myfunc(x, y):
|
|
1663
|
+
return 6
|
|
1664
|
+
|
|
1665
|
+
myfunc(2, 3)
|
|
1666
|
+
|
|
1667
|
+
[tree] = Parser.parse_stream(logger.messages)
|
|
1668
|
+
root = tree.root()
|
|
1669
|
+
self.assertNotIn("result", root.end_message.contents)
|
|
1670
|
+
self.assertEqual(root.status, SUCCEEDED_STATUS)
|
|
1671
|
+
|
|
1672
|
+
def test_pickleable(self):
|
|
1673
|
+
"""Functions decorated with C{log_call} are pickleable.
|
|
1674
|
+
|
|
1675
|
+
This is necessary for e.g. Dask usage.
|
|
1676
|
+
"""
|
|
1677
|
+
self.assertIs(for_pickling, pickle.loads(pickle.dumps(for_pickling)))
|
|
1678
|
+
|
|
1679
|
+
@capture_logging(None)
|
|
1680
|
+
def test_methods(self, logger):
|
|
1681
|
+
"""self is not logged."""
|
|
1682
|
+
|
|
1683
|
+
class C(object):
|
|
1684
|
+
@log_call
|
|
1685
|
+
def f(self, x):
|
|
1686
|
+
pass
|
|
1687
|
+
|
|
1688
|
+
C().f(2)
|
|
1689
|
+
self.assert_logged(logger, self.id() + ".<locals>.C.f", {"x": 2}, None)
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
class IndividualMessageLogTests(TestCase):
|
|
1693
|
+
"""Action.log() tests."""
|
|
1694
|
+
|
|
1695
|
+
def test_log_creates_new_dictionary(self):
|
|
1696
|
+
"""
|
|
1697
|
+
L{Action.log} creates a new dictionary on each call.
|
|
1698
|
+
|
|
1699
|
+
This is important because we might mutate the dictionary in
|
|
1700
|
+
``Logger.write``.
|
|
1701
|
+
"""
|
|
1702
|
+
messages = []
|
|
1703
|
+
add_destination(messages.append)
|
|
1704
|
+
self.addCleanup(remove_destination, messages.append)
|
|
1705
|
+
|
|
1706
|
+
with start_action(action_type="x") as action:
|
|
1707
|
+
action.log("mymessage", key=4)
|
|
1708
|
+
action.log(message_type="mymessage2", key=5)
|
|
1709
|
+
self.assertEqual(messages[1]["key"], 4)
|
|
1710
|
+
self.assertEqual(messages[2]["key"], 5)
|
|
1711
|
+
self.assertEqual(messages[1]["message_type"], "mymessage")
|
|
1712
|
+
self.assertEqual(messages[2]["message_type"], "mymessage2")
|
|
1713
|
+
|
|
1714
|
+
@patch("time.time")
|
|
1715
|
+
def test_log_adds_timestamp(self, time_func):
|
|
1716
|
+
"""
|
|
1717
|
+
L{Action.log} adds a C{"timestamp"} field to the dictionary written
|
|
1718
|
+
to the logger, with the current time in seconds since the epoch.
|
|
1719
|
+
"""
|
|
1720
|
+
messages = []
|
|
1721
|
+
add_destination(messages.append)
|
|
1722
|
+
self.addCleanup(remove_destination, messages.append)
|
|
1723
|
+
|
|
1724
|
+
time_func.return_value = timestamp = 1387299889.153187625
|
|
1725
|
+
with start_action(action_type="x") as action:
|
|
1726
|
+
action.log("mymessage", key=4)
|
|
1727
|
+
self.assertEqual(messages[-2]["message_type"], "mymessage")
|
|
1728
|
+
self.assertEqual(messages[-2]["timestamp"], timestamp)
|
|
1729
|
+
|
|
1730
|
+
def test_part_of_action(self):
|
|
1731
|
+
"""
|
|
1732
|
+
L{Action.log} adds the identification fields from the given
|
|
1733
|
+
L{Action} to the dictionary written to the logger.
|
|
1734
|
+
"""
|
|
1735
|
+
messages = []
|
|
1736
|
+
add_destination(messages.append)
|
|
1737
|
+
self.addCleanup(remove_destination, messages.append)
|
|
1738
|
+
|
|
1739
|
+
action = Action(None, "unique", TaskLevel(level=[37, 4]), "sys:thename")
|
|
1740
|
+
action.log("me", key=2)
|
|
1741
|
+
written = messages[0]
|
|
1742
|
+
del written["timestamp"]
|
|
1743
|
+
self.assertEqual(
|
|
1744
|
+
written,
|
|
1745
|
+
{
|
|
1746
|
+
"task_uuid": "unique",
|
|
1747
|
+
"task_level": [37, 4, 1],
|
|
1748
|
+
"key": 2,
|
|
1749
|
+
"message_type": "me",
|
|
1750
|
+
},
|
|
1751
|
+
)
|