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.
Files changed (72) hide show
  1. logxpy/__init__.py +126 -0
  2. logxpy/_action.py +958 -0
  3. logxpy/_async.py +186 -0
  4. logxpy/_base.py +80 -0
  5. logxpy/_compat.py +71 -0
  6. logxpy/_config.py +45 -0
  7. logxpy/_dest.py +88 -0
  8. logxpy/_errors.py +58 -0
  9. logxpy/_fmt.py +68 -0
  10. logxpy/_generators.py +136 -0
  11. logxpy/_mask.py +23 -0
  12. logxpy/_message.py +195 -0
  13. logxpy/_output.py +517 -0
  14. logxpy/_pool.py +93 -0
  15. logxpy/_traceback.py +126 -0
  16. logxpy/_types.py +71 -0
  17. logxpy/_util.py +56 -0
  18. logxpy/_validation.py +486 -0
  19. logxpy/_version.py +21 -0
  20. logxpy/cli.py +61 -0
  21. logxpy/dask.py +172 -0
  22. logxpy/decorators.py +268 -0
  23. logxpy/filter.py +124 -0
  24. logxpy/journald.py +88 -0
  25. logxpy/json.py +149 -0
  26. logxpy/loggerx.py +253 -0
  27. logxpy/logwriter.py +84 -0
  28. logxpy/parse.py +191 -0
  29. logxpy/prettyprint.py +173 -0
  30. logxpy/serializers.py +36 -0
  31. logxpy/stdlib.py +23 -0
  32. logxpy/tai64n.py +45 -0
  33. logxpy/testing.py +472 -0
  34. logxpy/tests/__init__.py +9 -0
  35. logxpy/tests/common.py +36 -0
  36. logxpy/tests/strategies.py +231 -0
  37. logxpy/tests/test_action.py +1751 -0
  38. logxpy/tests/test_api.py +86 -0
  39. logxpy/tests/test_async.py +67 -0
  40. logxpy/tests/test_compat.py +13 -0
  41. logxpy/tests/test_config.py +21 -0
  42. logxpy/tests/test_coroutines.py +105 -0
  43. logxpy/tests/test_dask.py +211 -0
  44. logxpy/tests/test_decorators.py +54 -0
  45. logxpy/tests/test_filter.py +122 -0
  46. logxpy/tests/test_fmt.py +42 -0
  47. logxpy/tests/test_generators.py +292 -0
  48. logxpy/tests/test_journald.py +246 -0
  49. logxpy/tests/test_json.py +208 -0
  50. logxpy/tests/test_loggerx.py +44 -0
  51. logxpy/tests/test_logwriter.py +262 -0
  52. logxpy/tests/test_message.py +334 -0
  53. logxpy/tests/test_output.py +921 -0
  54. logxpy/tests/test_parse.py +309 -0
  55. logxpy/tests/test_pool.py +55 -0
  56. logxpy/tests/test_prettyprint.py +303 -0
  57. logxpy/tests/test_pyinstaller.py +35 -0
  58. logxpy/tests/test_serializers.py +36 -0
  59. logxpy/tests/test_stdlib.py +73 -0
  60. logxpy/tests/test_tai64n.py +66 -0
  61. logxpy/tests/test_testing.py +1051 -0
  62. logxpy/tests/test_traceback.py +251 -0
  63. logxpy/tests/test_twisted.py +814 -0
  64. logxpy/tests/test_util.py +45 -0
  65. logxpy/tests/test_validation.py +989 -0
  66. logxpy/twisted.py +265 -0
  67. logxpy-0.1.0.dist-info/METADATA +100 -0
  68. logxpy-0.1.0.dist-info/RECORD +72 -0
  69. logxpy-0.1.0.dist-info/WHEEL +5 -0
  70. logxpy-0.1.0.dist-info/entry_points.txt +2 -0
  71. logxpy-0.1.0.dist-info/licenses/LICENSE +201 -0
  72. 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
+ )