source: trunk/src/allmydata/test/test_eliotutil.py

Last change on this file was 3d4945a, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-12-12T15:33:25Z

Write out Eliot messages in a thread.

  • Property mode set to 100644
File size: 9.5 KB
Line 
1"""
2Tests for ``allmydata.util.eliotutil``.
3"""
4
5from sys import stdout
6import logging
7
8from unittest import (
9    skip,
10)
11
12from fixtures import (
13    TempDir,
14)
15from testtools import (
16    TestCase,
17    TestResult,
18)
19from testtools.matchers import (
20    Is,
21    IsInstance,
22    Not,
23    MatchesStructure,
24    Equals,
25    HasLength,
26    AfterPreprocessing,
27)
28from testtools.twistedsupport import (
29    succeeded,
30    failed,
31)
32
33from eliot import (
34    Message,
35    MessageType,
36    fields,
37    MemoryLogger,
38)
39from eliot.twisted import DeferredContext
40from eliot.testing import (
41    capture_logging,
42    assertHasAction,
43    swap_logger,
44)
45
46from twisted.internet.defer import (
47    succeed,
48)
49from twisted.internet.task import deferLater
50from twisted.internet import reactor
51
52from ..util.eliotutil import (
53    log_call_deferred,
54    _parse_destination_description,
55    _EliotLogging,
56)
57from ..util.deferredutil import async_to_deferred
58
59from .common import (
60    SyncTestCase,
61    AsyncTestCase,
62)
63
64
65def passes():
66    """
67    Create a matcher that matches a ``TestCase`` that runs without failures or
68    errors.
69    """
70    def run(case):
71        result = TestResult()
72        case.run(result)
73        return result.wasSuccessful()
74    return AfterPreprocessing(run, Equals(True))
75
76
77class EliotLoggedTestTests(TestCase):
78    """
79    Tests for the automatic log-related provided by ``AsyncTestCase``.
80
81    This class uses ``testtools.TestCase`` because it is inconvenient to nest
82    ``AsyncTestCase`` inside ``AsyncTestCase`` (in particular, Eliot messages
83    emitted by the inner test case get observed by the outer test case and if
84    an inner case emits invalid messages they cause the outer test case to
85    fail).
86    """
87    def test_fails(self):
88        """
89        A test method of an ``AsyncTestCase`` subclass can fail.
90        """
91        class UnderTest(AsyncTestCase):
92            def test_it(self):
93                self.fail("make sure it can fail")
94
95        self.assertThat(UnderTest("test_it"), Not(passes()))
96
97    def test_unserializable_fails(self):
98        """
99        A test method of an ``AsyncTestCase`` subclass that logs an unserializable
100        value with Eliot fails.
101        """
102        class world(object):
103            """
104            an unserializable object
105            """
106
107        class UnderTest(AsyncTestCase):
108            def test_it(self):
109                Message.log(hello=world)
110
111        self.assertThat(UnderTest("test_it"), Not(passes()))
112
113    def test_logs_non_utf_8_byte(self):
114        """
115        A test method of an ``AsyncTestCase`` subclass can log a message that
116        contains a non-UTF-8 byte string and return ``None`` and pass.
117        """
118        class UnderTest(AsyncTestCase):
119            def test_it(self):
120                Message.log(hello=b"\xFF")
121
122        self.assertThat(UnderTest("test_it"), passes())
123
124    def test_returns_none(self):
125        """
126        A test method of an ``AsyncTestCase`` subclass can log a message and
127        return ``None`` and pass.
128        """
129        class UnderTest(AsyncTestCase):
130            def test_it(self):
131                Message.log(hello="world")
132
133        self.assertThat(UnderTest("test_it"), passes())
134
135    def test_returns_fired_deferred(self):
136        """
137        A test method of an ``AsyncTestCase`` subclass can log a message and
138        return an already-fired ``Deferred`` and pass.
139        """
140        class UnderTest(AsyncTestCase):
141            def test_it(self):
142                Message.log(hello="world")
143                return succeed(None)
144
145        self.assertThat(UnderTest("test_it"), passes())
146
147    def test_returns_unfired_deferred(self):
148        """
149        A test method of an ``AsyncTestCase`` subclass can log a message and
150        return an unfired ``Deferred`` and pass when the ``Deferred`` fires.
151        """
152        class UnderTest(AsyncTestCase):
153            def test_it(self):
154                Message.log(hello="world")
155                # @eliot_logged_test automatically gives us an action context
156                # but it's still our responsibility to maintain it across
157                # stack-busting operations.
158                d = DeferredContext(deferLater(reactor, 0.0, lambda: None))
159                d.addCallback(lambda ignored: Message.log(goodbye="world"))
160                # We didn't start an action.  We're not finishing an action.
161                return d.result
162
163        self.assertThat(UnderTest("test_it"), passes())
164
165
166class ParseDestinationDescriptionTests(SyncTestCase):
167    """
168    Tests for ``_parse_destination_description``.
169    """
170    def test_stdout(self):
171        """
172        A ``file:`` description with a path of ``-`` causes logs to be written to
173        stdout.
174        """
175        reactor = object()
176        self.assertThat(
177            _parse_destination_description("file:-")(reactor).file,
178            Equals(stdout),
179        )
180
181
182    def test_regular_file(self):
183        """
184        A ``file:`` description with any path other than ``-`` causes logs to be
185        written to a file with that name.
186        """
187        tempdir = TempDir()
188        self.useFixture(tempdir)
189
190        reactor = object()
191        path = tempdir.join("regular_file")
192
193        self.assertThat(
194            _parse_destination_description("file:{}".format(path))(reactor),
195            MatchesStructure(
196                file=MatchesStructure(
197                    path=Equals(path),
198                    rotateLength=AfterPreprocessing(bool, Equals(True)),
199                    maxRotatedFiles=AfterPreprocessing(bool, Equals(True)),
200                ),
201            ),
202        )
203
204
205# We need AsyncTestCase because logging happens in a thread tied to the
206# reactor.
207class EliotLoggingTests(AsyncTestCase):
208    """
209    Tests for ``_EliotLogging``.
210    """
211    @async_to_deferred
212    async def test_stdlib_event_relayed(self):
213        """
214        An event logged using the stdlib logging module is delivered to the Eliot
215        destination.
216        """
217        collected = []
218        service = _EliotLogging([collected.append])
219        service.startService()
220
221        logging.critical("oh no")
222        await service.stopService()
223
224        self.assertTrue(
225            "oh no" in str(collected[-1]), collected
226        )
227
228    @async_to_deferred
229    async def test_twisted_event_relayed(self):
230        """
231        An event logged with a ``twisted.logger.Logger`` is delivered to the Eliot
232        destination.
233        """
234        collected = []
235        service = _EliotLogging([collected.append])
236        service.startService()
237
238        from twisted.logger import Logger
239        Logger().critical("oh no")
240        await service.stopService()
241
242        self.assertTrue(
243            "oh no" in str(collected[-1]), collected
244        )
245
246    def test_validation_failure(self):
247        """
248        If a test emits a log message that fails validation then an error is added
249        to the result.
250        """
251        # Make sure we preserve the original global Eliot state.
252        original = swap_logger(MemoryLogger())
253        self.addCleanup(lambda: swap_logger(original))
254
255        class ValidationFailureProbe(SyncTestCase):
256            def test_bad_message(self):
257                # This message does not validate because "Hello" is not an
258                # int.
259                MSG = MessageType("test:eliotutil", fields(foo=int))
260                MSG(foo="Hello").write()
261
262        result = TestResult()
263        case = ValidationFailureProbe("test_bad_message")
264        case.run(result)
265
266        self.assertThat(
267            result.errors,
268            HasLength(1),
269        )
270
271    def test_skip_cleans_up(self):
272        """
273        After a skipped test the global Eliot logging state is restored.
274        """
275        # Save the logger that's active before we do anything so that we can
276        # restore it later.  Also install another logger so we can compare it
277        # to the active logger later.
278        expected = MemoryLogger()
279        original = swap_logger(expected)
280
281        # Restore it, whatever else happens.
282        self.addCleanup(lambda: swap_logger(original))
283
284        class SkipProbe(SyncTestCase):
285            @skip("It's a skip test.")
286            def test_skipped(self):
287                pass
288
289        case = SkipProbe("test_skipped")
290        case.run()
291
292        # Retrieve the logger that's active now that the skipped test is done
293        # so we can check it against the expected value.
294        actual = swap_logger(MemoryLogger())
295        self.assertThat(
296            actual,
297            Is(expected),
298        )
299
300
301class LogCallDeferredTests(TestCase):
302    """
303    Tests for ``log_call_deferred``.
304    """
305    @capture_logging(
306        lambda self, logger:
307        assertHasAction(self, logger, u"the-action", succeeded=True),
308    )
309    def test_return_value(self, logger):
310        """
311        The decorated function's return value is passed through.
312        """
313        result = object()
314        @log_call_deferred(action_type=u"the-action")
315        def f():
316            return result
317        self.assertThat(f(), succeeded(Is(result)))
318
319    @capture_logging(
320        lambda self, logger:
321        assertHasAction(self, logger, u"the-action", succeeded=False),
322    )
323    def test_raise_exception(self, logger):
324        """
325        An exception raised by the decorated function is passed through.
326        """
327        class Result(Exception):
328            pass
329        @log_call_deferred(action_type=u"the-action")
330        def f():
331            raise Result()
332        self.assertThat(
333            f(),
334            failed(
335                AfterPreprocessing(
336                    lambda f: f.value,
337                    IsInstance(Result),
338                ),
339            ),
340        )
Note: See TracBrowser for help on using the repository browser.