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,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")])
|