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,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for L{eliot.json}.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from unittest import TestCase, skipUnless, skipIf
|
|
6
|
+
from json import loads
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import numpy as np
|
|
11
|
+
except ImportError:
|
|
12
|
+
np = None
|
|
13
|
+
|
|
14
|
+
from logxpy.json import (
|
|
15
|
+
EliotJSONEncoder,
|
|
16
|
+
json_default,
|
|
17
|
+
_encoder_to_default_function,
|
|
18
|
+
_dumps_unicode as dumps,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def package_installed(name: str) -> bool:
|
|
23
|
+
"""Return whether the package is installed."""
|
|
24
|
+
try:
|
|
25
|
+
package_version(name)
|
|
26
|
+
return True
|
|
27
|
+
except PackageNotFoundError:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EliotJSONEncoderTests(TestCase):
|
|
32
|
+
"""Tests for L{EliotJSONEncoder} and L{json_default}."""
|
|
33
|
+
|
|
34
|
+
@skipUnless(np, "NumPy not installed.")
|
|
35
|
+
def test_numpy(self):
|
|
36
|
+
"""NumPy objects get serialized to readable JSON."""
|
|
37
|
+
encoder_default = _encoder_to_default_function(EliotJSONEncoder())
|
|
38
|
+
l = [
|
|
39
|
+
np.float32(12.5),
|
|
40
|
+
np.float64(2.0),
|
|
41
|
+
np.float16(0.5),
|
|
42
|
+
np.bool_(True),
|
|
43
|
+
np.str_("hello"),
|
|
44
|
+
np.byte(12),
|
|
45
|
+
np.short(12),
|
|
46
|
+
np.intc(-13),
|
|
47
|
+
np.int_(0),
|
|
48
|
+
np.longlong(100),
|
|
49
|
+
np.intp(7),
|
|
50
|
+
np.ubyte(12),
|
|
51
|
+
np.ushort(12),
|
|
52
|
+
np.uintc(13),
|
|
53
|
+
np.ulonglong(100),
|
|
54
|
+
np.uintp(7),
|
|
55
|
+
np.int8(1),
|
|
56
|
+
np.int16(3),
|
|
57
|
+
np.int32(4),
|
|
58
|
+
np.int64(5),
|
|
59
|
+
np.uint8(1),
|
|
60
|
+
np.uint16(3),
|
|
61
|
+
np.uint32(4),
|
|
62
|
+
np.uint64(5),
|
|
63
|
+
]
|
|
64
|
+
l2 = [l, np.array([1, 2, 3])]
|
|
65
|
+
roundtripped = loads(dumps(l2, default=encoder_default))
|
|
66
|
+
self.assertEqual([l, [1, 2, 3]], roundtripped)
|
|
67
|
+
roundtripped2 = loads(dumps(l2, default=json_default))
|
|
68
|
+
self.assertEqual([l, [1, 2, 3]], roundtripped2)
|
|
69
|
+
|
|
70
|
+
@skipIf(np, "NumPy is installed.")
|
|
71
|
+
def test_numpy_not_imported(self):
|
|
72
|
+
"""If NumPy is not available, C{json_default} continues to work.
|
|
73
|
+
|
|
74
|
+
This ensures NumPy isn't a hard dependency.
|
|
75
|
+
"""
|
|
76
|
+
with self.assertRaises(TypeError):
|
|
77
|
+
dumps([object()], default=json_default)
|
|
78
|
+
self.assertEqual(dumps(12, default=json_default), "12")
|
|
79
|
+
|
|
80
|
+
@skipUnless(np, "NumPy is not installed.")
|
|
81
|
+
def test_large_numpy_array(self):
|
|
82
|
+
"""
|
|
83
|
+
Large NumPy arrays are not serialized completely, since this is (A) a
|
|
84
|
+
performance hit (B) probably a mistake on the user's part.
|
|
85
|
+
"""
|
|
86
|
+
a1000 = np.array([0] * 10000)
|
|
87
|
+
self.assertEqual(loads(dumps(a1000, default=json_default)), a1000.tolist())
|
|
88
|
+
a1002 = np.zeros((2, 5001))
|
|
89
|
+
a1002[0][0] = 12
|
|
90
|
+
a1002[0][1] = 13
|
|
91
|
+
a1002[1][1] = 500
|
|
92
|
+
self.assertEqual(
|
|
93
|
+
loads(dumps(a1002, default=json_default)),
|
|
94
|
+
{"array_start": a1002.flat[:10000].tolist(), "original_shape": [2, 5001]},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def test_basic_types(self):
|
|
98
|
+
"""Test serialization of basic Python types."""
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
from datetime import datetime, date, time
|
|
101
|
+
from uuid import UUID
|
|
102
|
+
from collections import defaultdict, OrderedDict, Counter
|
|
103
|
+
from enum import Enum
|
|
104
|
+
|
|
105
|
+
class TestEnum(Enum):
|
|
106
|
+
A = 1
|
|
107
|
+
B = "test"
|
|
108
|
+
|
|
109
|
+
test_data = {
|
|
110
|
+
"path": Path("/tmp/test"),
|
|
111
|
+
"datetime": datetime(2024, 1, 1, 12, 0),
|
|
112
|
+
"date": date(2024, 1, 1),
|
|
113
|
+
"time": time(12, 0),
|
|
114
|
+
"uuid": UUID("12345678-1234-5678-1234-567812345678"),
|
|
115
|
+
"set": {1, 2, 3},
|
|
116
|
+
"defaultdict": defaultdict(list, {"a": [1, 2]}),
|
|
117
|
+
"ordered_dict": OrderedDict([("a", 1), ("b", 2)]),
|
|
118
|
+
"counter": Counter(["a", "a", "b"]),
|
|
119
|
+
"complex": 1 + 2j,
|
|
120
|
+
"enum": TestEnum.A,
|
|
121
|
+
"enum2": TestEnum.B,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
serialized = loads(dumps(test_data, default=json_default))
|
|
125
|
+
|
|
126
|
+
self.assertEqual(serialized["path"], "/tmp/test")
|
|
127
|
+
self.assertEqual(serialized["datetime"], "2024-01-01T12:00:00")
|
|
128
|
+
self.assertEqual(serialized["date"], "2024-01-01")
|
|
129
|
+
self.assertEqual(serialized["time"], "12:00:00")
|
|
130
|
+
self.assertEqual(serialized["uuid"], "12345678-1234-5678-1234-567812345678")
|
|
131
|
+
self.assertEqual(serialized["set"], [1, 2, 3])
|
|
132
|
+
self.assertEqual(serialized["defaultdict"], {"a": [1, 2]})
|
|
133
|
+
self.assertEqual(serialized["ordered_dict"], {"a": 1, "b": 2})
|
|
134
|
+
self.assertEqual(serialized["counter"], {"a": 2, "b": 1})
|
|
135
|
+
self.assertEqual(serialized["complex"], {"real": 1.0, "imag": 2.0})
|
|
136
|
+
self.assertEqual(serialized["enum"], 1)
|
|
137
|
+
self.assertEqual(serialized["enum2"], "test")
|
|
138
|
+
|
|
139
|
+
@skipUnless(package_installed("pydantic"), "Pydantic not installed.")
|
|
140
|
+
def test_pydantic(self):
|
|
141
|
+
"""Test serialization of Pydantic models."""
|
|
142
|
+
from pydantic import BaseModel
|
|
143
|
+
|
|
144
|
+
class TestModel(BaseModel):
|
|
145
|
+
name: str
|
|
146
|
+
value: int
|
|
147
|
+
|
|
148
|
+
model = TestModel(name="test", value=42)
|
|
149
|
+
serialized = loads(dumps(model, default=json_default))
|
|
150
|
+
self.assertEqual(serialized, {"name": "test", "value": 42})
|
|
151
|
+
|
|
152
|
+
@skipUnless(package_installed("pandas"), "Pandas not installed.")
|
|
153
|
+
def test_pandas(self):
|
|
154
|
+
"""Test serialization of Pandas objects."""
|
|
155
|
+
import pandas as pd
|
|
156
|
+
|
|
157
|
+
# Test Timestamp
|
|
158
|
+
ts = pd.Timestamp("2024-01-01 12:00:00")
|
|
159
|
+
self.assertEqual(loads(dumps(ts, default=json_default)), "2024-01-01T12:00:00")
|
|
160
|
+
|
|
161
|
+
# Test Series
|
|
162
|
+
series = pd.Series([1, 2, 3])
|
|
163
|
+
self.assertEqual(loads(dumps(series, default=json_default)), [1, 2, 3])
|
|
164
|
+
|
|
165
|
+
# Test DataFrame
|
|
166
|
+
df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
|
|
167
|
+
self.assertEqual(
|
|
168
|
+
loads(dumps(df, default=json_default)), [{"a": 1, "b": 3}, {"a": 2, "b": 4}]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Test Interval
|
|
172
|
+
interval = pd.Interval(0, 1, closed="both")
|
|
173
|
+
self.assertEqual(
|
|
174
|
+
loads(dumps(interval, default=json_default)),
|
|
175
|
+
{"left": 0, "right": 1, "closed": "both"},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Test Period
|
|
179
|
+
period = pd.Period("2024-01")
|
|
180
|
+
self.assertEqual(loads(dumps(period, default=json_default)), "2024-01")
|
|
181
|
+
|
|
182
|
+
@skipUnless(package_installed("polars"), "Polars not installed.")
|
|
183
|
+
def test_polars(self):
|
|
184
|
+
"""Test serialization of Polars objects."""
|
|
185
|
+
import polars as pl
|
|
186
|
+
|
|
187
|
+
# Test Series
|
|
188
|
+
series = pl.Series("a", [1, 2, 3])
|
|
189
|
+
self.assertEqual(loads(dumps(series, default=json_default)), [1, 2, 3])
|
|
190
|
+
|
|
191
|
+
# Test DataFrame
|
|
192
|
+
df = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
|
|
193
|
+
self.assertEqual(
|
|
194
|
+
loads(dumps(df, default=json_default)), [{"a": 1, "b": 3}, {"a": 2, "b": 4}]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def test_dataclass(self):
|
|
198
|
+
"""Test serialization of dataclasses."""
|
|
199
|
+
from dataclasses import dataclass
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class TestDataClass:
|
|
203
|
+
name: str
|
|
204
|
+
value: int
|
|
205
|
+
|
|
206
|
+
obj = TestDataClass(name="test", value=42)
|
|
207
|
+
serialized = loads(dumps(obj, default=json_default))
|
|
208
|
+
self.assertEqual(serialized, {"name": "test", "value": 42})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tests for eliot.loggerx."""
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
from logxpy.loggerx import Logger, log
|
|
4
|
+
from logxpy._async import _emit_handlers
|
|
5
|
+
from logxpy._types import Level
|
|
6
|
+
|
|
7
|
+
class LoggerTests(TestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self.records = []
|
|
10
|
+
_emit_handlers.append(self.records.append)
|
|
11
|
+
|
|
12
|
+
def tearDown(self):
|
|
13
|
+
_emit_handlers.remove(self.records.append)
|
|
14
|
+
|
|
15
|
+
def test_levels(self):
|
|
16
|
+
"""Logger methods emit correct levels."""
|
|
17
|
+
log.info("info msg")
|
|
18
|
+
log.error("error msg")
|
|
19
|
+
|
|
20
|
+
self.assertEqual(len(self.records), 2)
|
|
21
|
+
self.assertEqual(self.records[0].level, Level.INFO)
|
|
22
|
+
self.assertEqual(self.records[1].level, Level.ERROR)
|
|
23
|
+
|
|
24
|
+
def test_fluent(self):
|
|
25
|
+
"""Logger methods return self."""
|
|
26
|
+
l = Logger()
|
|
27
|
+
self.assertIs(l.info("msg"), l)
|
|
28
|
+
|
|
29
|
+
def test_context(self):
|
|
30
|
+
"""Logger respects scope."""
|
|
31
|
+
with log.scope(ctx=1):
|
|
32
|
+
log.info("msg")
|
|
33
|
+
|
|
34
|
+
self.assertEqual(self.records[0].context["ctx"], 1)
|
|
35
|
+
|
|
36
|
+
def test_types(self):
|
|
37
|
+
"""Type methods format data."""
|
|
38
|
+
log.json({"a": 1})
|
|
39
|
+
self.assertIn('"a": 1', self.records[0].fields["content"])
|
|
40
|
+
|
|
41
|
+
def test_child(self):
|
|
42
|
+
"""Child logger inherits settings."""
|
|
43
|
+
child = log.new("child")
|
|
44
|
+
self.assertEqual(child._name, "root.child")
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for L{eliot.logwriter}.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
from io import BytesIO
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from zope.interface.verify import verifyClass
|
|
12
|
+
from twisted.internet import reactor
|
|
13
|
+
from twisted.trial.unittest import TestCase
|
|
14
|
+
from twisted.application.service import IService
|
|
15
|
+
from twisted.python import threadable
|
|
16
|
+
except ImportError:
|
|
17
|
+
# Make tests not run at all.
|
|
18
|
+
TestCase = object
|
|
19
|
+
else:
|
|
20
|
+
# Make sure we always import this if Twisted is available, so broken
|
|
21
|
+
# logwriter.py causes a failure:
|
|
22
|
+
from ..logwriter import ThreadedWriter
|
|
23
|
+
|
|
24
|
+
from .. import Logger, removeDestination, FileDestination
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BlockingFile(object):
|
|
28
|
+
"""
|
|
29
|
+
A file-like whose writes can be blocked.
|
|
30
|
+
|
|
31
|
+
Also, allow calling C{getvalue} after C{close}, unlike L{BytesIO}.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.file = BytesIO()
|
|
36
|
+
self.lock = threading.Lock()
|
|
37
|
+
self.data = b""
|
|
38
|
+
|
|
39
|
+
def block(self):
|
|
40
|
+
"""
|
|
41
|
+
Prevent writes until L{unblock} is called.
|
|
42
|
+
"""
|
|
43
|
+
self.lock.acquire()
|
|
44
|
+
|
|
45
|
+
def unblock(self):
|
|
46
|
+
"""
|
|
47
|
+
Allow writes if L{block} was previous called.
|
|
48
|
+
"""
|
|
49
|
+
self.lock.release()
|
|
50
|
+
|
|
51
|
+
def getvalue(self):
|
|
52
|
+
"""
|
|
53
|
+
Get written bytes.
|
|
54
|
+
|
|
55
|
+
@return: Written bytes.
|
|
56
|
+
"""
|
|
57
|
+
return self.data
|
|
58
|
+
|
|
59
|
+
def write(self, data):
|
|
60
|
+
with self.lock:
|
|
61
|
+
self.file.write(data)
|
|
62
|
+
|
|
63
|
+
def flush(self):
|
|
64
|
+
self.data = self.file.getvalue()
|
|
65
|
+
|
|
66
|
+
def close(self):
|
|
67
|
+
self.file.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ThreadedWriterTests(TestCase):
|
|
71
|
+
"""
|
|
72
|
+
Tests for L{ThreadedWriter}.
|
|
73
|
+
|
|
74
|
+
Many of these tests involve interactions across threads, so they
|
|
75
|
+
arbitrarily wait for up to 5 seconds to reduce chances of slow thread
|
|
76
|
+
switching causing the test to fail.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def test_interface(self):
|
|
80
|
+
"""
|
|
81
|
+
L{ThreadedWriter} provides L{IService}.
|
|
82
|
+
"""
|
|
83
|
+
verifyClass(IService, ThreadedWriter)
|
|
84
|
+
|
|
85
|
+
def test_name(self):
|
|
86
|
+
"""
|
|
87
|
+
L{ThreadedWriter} has a name.
|
|
88
|
+
"""
|
|
89
|
+
self.assertEqual(ThreadedWriter.name, "Eliot Log Writer")
|
|
90
|
+
|
|
91
|
+
def test_startServiceRunning(self):
|
|
92
|
+
"""
|
|
93
|
+
L{ThreadedWriter.startService} starts the service as required by the
|
|
94
|
+
L{IService} interface.
|
|
95
|
+
"""
|
|
96
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
97
|
+
self.assertFalse(writer.running)
|
|
98
|
+
writer.startService()
|
|
99
|
+
self.addCleanup(writer.stopService)
|
|
100
|
+
self.assertTrue(writer.running)
|
|
101
|
+
|
|
102
|
+
def test_stopServiceRunning(self):
|
|
103
|
+
"""
|
|
104
|
+
L{ThreadedWriter.stopService} stops the service as required by the
|
|
105
|
+
L{IService} interface.
|
|
106
|
+
"""
|
|
107
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
108
|
+
writer.startService()
|
|
109
|
+
d = writer.stopService()
|
|
110
|
+
d.addCallback(lambda _: self.assertFalse(writer.running))
|
|
111
|
+
return d
|
|
112
|
+
|
|
113
|
+
def test_startServiceStartsThread(self):
|
|
114
|
+
"""
|
|
115
|
+
L{ThreadedWriter.startService} starts up a thread running
|
|
116
|
+
L{ThreadedWriter._writer}.
|
|
117
|
+
"""
|
|
118
|
+
previousThreads = threading.enumerate()
|
|
119
|
+
result = []
|
|
120
|
+
event = threading.Event()
|
|
121
|
+
|
|
122
|
+
def _reader():
|
|
123
|
+
current = threading.current_thread()
|
|
124
|
+
if current not in previousThreads:
|
|
125
|
+
result.append(current)
|
|
126
|
+
event.set()
|
|
127
|
+
|
|
128
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
129
|
+
writer._reader = _reader
|
|
130
|
+
writer.startService()
|
|
131
|
+
event.wait()
|
|
132
|
+
self.assertTrue(result)
|
|
133
|
+
# Make sure thread is dead so it doesn't die half way through another
|
|
134
|
+
# test:
|
|
135
|
+
result[0].join(5)
|
|
136
|
+
|
|
137
|
+
def test_stopServiceStopsThread(self):
|
|
138
|
+
"""
|
|
139
|
+
L{ThreadedWriter.stopService} stops the writer thread.
|
|
140
|
+
"""
|
|
141
|
+
previousThreads = set(threading.enumerate())
|
|
142
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
143
|
+
writer.startService()
|
|
144
|
+
start = time.time()
|
|
145
|
+
while set(threading.enumerate()) == previousThreads and (
|
|
146
|
+
time.time() - start < 5
|
|
147
|
+
):
|
|
148
|
+
time.sleep(0.0001)
|
|
149
|
+
# If not true the next assertion might pass by mistake:
|
|
150
|
+
self.assertNotEqual(set(threading.enumerate()), previousThreads)
|
|
151
|
+
writer.stopService()
|
|
152
|
+
while set(threading.enumerate()) != previousThreads and (
|
|
153
|
+
time.time() - start < 5
|
|
154
|
+
):
|
|
155
|
+
time.sleep(0.0001)
|
|
156
|
+
self.assertEqual(set(threading.enumerate()), previousThreads)
|
|
157
|
+
|
|
158
|
+
def test_stopServiceFinishesWriting(self):
|
|
159
|
+
"""
|
|
160
|
+
L{ThreadedWriter.stopService} stops the writer thread, but only after
|
|
161
|
+
all queued writes are written out.
|
|
162
|
+
"""
|
|
163
|
+
f = BlockingFile()
|
|
164
|
+
writer = ThreadedWriter(FileDestination(file=f), reactor)
|
|
165
|
+
f.block()
|
|
166
|
+
writer.startService()
|
|
167
|
+
for i in range(100):
|
|
168
|
+
writer({"write": 123})
|
|
169
|
+
threads = threading.enumerate()
|
|
170
|
+
writer.stopService()
|
|
171
|
+
# Make sure writes didn't happen before the stopService, thus making the
|
|
172
|
+
# test pointless:
|
|
173
|
+
self.assertEqual(f.getvalue(), b"")
|
|
174
|
+
f.unblock()
|
|
175
|
+
start = time.time()
|
|
176
|
+
while threading.enumerate() == threads and time.time() - start < 5:
|
|
177
|
+
time.sleep(0.0001)
|
|
178
|
+
self.assertEqual(f.getvalue(), b'{"write":123}\n' * 100)
|
|
179
|
+
|
|
180
|
+
def test_stopServiceResult(self):
|
|
181
|
+
"""
|
|
182
|
+
L{ThreadedWriter.stopService} returns a L{Deferred} that fires only
|
|
183
|
+
after the thread has shut down.
|
|
184
|
+
"""
|
|
185
|
+
f = BlockingFile()
|
|
186
|
+
writer = ThreadedWriter(FileDestination(file=f), reactor)
|
|
187
|
+
f.block()
|
|
188
|
+
writer.startService()
|
|
189
|
+
|
|
190
|
+
writer({"hello": 123})
|
|
191
|
+
threads = threading.enumerate()
|
|
192
|
+
d = writer.stopService()
|
|
193
|
+
f.unblock()
|
|
194
|
+
|
|
195
|
+
def done(_):
|
|
196
|
+
self.assertEqual(f.getvalue(), b'{"hello":123}\n')
|
|
197
|
+
self.assertNotEqual(threading.enumerate(), threads)
|
|
198
|
+
|
|
199
|
+
d.addCallback(done)
|
|
200
|
+
return d
|
|
201
|
+
|
|
202
|
+
def test_noChangeToIOThread(self):
|
|
203
|
+
"""
|
|
204
|
+
Running a L{ThreadedWriter} doesn't modify the Twisted registered IO
|
|
205
|
+
thread.
|
|
206
|
+
"""
|
|
207
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
208
|
+
writer.startService()
|
|
209
|
+
d = writer.stopService()
|
|
210
|
+
# Either the current thread (the one running the tests) is the the I/O
|
|
211
|
+
# thread or the I/O thread was never set. Either may happen depending on
|
|
212
|
+
# how and whether the reactor has been started by the unittesting
|
|
213
|
+
# framework.
|
|
214
|
+
d.addCallback(
|
|
215
|
+
lambda _: self.assertIn(
|
|
216
|
+
threadable.ioThread, (None, threading.current_thread().ident)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
return d
|
|
220
|
+
|
|
221
|
+
def test_startServiceRegistersDestination(self):
|
|
222
|
+
"""
|
|
223
|
+
L{ThreadedWriter.startService} registers itself as an Eliot log
|
|
224
|
+
destination.
|
|
225
|
+
"""
|
|
226
|
+
f = BlockingFile()
|
|
227
|
+
writer = ThreadedWriter(FileDestination(file=f), reactor)
|
|
228
|
+
writer.startService()
|
|
229
|
+
Logger().write({"x": "abc"})
|
|
230
|
+
d = writer.stopService()
|
|
231
|
+
d.addCallback(lambda _: self.assertIn(b"abc", f.getvalue()))
|
|
232
|
+
return d
|
|
233
|
+
|
|
234
|
+
def test_stopServiceUnregistersDestination(self):
|
|
235
|
+
"""
|
|
236
|
+
L{ThreadedWriter.stopService} unregisters itself as an Eliot log
|
|
237
|
+
destination.
|
|
238
|
+
"""
|
|
239
|
+
writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
|
|
240
|
+
writer.startService()
|
|
241
|
+
d = writer.stopService()
|
|
242
|
+
d.addCallback(lambda _: removeDestination(writer))
|
|
243
|
+
return self.assertFailure(d, ValueError)
|
|
244
|
+
|
|
245
|
+
def test_call(self):
|
|
246
|
+
"""
|
|
247
|
+
The message passed to L{ThreadedWriter.__call__} is passed to the
|
|
248
|
+
underlying destination in the writer thread.
|
|
249
|
+
"""
|
|
250
|
+
result = []
|
|
251
|
+
|
|
252
|
+
def destination(message):
|
|
253
|
+
result.append((message, threading.current_thread().ident))
|
|
254
|
+
|
|
255
|
+
writer = ThreadedWriter(destination, reactor)
|
|
256
|
+
writer.startService()
|
|
257
|
+
thread_ident = writer._thread.ident
|
|
258
|
+
msg = {"key": 123}
|
|
259
|
+
writer(msg)
|
|
260
|
+
d = writer.stopService()
|
|
261
|
+
d.addCallback(lambda _: self.assertEqual(result, [(msg, thread_ident)]))
|
|
262
|
+
return d
|