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

Last change on this file was 83fa028, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-11-16T18:53:51Z

Use the existing Tahoe JSON encoder.

  • Property mode set to 100644
File size: 4.8 KB
Line 
1"""
2Tools aimed at the interaction between tests and Eliot.
3
4Ported to Python 3.
5"""
6
7from six import ensure_text
8
9__all__ = [
10    "RUN_TEST",
11    "EliotLoggedRunTest",
12]
13
14from typing import Callable
15from functools import (
16    partial,
17    wraps,
18)
19
20import attr
21
22from zope.interface import (
23    implementer,
24)
25
26from eliot import (
27    ActionType,
28    Field,
29    ILogger,
30)
31from eliot.testing import (
32    MemoryLogger,
33    swap_logger,
34    check_for_errors,
35)
36
37from twisted.python.monkey import (
38    MonkeyPatcher,
39)
40
41from ..util.jsonbytes import (
42    AnyBytesJSONEncoder
43)
44
45_NAME = Field.for_types(
46    u"name",
47    [str],
48    u"The name of the test.",
49)
50
51RUN_TEST = ActionType(
52    u"run-test",
53    [_NAME],
54    [],
55    u"A test is run.",
56)
57
58
59@attr.s
60class EliotLoggedRunTest(object):
61    """
62    A *RunTest* implementation which surrounds test invocation with an
63    Eliot-based action.
64
65    This *RunTest* composes with another for convenience.
66
67    :ivar case: The test case to run.
68
69    :ivar handlers: Pass-through for the wrapped *RunTest*.
70    :ivar last_resort: Pass-through for the wrapped *RunTest*.
71
72    :ivar _run_tests_with_factory: A factory for the other *RunTest*.
73    """
74    _run_tests_with_factory = attr.ib()
75    case = attr.ib()
76    handlers = attr.ib(default=None)
77    last_resort = attr.ib(default=None)
78
79    @classmethod
80    def make_factory(cls, delegated_run_test_factory):
81        return partial(cls, delegated_run_test_factory)
82
83    @property
84    def eliot_logger(self):
85        return self.case.eliot_logger
86
87    @eliot_logger.setter
88    def eliot_logger(self, value):
89        self.case.eliot_logger = value
90
91    def addCleanup(self, *a, **kw):
92        return self.case.addCleanup(*a, **kw)
93
94    def id(self):
95        return self.case.id()
96
97    def run(self, result):
98        """
99        Run the test case in the context of a distinct Eliot action.
100
101        The action will finish after the test is done.  It will note the name of
102        the test being run.
103
104        All messages emitted by the test will be validated.  They will still be
105        delivered to the global logger.
106        """
107        # The idea here is to decorate the test method itself so that all of
108        # the extra logic happens at the point where test/application logic is
109        # expected to be.  This `run` method is more like test infrastructure
110        # and things do not go well when we add too much extra behavior here.
111        # For example, exceptions raised here often just kill the whole
112        # runner.
113        patcher = MonkeyPatcher()
114
115        # So, grab the test method.
116        name = self.case._testMethodName
117        original = getattr(self.case, name)
118        decorated = with_logging(ensure_text(self.case.id()), original)
119        patcher.addPatch(self.case, name, decorated)
120        try:
121            # Patch it in
122            patcher.patch()
123            # Then use the rest of the machinery to run it.
124            return self._run_tests_with_factory(
125                self.case,
126                self.handlers,
127                self.last_resort,
128            ).run(result)
129        finally:
130            # Clean up the patching for idempotency or something.
131            patcher.restore()
132
133
134def with_logging(
135        test_id: str,
136        test_method: Callable,
137):
138    """
139    Decorate a test method with additional log-related behaviors.
140
141    1. The test method will run in a distinct Eliot action.
142    2. Typed log messages will be validated.
143    3. Logged tracebacks will be added as errors.
144
145    :param test_id: The full identifier of the test being decorated.
146    :param test_method: The method itself.
147    """
148    @wraps(test_method)
149    def run_with_logging(*args, **kwargs):
150        validating_logger = MemoryLogger(encoder=AnyBytesJSONEncoder)
151        original = swap_logger(None)
152        try:
153            swap_logger(_TwoLoggers(original, validating_logger))
154            with RUN_TEST(name=test_id):
155                try:
156                    return test_method(*args, **kwargs)
157                finally:
158                    check_for_errors(validating_logger)
159        finally:
160            swap_logger(original)
161    return run_with_logging
162
163
164@implementer(ILogger)
165class _TwoLoggers(object):
166    """
167    Log to two loggers.
168
169    A single logger can have multiple destinations so this isn't typically a
170    useful thing to do.  However, MemoryLogger has inline validation instead
171    of destinations.  That means this *is* useful to simultaneously write to
172    the normal places and validate all written log messages.
173    """
174    def __init__(self, a, b):
175        """
176        :param ILogger a: One logger
177        :param ILogger b: Another logger
178        """
179        self._a = a # type: ILogger
180        self._b = b # type: ILogger
181
182    def write(self, dictionary, serializer=None):
183        self._a.write(dictionary, serializer)
184        self._b.write(dictionary, serializer)
Note: See TracBrowser for help on using the repository browser.