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,309 @@
1
+ """
2
+ Tests for L{eliot._parse}.
3
+ """
4
+
5
+ from unittest import TestCase
6
+ from itertools import chain, zip_longest
7
+
8
+ from hypothesis import strategies as st, given, assume
9
+
10
+ from pyrsistent import PClass, field, pvector_field
11
+
12
+ from .. import start_action, Message
13
+ from ..testing import MemoryLogger
14
+ from ..parse import Task, Parser
15
+ from .._message import (
16
+ WrittenMessage,
17
+ MESSAGE_TYPE_FIELD,
18
+ TASK_LEVEL_FIELD,
19
+ TASK_UUID_FIELD,
20
+ )
21
+ from .._action import FAILED_STATUS, ACTION_STATUS_FIELD, WrittenAction
22
+ from .strategies import labels
23
+
24
+
25
+ class ActionStructure(PClass):
26
+ """
27
+ A tree structure used to generate/compare to Eliot trees.
28
+
29
+ Individual messages are encoded as a unicode string; actions are
30
+ encoded as a L{ActionStructure} instance.
31
+ """
32
+
33
+ type = field(type=(str, None.__class__))
34
+ children = pvector_field(object) # XXX ("StubAction", unicode))
35
+ failed = field(type=bool)
36
+
37
+ @classmethod
38
+ def from_written(cls, written):
39
+ """
40
+ Create an L{ActionStructure} or L{str} from a L{WrittenAction} or
41
+ L{WrittenMessage}.
42
+ """
43
+ if isinstance(written, WrittenMessage):
44
+ return written.as_dict()[MESSAGE_TYPE_FIELD]
45
+ else: # WrittenAction
46
+ if not written.end_message:
47
+ raise AssertionError("Missing end message.")
48
+ return cls(
49
+ type=written.action_type,
50
+ failed=(
51
+ written.end_message.contents[ACTION_STATUS_FIELD] == FAILED_STATUS
52
+ ),
53
+ children=[cls.from_written(o) for o in written.children],
54
+ )
55
+
56
+ @classmethod
57
+ def to_eliot(cls, structure_or_message, logger):
58
+ """
59
+ Given a L{ActionStructure} or L{str}, generate appropriate
60
+ structured Eliot log mesages to given L{MemoryLogger}.
61
+ """
62
+ if isinstance(structure_or_message, cls):
63
+ action = structure_or_message
64
+ try:
65
+ with start_action(logger, action_type=action.type):
66
+ for child in action.children:
67
+ cls.to_eliot(child, logger)
68
+ if structure_or_message.failed:
69
+ raise RuntimeError("Make the logxpy action fail.")
70
+ except RuntimeError:
71
+ pass
72
+ else:
73
+ Message.new(message_type=structure_or_message).write(logger)
74
+ return logger.messages
75
+
76
+
77
+ @st.composite
78
+ def action_structures(draw):
79
+ """
80
+ A Hypothesis strategy that creates a tree of L{ActionStructure} and
81
+ L{str}.
82
+ """
83
+ tree = draw(st.recursive(labels, st.lists, max_leaves=20))
84
+
85
+ def to_structure(tree_or_message):
86
+ if isinstance(tree_or_message, list):
87
+ return ActionStructure(
88
+ type=draw(labels),
89
+ failed=draw(st.booleans()),
90
+ children=[to_structure(o) for o in tree_or_message],
91
+ )
92
+ else:
93
+ return tree_or_message
94
+
95
+ return to_structure(tree)
96
+
97
+
98
+ def _structure_and_messages(structure):
99
+ messages = ActionStructure.to_eliot(structure, MemoryLogger())
100
+ return st.permutations(messages).map(lambda permuted: (structure, permuted))
101
+
102
+
103
+ # Hypothesis strategy that creates a tuple of ActionStructure/unicode and
104
+ # corresponding serialized Eliot messages, randomly shuffled.
105
+ STRUCTURES_WITH_MESSAGES = action_structures().flatmap(_structure_and_messages)
106
+
107
+
108
+ def parse_to_task(messages):
109
+ """
110
+ Feed a set of messages to a L{Task}.
111
+
112
+ @param messages: Sequence of messages dictionaries to parse.
113
+
114
+ @return: Resulting L{Task}.
115
+ """
116
+ task = Task()
117
+ for message in messages:
118
+ task = task.add(message)
119
+ return task
120
+
121
+
122
+ class TaskTests(TestCase):
123
+ """
124
+ Tests for L{Task}.
125
+ """
126
+
127
+ @given(structure_and_messages=STRUCTURES_WITH_MESSAGES)
128
+ def test_missing_action(self, structure_and_messages):
129
+ """
130
+ If we parse messages (in shuffled order) but a start message is
131
+ missing then the structure is still deduced correctly from the
132
+ remaining messages.
133
+ """
134
+ action_structure, messages = structure_and_messages
135
+ assume(not isinstance(action_structure, str))
136
+
137
+ # Remove first start message we encounter; since messages are
138
+ # shuffled the location removed will differ over Hypothesis test
139
+ # iterations:
140
+ messages = messages[:]
141
+ for i, message in enumerate(messages):
142
+ if message[TASK_LEVEL_FIELD][-1] == 1: # start message
143
+ del messages[i]
144
+ break
145
+
146
+ task = parse_to_task(messages)
147
+ parsed_structure = ActionStructure.from_written(task.root())
148
+
149
+ # We expect the action with missing start message to otherwise
150
+ # be parsed correctly:
151
+ self.assertEqual(parsed_structure, action_structure)
152
+
153
+ @given(structure_and_messages=STRUCTURES_WITH_MESSAGES)
154
+ def test_parse_from_random_order(self, structure_and_messages):
155
+ """
156
+ If we shuffle messages and parse them the parser builds a tree of
157
+ actions that is the same as the one used to generate the messages.
158
+
159
+ Shuffled messages means we have to deal with (temporarily) missing
160
+ information sufficiently well to be able to parse correctly once
161
+ the missing information arrives.
162
+ """
163
+ action_structure, messages = structure_and_messages
164
+
165
+ task = Task()
166
+ for message in messages:
167
+ task = task.add(message)
168
+
169
+ # Assert parsed structure matches input structure:
170
+ parsed_structure = ActionStructure.from_written(task.root())
171
+ self.assertEqual(parsed_structure, action_structure)
172
+
173
+ @given(structure_and_messages=STRUCTURES_WITH_MESSAGES)
174
+ def test_is_complete(self, structure_and_messages):
175
+ """
176
+ ``Task.is_complete()`` only returns true when all messages within the
177
+ tree have been delivered.
178
+ """
179
+ action_structure, messages = structure_and_messages
180
+
181
+ task = Task()
182
+ completed = []
183
+ for message in messages:
184
+ task = task.add(message)
185
+ completed.append(task.is_complete())
186
+
187
+ self.assertEqual(completed, [False for m in messages[:-1]] + [True])
188
+
189
+ def test_parse_contents(self):
190
+ """
191
+ L{{Task.add}} parses the contents of the messages it receives.
192
+ """
193
+ logger = MemoryLogger()
194
+ with start_action(logger, action_type="xxx", y=123) as ctx:
195
+ Message.new(message_type="zzz", z=4).write(logger)
196
+ ctx.add_success_fields(foo=[1, 2])
197
+ messages = logger.messages
198
+ expected = WrittenAction.from_messages(
199
+ WrittenMessage.from_dict(messages[0]),
200
+ [WrittenMessage.from_dict(messages[1])],
201
+ WrittenMessage.from_dict(messages[2]),
202
+ )
203
+
204
+ task = parse_to_task(messages)
205
+ self.assertEqual(task.root(), expected)
206
+
207
+
208
+ class ParserTests(TestCase):
209
+ """
210
+ Tests for L{Parser}.
211
+ """
212
+
213
+ @given(
214
+ structure_and_messages1=STRUCTURES_WITH_MESSAGES,
215
+ structure_and_messages2=STRUCTURES_WITH_MESSAGES,
216
+ structure_and_messages3=STRUCTURES_WITH_MESSAGES,
217
+ )
218
+ def test_parse_into_tasks(
219
+ self, structure_and_messages1, structure_and_messages2, structure_and_messages3
220
+ ):
221
+ """
222
+ Adding messages to a L{Parser} parses them into a L{Task} instances.
223
+ """
224
+ _, messages1 = structure_and_messages1
225
+ _, messages2 = structure_and_messages2
226
+ _, messages3 = structure_and_messages3
227
+ all_messages = (messages1, messages2, messages3)
228
+ # Need unique UUIDs per task:
229
+ assume(len(set(m[0][TASK_UUID_FIELD] for m in all_messages)) == 3)
230
+
231
+ parser = Parser()
232
+ all_tasks = []
233
+ for message in chain(*zip_longest(*all_messages)):
234
+ if message is not None:
235
+ completed_tasks, parser = parser.add(message)
236
+ all_tasks.extend(completed_tasks)
237
+
238
+ self.assertCountEqual(all_tasks, [parse_to_task(msgs) for msgs in all_messages])
239
+
240
+ @given(structure_and_messages=STRUCTURES_WITH_MESSAGES)
241
+ def test_incomplete_tasks(self, structure_and_messages):
242
+ """
243
+ Until a L{Task} is fully parsed, it is returned in
244
+ L{Parser.incomplete_tasks}.
245
+ """
246
+ _, messages = structure_and_messages
247
+ parser = Parser()
248
+ task = Task()
249
+ incomplete_matches = []
250
+ for message in messages[:-1]:
251
+ _, parser = parser.add(message)
252
+ task = task.add(message)
253
+ incomplete_matches.append(parser.incomplete_tasks() == [task])
254
+
255
+ task = task.add(messages[-1])
256
+ _, parser = parser.add(messages[-1])
257
+ self.assertEqual(
258
+ dict(
259
+ incomplete_matches=incomplete_matches,
260
+ final_incompleted=parser.incomplete_tasks(),
261
+ ),
262
+ dict(incomplete_matches=[True] * (len(messages) - 1), final_incompleted=[]),
263
+ )
264
+
265
+ @given(
266
+ structure_and_messages1=STRUCTURES_WITH_MESSAGES,
267
+ structure_and_messages2=STRUCTURES_WITH_MESSAGES,
268
+ structure_and_messages3=STRUCTURES_WITH_MESSAGES,
269
+ )
270
+ def test_parse_stream(
271
+ self, structure_and_messages1, structure_and_messages2, structure_and_messages3
272
+ ):
273
+ """
274
+ L{Parser.parse_stream} returns an iterable of completed and then
275
+ incompleted tasks.
276
+ """
277
+ _, messages1 = structure_and_messages1
278
+ _, messages2 = structure_and_messages2
279
+ _, messages3 = structure_and_messages3
280
+ # Need at least one non-dropped message in partial tree:
281
+ assume(len(messages3) > 1)
282
+ # Need unique UUIDs per task:
283
+ assume(
284
+ len(set(m[0][TASK_UUID_FIELD] for m in (messages1, messages2, messages3)))
285
+ == 3
286
+ )
287
+
288
+ # Two complete tasks, one incomplete task:
289
+ all_messages = (messages1, messages2, messages3[:-1])
290
+
291
+ all_tasks = list(
292
+ Parser.parse_stream(
293
+ [m for m in chain(*zip_longest(*all_messages)) if m is not None]
294
+ )
295
+ )
296
+ self.assertCountEqual(all_tasks, [parse_to_task(msgs) for msgs in all_messages])
297
+
298
+
299
+ class BackwardsCompatibility(TestCase):
300
+ """Tests for backwards compatibility."""
301
+
302
+ def test_imports(self):
303
+ """Old ways of importing still work."""
304
+ import eliot._parse
305
+ from logxpy import _parse
306
+ import eliot.parse
307
+
308
+ self.assertIs(eliot.parse, eliot._parse)
309
+ self.assertIs(_parse, eliot.parse)
@@ -0,0 +1,55 @@
1
+ """Tests for eliot._pool."""
2
+ from unittest import TestCase
3
+ import asyncio
4
+ from logxpy._pool import Channel, Pool
5
+
6
+ class ChannelTests(TestCase):
7
+ def test_send_recv(self):
8
+ """Channel sends and receives items."""
9
+ async def run():
10
+ c = Channel(size=10)
11
+ await c.send(1)
12
+ await c.send(2)
13
+ self.assertEqual(await c.recv(), 1)
14
+ self.assertEqual(await c.recv(), 2)
15
+ asyncio.run(run())
16
+
17
+ def test_close(self):
18
+ """Closed channel returns None."""
19
+ async def run():
20
+ c = Channel()
21
+ await c.send(1)
22
+ c.close()
23
+ self.assertEqual(await c.recv(), 1)
24
+ self.assertEqual(await c.recv(), None)
25
+ asyncio.run(run())
26
+
27
+ def test_drop_oldest(self):
28
+ """Channel drops oldest when full if requested."""
29
+ async def run():
30
+ c = Channel(size=2, drop_oldest=True)
31
+ await c.send(1)
32
+ await c.send(2)
33
+ await c.send(3) # Should drop 1
34
+
35
+ self.assertEqual(c.stats.dropped, 1)
36
+ self.assertEqual(await c.recv(), 2)
37
+ self.assertEqual(await c.recv(), 3)
38
+ asyncio.run(run())
39
+
40
+ class PoolTests(TestCase):
41
+ def test_cpu_pool(self):
42
+ """CPU pool executes function."""
43
+ async def run():
44
+ pool = Pool()
45
+ res = await pool.cpu(sum, [1, 2, 3])
46
+ self.assertEqual(res, 6)
47
+ asyncio.run(run())
48
+
49
+ def test_io_pool(self):
50
+ """IO pool executes function."""
51
+ async def run():
52
+ pool = Pool()
53
+ res = await pool.io(str, 123)
54
+ self.assertEqual(res, "123")
55
+ asyncio.run(run())
@@ -0,0 +1,303 @@
1
+ """
2
+ Tests for C{eliot.prettyprint}.
3
+ """
4
+
5
+ from unittest import TestCase
6
+ from subprocess import check_output, Popen, PIPE
7
+ from collections import OrderedDict
8
+ from datetime import datetime
9
+
10
+ from pyrsistent import pmap
11
+
12
+ from ..json import _dumps_bytes as dumps
13
+ from ..prettyprint import pretty_format, compact_format, REQUIRED_FIELDS
14
+
15
+ SIMPLE_MESSAGE = {
16
+ "timestamp": 1443193754,
17
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
18
+ "message_type": "messagey",
19
+ "task_level": [1, 2],
20
+ "keys": [123, 456],
21
+ }
22
+
23
+ UNTYPED_MESSAGE = {
24
+ "timestamp": 1443193754,
25
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
26
+ "task_level": [1],
27
+ "key": 1234,
28
+ "abc": "def",
29
+ }
30
+
31
+
32
+ class FormattingTests(TestCase):
33
+ """
34
+ Tests for L{pretty_format}.
35
+ """
36
+
37
+ def test_message(self):
38
+ """
39
+ A typed message is printed as expected.
40
+ """
41
+ self.assertEqual(
42
+ pretty_format(SIMPLE_MESSAGE),
43
+ """\
44
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1/2
45
+ 2015-09-25T15:09:14Z
46
+ message_type: 'messagey'
47
+ keys: [123, 456]
48
+ """,
49
+ )
50
+
51
+ def test_untyped_message(self):
52
+ """
53
+ A message with no type is printed as expected.
54
+ """
55
+ self.assertEqual(
56
+ pretty_format(UNTYPED_MESSAGE),
57
+ """\
58
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1
59
+ 2015-09-25T15:09:14Z
60
+ abc: 'def'
61
+ key: 1234
62
+ """,
63
+ )
64
+
65
+ def test_action(self):
66
+ """
67
+ An action message is printed as expected.
68
+ """
69
+ message = {
70
+ "task_uuid": "8bc6ded2-446c-4b6d-abbc-4f21f1c9a7d8",
71
+ "place": "Statue #1",
72
+ "task_level": [2, 2, 2, 1],
73
+ "action_type": "visited",
74
+ "timestamp": 1443193958.0,
75
+ "action_status": "started",
76
+ }
77
+ self.assertEqual(
78
+ pretty_format(message),
79
+ """\
80
+ 8bc6ded2-446c-4b6d-abbc-4f21f1c9a7d8 -> /2/2/2/1
81
+ 2015-09-25T15:12:38Z
82
+ action_type: 'visited'
83
+ action_status: 'started'
84
+ place: 'Statue #1'
85
+ """,
86
+ )
87
+
88
+ def test_multi_line(self):
89
+ """
90
+ Multiple line values are indented nicely.
91
+ """
92
+ message = {
93
+ "timestamp": 1443193754,
94
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
95
+ "task_level": [1],
96
+ "key": "hello\nthere\nmonkeys!\n",
97
+ "more": "stuff",
98
+ }
99
+ self.assertEqual(
100
+ pretty_format(message),
101
+ """\
102
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1
103
+ 2015-09-25T15:09:14Z
104
+ key: 'hello
105
+ | there
106
+ | monkeys!
107
+ | '
108
+ more: 'stuff'
109
+ """,
110
+ )
111
+
112
+ def test_tabs(self):
113
+ """
114
+ Tabs are formatted as tabs, not quoted.
115
+ """
116
+ message = {
117
+ "timestamp": 1443193754,
118
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
119
+ "task_level": [1],
120
+ "key": "hello\tmonkeys!",
121
+ }
122
+ self.assertEqual(
123
+ pretty_format(message),
124
+ """\
125
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1
126
+ 2015-09-25T15:09:14Z
127
+ key: 'hello monkeys!'
128
+ """,
129
+ )
130
+
131
+ def test_structured(self):
132
+ """
133
+ Structured field values (e.g. a dictionary) are formatted in a helpful
134
+ manner.
135
+ """
136
+ message = {
137
+ "timestamp": 1443193754,
138
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
139
+ "task_level": [1],
140
+ "key": {"value": 123, "another": [1, 2, {"more": "data"}]},
141
+ }
142
+ self.assertEqual(
143
+ pretty_format(message),
144
+ """\
145
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1
146
+ 2015-09-25T15:09:14Z
147
+ key: {'another': [1, 2, {'more': 'data'}],
148
+ | 'value': 123}
149
+ """,
150
+ )
151
+
152
+ def test_microsecond(self):
153
+ """
154
+ Microsecond timestamps are rendered in the output.
155
+ """
156
+ message = {
157
+ "timestamp": 1443193754.123455,
158
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
159
+ "task_level": [1],
160
+ }
161
+ self.assertEqual(
162
+ pretty_format(message),
163
+ """\
164
+ 8c668cde-235b-4872-af4e-caea524bd1c0 -> /1
165
+ 2015-09-25T15:09:14.123455Z
166
+ """,
167
+ )
168
+
169
+ def test_compact(self):
170
+ """
171
+ The compact mode does everything on a single line, including
172
+ dictionaries and multi-line messages.
173
+ """
174
+ message = {
175
+ "timestamp": 1443193754,
176
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
177
+ "task_level": [1],
178
+ "key": OrderedDict([("value", 123), ("another", [1, 2, {"more": "data"}])]),
179
+ "multiline": "hello\n\tthere!\nabc",
180
+ }
181
+ self.assertEqual(
182
+ compact_format(message),
183
+ r'8c668cde-235b-4872-af4e-caea524bd1c0/1 2015-09-25T15:09:14Z key={"value":123,"another":[1,2,{"more":"data"}]} multiline="hello\n\tthere!\nabc"',
184
+ )
185
+
186
+ def test_local(self):
187
+ """
188
+ Timestamps can be generated in local timezone.
189
+ """
190
+ message = {
191
+ "timestamp": 1443193754,
192
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
193
+ "task_level": [1],
194
+ }
195
+ expected = datetime.fromtimestamp(1443193754).isoformat(sep="T")
196
+ self.assertIn(expected, pretty_format(message, True))
197
+ self.assertIn(expected, compact_format(message, True))
198
+
199
+
200
+ class CommandLineTests(TestCase):
201
+ """
202
+ Tests for the command-line tool.
203
+ """
204
+
205
+ def test_help(self):
206
+ """
207
+ C{--help} prints out the help text and exits.
208
+ """
209
+ result = check_output(["eliot-prettyprint", "--help"])
210
+ self.assertIn(b"Convert Eliot messages into more readable", result)
211
+
212
+ def write_and_read(self, lines, extra_args=()):
213
+ """
214
+ Write the given lines to the command-line on stdin, return stdout.
215
+
216
+ @param lines: Sequences of lines to write, as bytes, and lacking
217
+ new lines.
218
+ @return: Unicode-decoded result of subprocess stdout.
219
+ """
220
+ process = Popen(
221
+ [b"eliot-prettyprint"] + list(extra_args), stdin=PIPE, stdout=PIPE
222
+ )
223
+ process.stdin.write(b"".join(line + b"\n" for line in lines))
224
+ process.stdin.close()
225
+ result = process.stdout.read().decode("utf-8")
226
+ process.stdout.close()
227
+ return result
228
+
229
+ def test_output(self):
230
+ """
231
+ Lacking command-line arguments the process reads JSON lines from stdin
232
+ and writes out a pretty-printed version.
233
+ """
234
+ messages = [SIMPLE_MESSAGE, UNTYPED_MESSAGE, SIMPLE_MESSAGE]
235
+ stdout = self.write_and_read(map(dumps, messages))
236
+ self.assertEqual(
237
+ stdout, "".join(pretty_format(message) + "\n" for message in messages)
238
+ )
239
+
240
+ def test_compact_output(self):
241
+ """
242
+ In compact mode, the process reads JSON lines from stdin and writes out
243
+ a pretty-printed compact version.
244
+ """
245
+ messages = [SIMPLE_MESSAGE, UNTYPED_MESSAGE, SIMPLE_MESSAGE]
246
+ stdout = self.write_and_read(map(dumps, messages), [b"--compact"])
247
+ self.assertEqual(
248
+ stdout, "".join(compact_format(message) + "\n" for message in messages)
249
+ )
250
+
251
+ def test_local_timezone(self):
252
+ """
253
+ Local timezones are used if --local-timezone is given.
254
+ """
255
+ message = {
256
+ "timestamp": 1443193754,
257
+ "task_uuid": "8c668cde-235b-4872-af4e-caea524bd1c0",
258
+ "task_level": [1],
259
+ }
260
+ expected = datetime.fromtimestamp(1443193754).isoformat(sep="T")
261
+ stdout = self.write_and_read(
262
+ [dumps(message)], [b"--compact", b"--local-timezone"]
263
+ )
264
+ self.assertIn(expected, stdout)
265
+ stdout = self.write_and_read(
266
+ [dumps(message)], [b"--compact", b"--local-timezone"]
267
+ )
268
+ self.assertIn(expected, stdout)
269
+
270
+ def test_not_json_message(self):
271
+ """
272
+ Non-JSON lines are not formatted.
273
+ """
274
+ not_json = b"NOT JSON!!"
275
+ lines = [dumps(SIMPLE_MESSAGE), not_json, dumps(UNTYPED_MESSAGE)]
276
+ stdout = self.write_and_read(lines)
277
+ self.assertEqual(
278
+ stdout,
279
+ "{}\nNot JSON: {}\n\n{}\n".format(
280
+ pretty_format(SIMPLE_MESSAGE),
281
+ str(not_json),
282
+ pretty_format(UNTYPED_MESSAGE),
283
+ ),
284
+ )
285
+
286
+ def test_missing_required_field(self):
287
+ """
288
+ Non-Eliot JSON messages are not formatted.
289
+ """
290
+ base = pmap(SIMPLE_MESSAGE)
291
+ messages = [dumps(dict(base.remove(field))) for field in REQUIRED_FIELDS] + [
292
+ dumps(SIMPLE_MESSAGE)
293
+ ]
294
+ stdout = self.write_and_read(messages)
295
+ self.assertEqual(
296
+ stdout,
297
+ "{}{}\n".format(
298
+ "".join(
299
+ "Not an Eliot message: {}\n\n".format(msg) for msg in messages[:-1]
300
+ ),
301
+ pretty_format(SIMPLE_MESSAGE),
302
+ ),
303
+ )
@@ -0,0 +1,35 @@
1
+ """Test for pyinstaller compatibility."""
2
+
3
+ from unittest import TestCase, SkipTest
4
+ from tempfile import mkdtemp, NamedTemporaryFile
5
+ from subprocess import check_call, CalledProcessError
6
+ import os
7
+
8
+
9
+ class PyInstallerTests(TestCase):
10
+ """Make sure PyInstaller doesn't break Eliot."""
11
+
12
+ def setUp(self):
13
+ try:
14
+ check_call(["pyinstaller", "--help"])
15
+ except (CalledProcessError, FileNotFoundError):
16
+ raise SkipTest("Can't find pyinstaller.")
17
+
18
+ def test_importable(self):
19
+ """The Eliot package can be imported inside a PyInstaller packaged binary."""
20
+ output_dir = mkdtemp()
21
+ with NamedTemporaryFile(mode="w") as f:
22
+ f.write("import eliot; import eliot.prettyprint\n")
23
+ f.flush()
24
+ check_call(
25
+ [
26
+ "pyinstaller",
27
+ "--distpath",
28
+ output_dir,
29
+ "-F",
30
+ "-n",
31
+ "importeliot",
32
+ f.name,
33
+ ]
34
+ )
35
+ check_call([os.path.join(output_dir, "importeliot")])