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
logxpy/tests/test_fmt.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Tests for eliot._fmt."""
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
from logxpy._fmt import format_value, DFFormatter, TensorFormatter
|
|
4
|
+
|
|
5
|
+
class MockDF:
|
|
6
|
+
shape = (10, 2)
|
|
7
|
+
columns = ["a", "b"]
|
|
8
|
+
dtypes = {"a": "int", "b": "float"}
|
|
9
|
+
def head(self, n): return self
|
|
10
|
+
def to_dict(self, orient): return [{"a": 1, "b": 1.1}]
|
|
11
|
+
|
|
12
|
+
class MockTensor:
|
|
13
|
+
shape = (2, 2)
|
|
14
|
+
dtype = "float32"
|
|
15
|
+
device = "cpu"
|
|
16
|
+
def min(self): return 0.0
|
|
17
|
+
def max(self): return 1.0
|
|
18
|
+
def mean(self): return 0.5
|
|
19
|
+
|
|
20
|
+
class FormatTests(TestCase):
|
|
21
|
+
def test_truncate(self):
|
|
22
|
+
"""Values are truncated."""
|
|
23
|
+
long_str = "x" * 1000
|
|
24
|
+
res = format_value(long_str, max_len=10)
|
|
25
|
+
self.assertEqual(len(res), 13) # 10 + "..."
|
|
26
|
+
|
|
27
|
+
def test_df_formatter(self):
|
|
28
|
+
"""DataFrames are formatted."""
|
|
29
|
+
# Mocking get_module is hard without dependency injection,
|
|
30
|
+
# so we'll test the formatter directly assuming support check passed
|
|
31
|
+
fmt = DFFormatter()
|
|
32
|
+
res = fmt.format(MockDF())
|
|
33
|
+
self.assertEqual(res["_type"], "DataFrame")
|
|
34
|
+
self.assertEqual(res["shape"], [10, 2])
|
|
35
|
+
|
|
36
|
+
def test_tensor_formatter(self):
|
|
37
|
+
"""Tensors are formatted."""
|
|
38
|
+
fmt = TensorFormatter()
|
|
39
|
+
self.assertTrue(fmt.supports(MockTensor()))
|
|
40
|
+
res = fmt.format(MockTensor())
|
|
41
|
+
self.assertEqual(res["_type"], "MockTensor")
|
|
42
|
+
self.assertEqual(res["dtype"], "float32")
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for L{eliot._generators}.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
from unittest import TestCase
|
|
7
|
+
|
|
8
|
+
from logxpy import Message, start_action
|
|
9
|
+
from ..testing import capture_logging, assertHasAction
|
|
10
|
+
|
|
11
|
+
from .._generators import eliot_friendly_generator_function
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assert_expected_action_tree(
|
|
15
|
+
testcase, logger, expected_action_type, expected_type_tree
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Assert that a logger has a certain logged action with certain children.
|
|
19
|
+
|
|
20
|
+
@see: L{assert_generator_logs_action_tree}
|
|
21
|
+
"""
|
|
22
|
+
logged_action = assertHasAction(testcase, logger, expected_action_type, True)
|
|
23
|
+
type_tree = logged_action.type_tree()
|
|
24
|
+
testcase.assertEqual(
|
|
25
|
+
{expected_action_type: expected_type_tree},
|
|
26
|
+
type_tree,
|
|
27
|
+
"Logger had messages:\n{}".format(pformat(logger.messages, indent=4)),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def assert_generator_logs_action_tree(
|
|
32
|
+
testcase, generator_function, logger, expected_action_type, expected_type_tree
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Assert that exhausting a generator from the given function logs an action
|
|
36
|
+
of the given type with children matching the given type tree.
|
|
37
|
+
|
|
38
|
+
@param testcase: A test case instance to use to make assertions.
|
|
39
|
+
@type testcase: L{unittest.TestCase}
|
|
40
|
+
|
|
41
|
+
@param generator_function: A no-argument callable that returns a generator
|
|
42
|
+
to be exhausted.
|
|
43
|
+
|
|
44
|
+
@param logger: A logger to inspect for logged messages.
|
|
45
|
+
@type logger: L{MemoryLogger}
|
|
46
|
+
|
|
47
|
+
@param expected_action_type: An action type which should be logged by the
|
|
48
|
+
generator.
|
|
49
|
+
@type expected_action_type: L{unicode}
|
|
50
|
+
|
|
51
|
+
@param expected_type_tree: The types of actions and messages which should
|
|
52
|
+
be logged beneath the expected action. The structure of this value
|
|
53
|
+
matches the structure returned by L{LoggedAction.type_tree}.
|
|
54
|
+
@type expected_type_tree: L{list}
|
|
55
|
+
"""
|
|
56
|
+
list(eliot_friendly_generator_function(generator_function)())
|
|
57
|
+
assert_expected_action_tree(
|
|
58
|
+
testcase, logger, expected_action_type, expected_type_tree
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EliotFriendlyGeneratorFunctionTests(TestCase):
|
|
63
|
+
"""
|
|
64
|
+
Tests for L{eliot_friendly_generator_function}.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# Get our custom assertion failure messages *and* the standard ones.
|
|
68
|
+
longMessage = True
|
|
69
|
+
|
|
70
|
+
@capture_logging(None)
|
|
71
|
+
def test_yield_none(self, logger):
|
|
72
|
+
@eliot_friendly_generator_function
|
|
73
|
+
def g():
|
|
74
|
+
Message.log(message_type="hello")
|
|
75
|
+
yield
|
|
76
|
+
Message.log(message_type="goodbye")
|
|
77
|
+
|
|
78
|
+
g.debug = True # output yielded messages
|
|
79
|
+
|
|
80
|
+
with start_action(action_type="the-action"):
|
|
81
|
+
list(g())
|
|
82
|
+
|
|
83
|
+
assert_expected_action_tree(
|
|
84
|
+
self, logger, "the-action", ["hello", "yielded", "goodbye"]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@capture_logging(None)
|
|
88
|
+
def test_yield_value(self, logger):
|
|
89
|
+
expected = object()
|
|
90
|
+
|
|
91
|
+
@eliot_friendly_generator_function
|
|
92
|
+
def g():
|
|
93
|
+
Message.log(message_type="hello")
|
|
94
|
+
yield expected
|
|
95
|
+
Message.log(message_type="goodbye")
|
|
96
|
+
|
|
97
|
+
g.debug = True # output yielded messages
|
|
98
|
+
|
|
99
|
+
with start_action(action_type="the-action"):
|
|
100
|
+
self.assertEqual([expected], list(g()))
|
|
101
|
+
|
|
102
|
+
assert_expected_action_tree(
|
|
103
|
+
self, logger, "the-action", ["hello", "yielded", "goodbye"]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@capture_logging(None)
|
|
107
|
+
def test_yield_inside_another_action(self, logger):
|
|
108
|
+
@eliot_friendly_generator_function
|
|
109
|
+
def g():
|
|
110
|
+
Message.log(message_type="a")
|
|
111
|
+
with start_action(action_type="confounding-factor"):
|
|
112
|
+
Message.log(message_type="b")
|
|
113
|
+
yield None
|
|
114
|
+
Message.log(message_type="c")
|
|
115
|
+
Message.log(message_type="d")
|
|
116
|
+
|
|
117
|
+
g.debug = True # output yielded messages
|
|
118
|
+
|
|
119
|
+
with start_action(action_type="the-action"):
|
|
120
|
+
list(g())
|
|
121
|
+
|
|
122
|
+
assert_expected_action_tree(
|
|
123
|
+
self,
|
|
124
|
+
logger,
|
|
125
|
+
"the-action",
|
|
126
|
+
["a", {"confounding-factor": ["b", "yielded", "c"]}, "d"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@capture_logging(None)
|
|
130
|
+
def test_yield_inside_nested_actions(self, logger):
|
|
131
|
+
@eliot_friendly_generator_function
|
|
132
|
+
def g():
|
|
133
|
+
Message.log(message_type="a")
|
|
134
|
+
with start_action(action_type="confounding-factor"):
|
|
135
|
+
Message.log(message_type="b")
|
|
136
|
+
yield None
|
|
137
|
+
with start_action(action_type="double-confounding-factor"):
|
|
138
|
+
yield None
|
|
139
|
+
Message.log(message_type="c")
|
|
140
|
+
Message.log(message_type="d")
|
|
141
|
+
Message.log(message_type="e")
|
|
142
|
+
|
|
143
|
+
g.debug = True # output yielded messages
|
|
144
|
+
|
|
145
|
+
with start_action(action_type="the-action"):
|
|
146
|
+
list(g())
|
|
147
|
+
|
|
148
|
+
assert_expected_action_tree(
|
|
149
|
+
self,
|
|
150
|
+
logger,
|
|
151
|
+
"the-action",
|
|
152
|
+
[
|
|
153
|
+
"a",
|
|
154
|
+
{
|
|
155
|
+
"confounding-factor": [
|
|
156
|
+
"b",
|
|
157
|
+
"yielded",
|
|
158
|
+
{"double-confounding-factor": ["yielded", "c"]},
|
|
159
|
+
"d",
|
|
160
|
+
]
|
|
161
|
+
},
|
|
162
|
+
"e",
|
|
163
|
+
],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@capture_logging(None)
|
|
167
|
+
def test_generator_and_non_generator(self, logger):
|
|
168
|
+
@eliot_friendly_generator_function
|
|
169
|
+
def g():
|
|
170
|
+
Message.log(message_type="a")
|
|
171
|
+
yield
|
|
172
|
+
with start_action(action_type="action-a"):
|
|
173
|
+
Message.log(message_type="b")
|
|
174
|
+
yield
|
|
175
|
+
Message.log(message_type="c")
|
|
176
|
+
|
|
177
|
+
Message.log(message_type="d")
|
|
178
|
+
yield
|
|
179
|
+
|
|
180
|
+
g.debug = True # output yielded messages
|
|
181
|
+
|
|
182
|
+
with start_action(action_type="the-action"):
|
|
183
|
+
generator = g()
|
|
184
|
+
next(generator)
|
|
185
|
+
Message.log(message_type="0")
|
|
186
|
+
next(generator)
|
|
187
|
+
Message.log(message_type="1")
|
|
188
|
+
next(generator)
|
|
189
|
+
Message.log(message_type="2")
|
|
190
|
+
self.assertRaises(StopIteration, lambda: next(generator))
|
|
191
|
+
|
|
192
|
+
assert_expected_action_tree(
|
|
193
|
+
self,
|
|
194
|
+
logger,
|
|
195
|
+
"the-action",
|
|
196
|
+
[
|
|
197
|
+
"a",
|
|
198
|
+
"yielded",
|
|
199
|
+
"0",
|
|
200
|
+
{"action-a": ["b", "yielded", "c"]},
|
|
201
|
+
"1",
|
|
202
|
+
"d",
|
|
203
|
+
"yielded",
|
|
204
|
+
"2",
|
|
205
|
+
],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
@capture_logging(None)
|
|
209
|
+
def test_concurrent_generators(self, logger):
|
|
210
|
+
@eliot_friendly_generator_function
|
|
211
|
+
def g(which):
|
|
212
|
+
Message.log(message_type="{}-a".format(which))
|
|
213
|
+
with start_action(action_type=which):
|
|
214
|
+
Message.log(message_type="{}-b".format(which))
|
|
215
|
+
yield
|
|
216
|
+
Message.log(message_type="{}-c".format(which))
|
|
217
|
+
Message.log(message_type="{}-d".format(which))
|
|
218
|
+
|
|
219
|
+
g.debug = True # output yielded messages
|
|
220
|
+
|
|
221
|
+
gens = [g("1"), g("2")]
|
|
222
|
+
with start_action(action_type="the-action"):
|
|
223
|
+
while gens:
|
|
224
|
+
for g in gens[:]:
|
|
225
|
+
try:
|
|
226
|
+
next(g)
|
|
227
|
+
except StopIteration:
|
|
228
|
+
gens.remove(g)
|
|
229
|
+
|
|
230
|
+
assert_expected_action_tree(
|
|
231
|
+
self,
|
|
232
|
+
logger,
|
|
233
|
+
"the-action",
|
|
234
|
+
[
|
|
235
|
+
"1-a",
|
|
236
|
+
{"1": ["1-b", "yielded", "1-c"]},
|
|
237
|
+
"2-a",
|
|
238
|
+
{"2": ["2-b", "yielded", "2-c"]},
|
|
239
|
+
"1-d",
|
|
240
|
+
"2-d",
|
|
241
|
+
],
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@capture_logging(None)
|
|
245
|
+
def test_close_generator(self, logger):
|
|
246
|
+
@eliot_friendly_generator_function
|
|
247
|
+
def g():
|
|
248
|
+
Message.log(message_type="a")
|
|
249
|
+
try:
|
|
250
|
+
yield
|
|
251
|
+
Message.log(message_type="b")
|
|
252
|
+
finally:
|
|
253
|
+
Message.log(message_type="c")
|
|
254
|
+
|
|
255
|
+
g.debug = True # output yielded messages
|
|
256
|
+
|
|
257
|
+
with start_action(action_type="the-action"):
|
|
258
|
+
gen = g()
|
|
259
|
+
next(gen)
|
|
260
|
+
gen.close()
|
|
261
|
+
|
|
262
|
+
assert_expected_action_tree(self, logger, "the-action", ["a", "yielded", "c"])
|
|
263
|
+
|
|
264
|
+
@capture_logging(None)
|
|
265
|
+
def test_nested_generators(self, logger):
|
|
266
|
+
@eliot_friendly_generator_function
|
|
267
|
+
def g(recurse):
|
|
268
|
+
with start_action(action_type="a-recurse={}".format(recurse)):
|
|
269
|
+
Message.log(message_type="m-recurse={}".format(recurse))
|
|
270
|
+
if recurse:
|
|
271
|
+
set(g(False))
|
|
272
|
+
else:
|
|
273
|
+
yield
|
|
274
|
+
|
|
275
|
+
g.debug = True # output yielded messages
|
|
276
|
+
|
|
277
|
+
with start_action(action_type="the-action"):
|
|
278
|
+
set(g(True))
|
|
279
|
+
|
|
280
|
+
assert_expected_action_tree(
|
|
281
|
+
self,
|
|
282
|
+
logger,
|
|
283
|
+
"the-action",
|
|
284
|
+
[
|
|
285
|
+
{
|
|
286
|
+
"a-recurse=True": [
|
|
287
|
+
"m-recurse=True",
|
|
288
|
+
{"a-recurse=False": ["m-recurse=False", "yielded"]},
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for L{eliot.journald}.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from os import getpid, strerror
|
|
6
|
+
from unittest import skipUnless, TestCase
|
|
7
|
+
from subprocess import check_output, CalledProcessError, STDOUT
|
|
8
|
+
from errno import EINVAL
|
|
9
|
+
from sys import argv
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
from time import sleep
|
|
12
|
+
from json import loads
|
|
13
|
+
|
|
14
|
+
from .._output import MemoryLogger
|
|
15
|
+
from .._message import TASK_UUID_FIELD
|
|
16
|
+
from .. import start_action, Message, write_traceback
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from ..journald import sd_journal_send, JournaldDestination
|
|
20
|
+
except ImportError:
|
|
21
|
+
sd_journal_send = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _journald_available():
|
|
25
|
+
"""
|
|
26
|
+
:return: Boolean indicating whether journald is available to use.
|
|
27
|
+
"""
|
|
28
|
+
if sd_journal_send is None:
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
check_output(["journalctl", "-b", "-n1"], stderr=STDOUT)
|
|
32
|
+
except (OSError, CalledProcessError):
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def last_journald_message():
|
|
38
|
+
"""
|
|
39
|
+
@return: Last journald message from this process as a dictionary in
|
|
40
|
+
journald JSON format.
|
|
41
|
+
"""
|
|
42
|
+
# It may take a little for messages to actually reach journald, so we
|
|
43
|
+
# write out marker message and wait until it arrives. We can then be
|
|
44
|
+
# sure the message right before it is the one we want.
|
|
45
|
+
marker = str(uuid4())
|
|
46
|
+
sd_journal_send(MESSAGE=marker.encode("ascii"))
|
|
47
|
+
for i in range(500):
|
|
48
|
+
messages = check_output(
|
|
49
|
+
[
|
|
50
|
+
b"journalctl",
|
|
51
|
+
b"-a",
|
|
52
|
+
b"-o",
|
|
53
|
+
b"json",
|
|
54
|
+
b"-n2",
|
|
55
|
+
b"_PID=" + str(getpid()).encode("ascii"),
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
messages = [loads(m) for m in messages.splitlines()]
|
|
59
|
+
if len(messages) == 2 and messages[1]["MESSAGE"] == marker:
|
|
60
|
+
return messages[0]
|
|
61
|
+
sleep(0.01)
|
|
62
|
+
raise RuntimeError("Message never arrived?!")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SdJournaldSendTests(TestCase):
|
|
66
|
+
"""
|
|
67
|
+
Functional tests for L{sd_journal_send}.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@skipUnless(
|
|
71
|
+
_journald_available(), "journald unavailable or inactive on this machine."
|
|
72
|
+
)
|
|
73
|
+
def setUp(self):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def assert_roundtrip(self, value):
|
|
77
|
+
"""
|
|
78
|
+
Write a value as a C{MESSAGE} field, assert it is output.
|
|
79
|
+
|
|
80
|
+
@param value: Value to write as unicode.
|
|
81
|
+
"""
|
|
82
|
+
sd_journal_send(MESSAGE=value)
|
|
83
|
+
result = last_journald_message()
|
|
84
|
+
self.assertEqual(value, result["MESSAGE"].encode("utf-8"))
|
|
85
|
+
|
|
86
|
+
def test_message(self):
|
|
87
|
+
"""
|
|
88
|
+
L{sd_journal_send} can write a C{MESSAGE} field.
|
|
89
|
+
"""
|
|
90
|
+
self.assert_roundtrip(b"hello")
|
|
91
|
+
|
|
92
|
+
def test_percent(self):
|
|
93
|
+
"""
|
|
94
|
+
L{sd_journal_send} can write a C{MESSAGE} field with a percent.
|
|
95
|
+
|
|
96
|
+
Underlying C API calls does printf formatting so this is a
|
|
97
|
+
plausible failure mode.
|
|
98
|
+
"""
|
|
99
|
+
self.assert_roundtrip(b"hello%world")
|
|
100
|
+
|
|
101
|
+
def test_large(self):
|
|
102
|
+
"""
|
|
103
|
+
L{sd_journal_send} can write a C{MESSAGE} field with a large message.
|
|
104
|
+
"""
|
|
105
|
+
self.assert_roundtrip(b"hello world" * 20000)
|
|
106
|
+
|
|
107
|
+
def test_multiple_fields(self):
|
|
108
|
+
"""
|
|
109
|
+
L{sd_journal_send} can send multiple fields.
|
|
110
|
+
"""
|
|
111
|
+
sd_journal_send(MESSAGE=b"hello", BONUS_FIELD=b"world")
|
|
112
|
+
result = last_journald_message()
|
|
113
|
+
self.assertEqual(
|
|
114
|
+
(b"hello", b"world"),
|
|
115
|
+
(result["MESSAGE"].encode("ascii"), result["BONUS_FIELD"].encode("ascii")),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def test_error(self):
|
|
119
|
+
"""
|
|
120
|
+
L{sd_journal_send} raises an error when it gets a non-0 result
|
|
121
|
+
from the underlying API.
|
|
122
|
+
"""
|
|
123
|
+
with self.assertRaises(IOError) as context:
|
|
124
|
+
sd_journal_send(**{"": b"123"})
|
|
125
|
+
exc = context.exception
|
|
126
|
+
self.assertEqual((exc.errno, exc.strerror), (EINVAL, strerror(EINVAL)))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class JournaldDestinationTests(TestCase):
|
|
130
|
+
"""
|
|
131
|
+
Tests for L{JournaldDestination}.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
@skipUnless(
|
|
135
|
+
_journald_available(), "journald unavailable or inactive on this machine."
|
|
136
|
+
)
|
|
137
|
+
def setUp(self):
|
|
138
|
+
self.destination = JournaldDestination()
|
|
139
|
+
self.logger = MemoryLogger()
|
|
140
|
+
|
|
141
|
+
def test_json(self):
|
|
142
|
+
"""
|
|
143
|
+
The message is stored as JSON in the MESSAGE field.
|
|
144
|
+
"""
|
|
145
|
+
Message.new(hello="world", key=123).write(self.logger)
|
|
146
|
+
message = self.logger.messages[0]
|
|
147
|
+
self.destination(message)
|
|
148
|
+
self.assertEqual(loads(last_journald_message()["MESSAGE"]), message)
|
|
149
|
+
|
|
150
|
+
def assert_field_for(self, message, field_name, field_value):
|
|
151
|
+
"""
|
|
152
|
+
If the given message is logged by Eliot, the given journald field has
|
|
153
|
+
the expected value.
|
|
154
|
+
|
|
155
|
+
@param message: Dictionary to log.
|
|
156
|
+
@param field_name: Journald field name to check.
|
|
157
|
+
@param field_value: Expected value for the field.
|
|
158
|
+
"""
|
|
159
|
+
self.destination(message)
|
|
160
|
+
self.assertEqual(last_journald_message()[field_name], field_value)
|
|
161
|
+
|
|
162
|
+
def test_action_type(self):
|
|
163
|
+
"""
|
|
164
|
+
The C{action_type} is stored in the ELIOT_TYPE field.
|
|
165
|
+
"""
|
|
166
|
+
action_type = "test:type"
|
|
167
|
+
start_action(self.logger, action_type=action_type)
|
|
168
|
+
self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", action_type)
|
|
169
|
+
|
|
170
|
+
def test_message_type(self):
|
|
171
|
+
"""
|
|
172
|
+
The C{message_type} is stored in the ELIOT_TYPE field.
|
|
173
|
+
"""
|
|
174
|
+
message_type = "test:type:message"
|
|
175
|
+
Message.new(message_type=message_type).write(self.logger)
|
|
176
|
+
self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", message_type)
|
|
177
|
+
|
|
178
|
+
def test_no_type(self):
|
|
179
|
+
"""
|
|
180
|
+
An empty string is stored in ELIOT_TYPE if no type is known.
|
|
181
|
+
"""
|
|
182
|
+
Message.new().write(self.logger)
|
|
183
|
+
self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", "")
|
|
184
|
+
|
|
185
|
+
def test_uuid(self):
|
|
186
|
+
"""
|
|
187
|
+
The task UUID is stored in the ELIOT_TASK field.
|
|
188
|
+
"""
|
|
189
|
+
start_action(self.logger, action_type="xxx")
|
|
190
|
+
self.assert_field_for(
|
|
191
|
+
self.logger.messages[0],
|
|
192
|
+
"ELIOT_TASK",
|
|
193
|
+
self.logger.messages[0][TASK_UUID_FIELD],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def test_info_priorities(self):
|
|
197
|
+
"""
|
|
198
|
+
Untyped messages, action start, successful action end, random typed
|
|
199
|
+
message all get priority 6 ("info").
|
|
200
|
+
"""
|
|
201
|
+
with start_action(self.logger, action_type="xxx"):
|
|
202
|
+
Message.new(message_type="msg").write(self.logger)
|
|
203
|
+
Message.new(x=123).write(self.logger)
|
|
204
|
+
priorities = []
|
|
205
|
+
for message in self.logger.messages:
|
|
206
|
+
self.destination(message)
|
|
207
|
+
priorities.append(last_journald_message()["PRIORITY"])
|
|
208
|
+
self.assertEqual(priorities, ["6", "6", "6", "6"])
|
|
209
|
+
|
|
210
|
+
def test_error_priority(self):
|
|
211
|
+
"""
|
|
212
|
+
A failed action gets priority 3 ("error").
|
|
213
|
+
"""
|
|
214
|
+
try:
|
|
215
|
+
with start_action(self.logger, action_type="xxx"):
|
|
216
|
+
raise ZeroDivisionError()
|
|
217
|
+
except ZeroDivisionError:
|
|
218
|
+
pass
|
|
219
|
+
self.assert_field_for(self.logger.messages[-1], "PRIORITY", "3")
|
|
220
|
+
|
|
221
|
+
def test_critical_priority(self):
|
|
222
|
+
"""
|
|
223
|
+
A traceback gets priority 2 ("critical").
|
|
224
|
+
"""
|
|
225
|
+
try:
|
|
226
|
+
raise ZeroDivisionError()
|
|
227
|
+
except ZeroDivisionError:
|
|
228
|
+
write_traceback(logger=self.logger)
|
|
229
|
+
self.assert_field_for(self.logger.serialize()[-1], "PRIORITY", "2")
|
|
230
|
+
|
|
231
|
+
def test_identifier(self):
|
|
232
|
+
"""
|
|
233
|
+
C{SYSLOG_IDENTIFIER} defaults to C{os.path.basename(sys.argv[0])}.
|
|
234
|
+
"""
|
|
235
|
+
identifier = "/usr/bin/testing123"
|
|
236
|
+
try:
|
|
237
|
+
original = argv[0]
|
|
238
|
+
argv[0] = identifier
|
|
239
|
+
# Recreate JournaldDestination with the newly set argv[0].
|
|
240
|
+
self.destination = JournaldDestination()
|
|
241
|
+
Message.new(message_type="msg").write(self.logger)
|
|
242
|
+
self.assert_field_for(
|
|
243
|
+
self.logger.messages[0], "SYSLOG_IDENTIFIER", "testing123"
|
|
244
|
+
)
|
|
245
|
+
finally:
|
|
246
|
+
argv[0] = original
|