1 | """ |
---|
2 | Tools aimed at the interaction between tests and Eliot. |
---|
3 | |
---|
4 | Ported to Python 3. |
---|
5 | """ |
---|
6 | |
---|
7 | from six import ensure_text |
---|
8 | |
---|
9 | __all__ = [ |
---|
10 | "RUN_TEST", |
---|
11 | "EliotLoggedRunTest", |
---|
12 | ] |
---|
13 | |
---|
14 | from typing import Callable |
---|
15 | from functools import ( |
---|
16 | partial, |
---|
17 | wraps, |
---|
18 | ) |
---|
19 | |
---|
20 | import attr |
---|
21 | |
---|
22 | from zope.interface import ( |
---|
23 | implementer, |
---|
24 | ) |
---|
25 | |
---|
26 | from eliot import ( |
---|
27 | ActionType, |
---|
28 | Field, |
---|
29 | ILogger, |
---|
30 | ) |
---|
31 | from eliot.testing import ( |
---|
32 | MemoryLogger, |
---|
33 | swap_logger, |
---|
34 | check_for_errors, |
---|
35 | ) |
---|
36 | |
---|
37 | from twisted.python.monkey import ( |
---|
38 | MonkeyPatcher, |
---|
39 | ) |
---|
40 | |
---|
41 | from ..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 | |
---|
51 | RUN_TEST = ActionType( |
---|
52 | u"run-test", |
---|
53 | [_NAME], |
---|
54 | [], |
---|
55 | u"A test is run.", |
---|
56 | ) |
---|
57 | |
---|
58 | |
---|
59 | @attr.s |
---|
60 | class 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 | |
---|
134 | def 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) |
---|
165 | class _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) |
---|