This repository was archived by the owner on Apr 11, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathselftest.py
More file actions
284 lines (236 loc) · 9.46 KB
/
selftest.py
File metadata and controls
284 lines (236 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""Define the interfaces used by ExternalIntegration self-tests.
"""
from .util.http import IntegrationException
import json
import logging
import traceback
from .util.opds_writer import AtomFeed
from .util.datetime_helpers import utc_now
class SelfTestResult(object):
"""The result of running a single self-test.
HasSelfTest.run_self_tests() returns a list of these
"""
def __init__(self, name):
# Name of the test.
self.name = name
# Set to True when the test runs without raising an exception.
self.success = False
# The exception raised, if any.
self.exception = None
# The return value of the test method, assuming it ran to
# completion.
self.result = None
# Start time of the test.
self.start = utc_now()
# End time of the test.
self.end = None
# Collection associated with the test
self.collection = None
@property
def to_dict(self):
"""Convert this SelfTestResult to a dictionary for use in
JSON serialization.
"""
# Time formatting method
f = AtomFeed._strftime
if self.exception:
exception = { "class": self.exception.__class__.__name__,
"message": str(self.exception),
"debug_message" : self.debug_message }
else:
exception = None
value = dict(
name=self.name, success=self.success,
duration=self.duration,
exception=exception,
)
if self.start:
value['start'] = f(self.start)
if self.end:
value['end'] = f(self.end)
if self.collection:
value['collection'] = self.collection.name
# String results will be displayed in a fixed-width font.
# Lists of strings will be hidden behind an expandable toggle.
# Other return values have no defined method of display.
if isinstance(self.result, str) or isinstance(self.result, list):
value['result'] = self.result
else:
value['result'] = None
return value
def __repr__(self):
if self.exception:
if isinstance(self.exception, IntegrationException):
exception = " exception=%r debug=%r" % (
str(self.exception),
self.debug_message
)
else:
exception = " exception=%r" % self.exception
else:
exception = ""
if self.collection:
collection = " collection=%r" % self.collection.name
else:
collection = ""
return "<SelfTestResult: name=%r%s duration=%.2fsec success=%r%s result=%r>" % (
self.name, collection, self.duration, self.success,
exception, self.result
)
@property
def duration(self):
"""How long the test took to run."""
if not self.start or not self.end:
return 0
return (self.end-self.start).total_seconds()
@property
def debug_message(self):
"""The debug message associated with the Exception, if any."""
if not self.exception:
return None
return getattr(self.exception, 'debug_message', None)
class HasSelfTests(object):
"""An object capable of verifying its own setup by running a
series of self-tests.
"""
# Self-test results are stored in a ConfigurationSetting with this name,
# associated with the appropriate ExternalIntegration.
SELF_TEST_RESULTS_SETTING = 'self_test_results'
@classmethod
def run_self_tests(cls, _db, constructor_method=None, *args, **kwargs):
"""Instantiate this class and call _run_self_tests on it.
:param _db: A database connection. Will be passed into `_run_self_tests`.
This connection may need to be used again
in args, if the constructor needs it.
:param constructor_method: Method to use to instantiate the
class, if different from the default constructor.
:param args: Positional arguments to pass into the constructor.
:param kwargs: Keyword arguments to pass into the constructor.
:return: A 2-tuple (results_dict, results_list) `results_dict`
is a JSON-serializable dictionary describing the results of
the self-test. `results_list` is a list of SelfTestResult
objects.
"""
from .external_search import ExternalSearchIndex
constructor_method = constructor_method or cls
start = utc_now()
result = SelfTestResult("Initial setup.")
instance = None
integration = None
results = []
# Treat the construction of the integration code as its own
# test.
try:
instance = constructor_method(*args, **kwargs)
result.success = True
result.result = instance
except Exception as e:
result.exception = e
result.success = False
finally:
result.end = utc_now()
results.append(result)
if instance:
try:
for result in instance._run_self_tests(_db):
results.append(result)
except Exception as e:
# This should only happen when there's a bug in the
# self-test method itself.
failure = instance.test_failure(
"Uncaught exception in the self-test method itself.", e
)
results.append(failure)
end = utc_now()
# Format the results in a useful way.
value = dict(
start=AtomFeed._strftime(start),
end=AtomFeed._strftime(end),
duration = (end-start).total_seconds(),
results = [x.to_dict for x in results]
)
# Store the formatted results in the database, if we can find
# a place to store them.
if instance and isinstance(instance, ExternalSearchIndex):
integration = instance.search_integration(_db)
for idx, result in enumerate(value.get("results")):
if isinstance(results[idx].result, list):
result["result"] = results[idx].result
elif instance:
integration = instance.external_integration(_db)
if integration:
integration.setting(
cls.SELF_TEST_RESULTS_SETTING
).value = json.dumps(value)
return value, results
@classmethod
def prior_test_results(cls, _db, constructor_method=None, *args, **kwargs):
"""Retrieve the last set of test results from the database.
The arguments here are the same as the arguments to run_self_tests.
"""
constructor_method = constructor_method or cls
integration = None
instance = constructor_method(*args, **kwargs)
from .external_search import ExternalSearchIndex
if isinstance(instance, ExternalSearchIndex):
integration = instance.search_integration(_db)
else:
integration = instance.external_integration(_db)
if integration:
return integration.setting(cls.SELF_TEST_RESULTS_SETTING).json_value or "No results yet"
def external_integration(self, _db):
"""Locate the ExternalIntegration associated with this object.
The status of the self-tests will be stored as a ConfigurationSetting
on this ExternalIntegration.
By default, there is no way to get from an object to its
ExternalIntegration, and self-test status will not be stored.
"""
logger = logging.getLogger("Self-test system")
logger.error(
"No ExternalIntegration was found. Self-test results will not be stored."
)
return None
def _run_self_tests(self, _db):
"""Run a series of self-tests.
:return: A list of SelfTestResult objects.
"""
raise NotImplementedError()
def run_test(self, name, method, *args, **kwargs):
"""Run a test method, record any exception that happens, and keep
track of how long the test takes to run.
:param name: The name of the test to be run.
:param method: A method to call to run the test.
:param args: Positional arguments to `method`.
:param kwargs: Keyword arguments to `method`.
:return: A filled-in SelfTestResult.
"""
result = SelfTestResult(name)
try:
return_value = method(*args, **kwargs)
result.success = True
result.result = return_value
except Exception as e:
result.exception = e
result.success = False
result.result = None
finally:
if not result.end:
result.end = utc_now()
return result
@classmethod
def test_failure(cls, name, message, debug_message=None):
"""Create a SelfTestResult for a known failure.
This is useful when you can't even get the data necessary to
run a test method.
"""
result = SelfTestResult(name)
result.end = result.start
result.success = False
if isinstance(message, Exception):
exception = message
message = str(exception)
if not debug_message:
debug_message = traceback.format_exc()
exception = IntegrationException(message, debug_message)
result.exception = exception
return result