10637.3.1
by Guilherme Salgado
Use the default python version instead of a hard-coded version |
1 |
#! /usr/bin/python -S
|
8687.15.4
by Karl Fogel
Add the copyright header block to more files; tweak format in a few files. |
2 |
#
|
3 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
|
4 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
5 |
|
6 |
"""
|
|
7 |
check-sampledata.py - Perform various checks on Sample Data
|
|
8 |
||
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
9 |
= Launchpad Sample Data Consistency Checks =
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
10 |
|
11 |
XXX flacoste 2007/03/08 Once all problems exposed by this script are solved,
|
|
12 |
it should be integrated to our automated test suite.
|
|
13 |
||
14 |
This script verify that all objects in sample data provides the interfaces
|
|
15 |
they are supposed to. It also makes sure that the object pass its schema
|
|
16 |
validation.
|
|
17 |
||
18 |
Finally, it can also be used to report about sample data lacking in breadth.
|
|
19 |
||
20 |
"""
|
|
21 |
||
22 |
__metatype__ = type |
|
23 |
||
24 |
import _pythonpath |
|
25 |
||
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
26 |
import inspect |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
27 |
from optparse import OptionParser |
28 |
import re |
|
29 |
from textwrap import dedent |
|
30 |
||
5821.2.85
by James Henstridge
Add "make check_launchpad_storm_on_merge" target that runs the tests |
31 |
from psycopg2 import ProgrammingError |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
32 |
|
33 |
from zope.interface import providedBy |
|
34 |
from zope.interface.exceptions import ( |
|
35 |
BrokenImplementation, BrokenMethodImplementation) |
|
36 |
from zope.interface.verify import verifyObject |
|
37 |
from zope.schema.interfaces import IField, ValidationError |
|
38 |
||
39 |
from canonical.database.sqlbase import SQLBase |
|
40 |
import canonical.launchpad.database |
|
41 |
from canonical.lp import initZopeless |
|
42 |
from canonical.launchpad.scripts import execute_zcml_for_scripts |
|
43 |
||
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
44 |
|
45 |
def get_class_name(cls): |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
46 |
"""Return the class name without its package prefix."""
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
47 |
return cls.__name__.split('.')[-1] |
48 |
||
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
49 |
|
50 |
def error_msg(error): |
|
51 |
"""Convert an exception to a proper error.
|
|
52 |
||
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
53 |
It make sure that the exception type is in the message and takes care
|
54 |
of possible unicode conversion error.
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
55 |
"""
|
56 |
try: |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
57 |
return "%s: %s" % (get_class_name(error.__class__), str(error)) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
58 |
except UnicodeEncodeError: |
59 |
return "UnicodeEncodeError in str(%s)" % error.__class__.__name__ |
|
60 |
||
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
61 |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
62 |
class SampleDataVerification: |
63 |
"""Runs various checks on sample data and report about them."""
|
|
64 |
||
65 |
def __init__(self, dbname="launchpad_ftest_template", dbuser="launchpad", |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
66 |
table_filter=None, min_rows=10, only_summary=False): |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
67 |
"""Initialize the verification object.
|
68 |
||
69 |
:param dbname: The database which contains the sample data to check.
|
|
70 |
:param dbuser: The user to connect as.
|
|
71 |
"""
|
|
72 |
self.txn = initZopeless(dbname=dbname, dbuser=dbuser) |
|
73 |
execute_zcml_for_scripts() |
|
74 |
self.classes_with_error = {} |
|
75 |
self.class_rows = {} |
|
76 |
self.table_filter = table_filter |
|
77 |
self.min_rows = min_rows |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
78 |
self.only_summary = only_summary |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
79 |
|
80 |
def findSQLBaseClasses(self): |
|
81 |
"""Return an iterator over the classes in canonical.launchpad.database
|
|
82 |
that extends SQLBase.
|
|
83 |
"""
|
|
84 |
if self.table_filter: |
|
85 |
include_only_re = re.compile(self.table_filter) |
|
86 |
for class_name in dir(canonical.launchpad.database): |
|
87 |
if self.table_filter and not include_only_re.search(class_name): |
|
88 |
continue
|
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
89 |
cls = getattr(canonical.launchpad.database, class_name) |
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
90 |
if inspect.isclass(cls) and issubclass(cls, SQLBase): |
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
91 |
yield cls |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
92 |
|
93 |
def fetchTableRowsCount(self): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
94 |
"""Fetch the number of rows of each tables.
|
95 |
||
96 |
The count are stored in the table_rows_count attribute.
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
97 |
"""
|
98 |
self.table_rows_count = {} |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
99 |
for cls in self.findSQLBaseClasses(): |
100 |
class_name = get_class_name(cls) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
101 |
try: |
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
102 |
self.table_rows_count[class_name] = cls.select().count() |
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
103 |
except ProgrammingError, error: |
104 |
self.classes_with_error[class_name] = str(error) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
105 |
# Transaction is borked, start another one.
|
106 |
self.txn.begin() |
|
107 |
||
108 |
def checkSampleDataInterfaces(self): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
109 |
"""Check that all sample data objects complies with the interfaces it
|
110 |
declares.
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
111 |
"""
|
112 |
self.validation_errors = {} |
|
113 |
self.broken_instances= {} |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
114 |
for cls in self.findSQLBaseClasses(): |
115 |
class_name = get_class_name(cls) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
116 |
if class_name in self.classes_with_error: |
117 |
continue
|
|
118 |
try: |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
119 |
for object in cls.select(): |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
120 |
self.checkObjectInterfaces(object) |
121 |
self.validateObjectSchemas(object) |
|
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
122 |
except ProgrammingError, error: |
123 |
self.classes_with_error[get_class_name(cls)] = str(error) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
124 |
# Transaction is borked, start another one.
|
125 |
self.txn.begin() |
|
126 |
||
127 |
def checkObjectInterfaces(self, object): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
128 |
"""Check that object provides every attributes in its declared interfaces.
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
129 |
|
130 |
Collect errors in broken_instances dictionary attribute.
|
|
131 |
"""
|
|
132 |
for interface in providedBy(object): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
133 |
interface_name = get_class_name(interface) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
134 |
try: |
135 |
result = verifyObject(interface, object) |
|
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
136 |
except BrokenImplementation, error: |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
137 |
self.setInterfaceError( |
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
138 |
interface, object, "missing attribute %s" % error.name) |
139 |
except BrokenMethodImplementation, error: |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
140 |
self.setInterfaceError( |
141 |
interface, object, |
|
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
142 |
"invalid method %s: %s" % (error.method, error.mess)) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
143 |
|
144 |
def setInterfaceError(self, interface, object, error_msg): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
145 |
"""Store an error about an interface in the broken_instances dictionary
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
146 |
|
147 |
The errors data structure looks like:
|
|
148 |
||
149 |
{interface: {
|
|
150 |
error_msg: {
|
|
151 |
class_name: [instance_id...]}}}
|
|
152 |
"""
|
|
153 |
interface_errors = self.broken_instances.setdefault( |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
154 |
get_class_name(interface), {}) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
155 |
classes_with_error = interface_errors.setdefault(error_msg, {}) |
156 |
object_ids_with_error = classes_with_error.setdefault( |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
157 |
get_class_name(object.__class__), []) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
158 |
object_ids_with_error.append(object.id) |
159 |
||
160 |
def validateObjectSchemas(self, object): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
161 |
"""Check that object validates with the schemas it says it provides.
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
162 |
|
163 |
Collect errors in validation_errors. Data structure format is
|
|
164 |
{schema:
|
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
165 |
[[class_name, object_id,
|
166 |
[(field, error), ...]],
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
167 |
...]}
|
168 |
"""
|
|
169 |
for schema in providedBy(object): |
|
170 |
field_errors = [] |
|
171 |
for name in schema.names(all=True): |
|
172 |
description = schema[name] |
|
173 |
if not IField.providedBy(description): |
|
174 |
continue
|
|
175 |
try: |
|
176 |
value = getattr(object, name) |
|
177 |
except AttributeError: |
|
178 |
# This is an already reported verifyObject failures.
|
|
179 |
continue
|
|
180 |
try: |
|
181 |
description.validate(value) |
|
182 |
except ValidationError, error: |
|
183 |
field_errors.append((name, error_msg(error))) |
|
184 |
except (KeyboardInterrupt, SystemExit): |
|
185 |
# We should never catch KeyboardInterrupt or SystemExit.
|
|
186 |
raise
|
|
187 |
except ProgrammingError, error: |
|
188 |
field_errors.append((name, error_msg(error))) |
|
189 |
# We need to restart the transaction after these errors.
|
|
190 |
self.txn.begin() |
|
191 |
except Exception, error: |
|
192 |
# Exception usually indicates a deeper problem with
|
|
193 |
# the interface declaration or the validation code, than
|
|
194 |
# the expected ValidationError.
|
|
195 |
field_errors.append((name, error_msg(error))) |
|
196 |
if field_errors: |
|
197 |
schema_errors= self.validation_errors.setdefault( |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
198 |
get_class_name(schema), []) |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
199 |
schema_errors.append([ |
3894.2.7
by Francis J. Lacoste
Add summary option. |
200 |
get_class_name(object.__class__), object.id, |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
201 |
field_errors]) |
202 |
||
3894.2.7
by Francis J. Lacoste
Add summary option. |
203 |
def getShortTables(self): |
204 |
"""Return a list of tables which have less rows than self.min_rows.
|
|
205 |
||
206 |
:return: [(table, rows_count)...]
|
|
207 |
"""
|
|
208 |
return [ |
|
209 |
(table, rows_count) |
|
210 |
for table, rows_count in self.table_rows_count.items() |
|
211 |
if rows_count < self.min_rows] |
|
212 |
||
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
213 |
def reportShortTables(self): |
214 |
"""Report about tables with less than self.min_rows."""
|
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
215 |
short_tables = self.getShortTables() |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
216 |
if not short_tables: |
217 |
print """All tables have more than %d rows!!!""" % self.min_rows |
|
218 |
return
|
|
219 |
||
220 |
print dedent("""\ |
|
221 |
%d Tables with less than %d rows |
|
222 |
--------------------------------""" % ( |
|
223 |
len(short_tables), self.min_rows)) |
|
224 |
for table, rows_count in sorted(short_tables): |
|
225 |
print "%-20s: %2d" % (table, rows_count) |
|
226 |
||
227 |
def reportErrors(self): |
|
3894.2.4
by Francis J. Lacoste
Rename some variables and PEP-257 fixes. |
228 |
"""Report about classes with database error.
|
229 |
||
230 |
This will usually be classes without a database table.
|
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
231 |
"""
|
232 |
if not self.classes_with_error: |
|
233 |
return
|
|
234 |
print dedent("""\ |
|
235 |
Classes with database errors
|
|
236 |
----------------------------""") |
|
237 |
for class_name, error_msg in sorted(self.classes_with_error.items()): |
|
238 |
print "%-20s %s" % (class_name, error_msg) |
|
239 |
||
240 |
def reportInterfaceErrors(self): |
|
241 |
"""Report objects failing the verifyObject and schema validation."""
|
|
242 |
if not self.broken_instances: |
|
3894.2.6
by Francis J. Lacoste
Use inspect. Remove use of single-letter variable. |
243 |
print "All sample data comply with its provided interfaces!!!" |
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
244 |
return
|
245 |
print dedent("""\ |
|
246 |
%d Interfaces with broken instances |
|
247 |
-----------------------------------""" % len( |
|
248 |
self.broken_instances)) |
|
249 |
for interface, errors in sorted( |
|
250 |
self.broken_instances.items()): |
|
251 |
print "%-20s:" % interface |
|
252 |
for error_msg, classes_with_error in sorted(errors.items()): |
|
253 |
print " %s:" % error_msg |
|
254 |
for class_name, object_ids in sorted( |
|
255 |
classes_with_error.items()): |
|
256 |
print " %s: %s" % ( |
|
257 |
class_name, ", ".join([ |
|
258 |
str(id) for id in sorted(object_ids)])) |
|
259 |
||
260 |
def reportValidationErrors(self): |
|
261 |
"""Report object that fails their validation."""
|
|
262 |
if not self.validation_errors: |
|
263 |
print "All sample data pass validation!!!" |
|
264 |
return
|
|
265 |
||
266 |
print dedent("""\ |
|
267 |
%d Schemas with instances failing validation |
|
268 |
--------------------------------------------""" % len( |
|
269 |
self.validation_errors)) |
|
270 |
for schema, instances in sorted(self.validation_errors.items()): |
|
271 |
print "%-20s (%d objects with errors):" % (schema, len(instances)) |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
272 |
for class_name, object_id, errors in sorted(instances): |
273 |
print " <%s %s> (%d errors):" % ( |
|
274 |
class_name, object_id, len(errors)) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
275 |
for field, error in sorted(errors): |
276 |
print " %s: %s" % (field, error) |
|
277 |
||
3894.2.7
by Francis J. Lacoste
Add summary option. |
278 |
def reportSummary(self): |
279 |
"""Only report the name of the classes with errors."""
|
|
280 |
||
281 |
short_tables = dict(self.getShortTables()) |
|
282 |
||
283 |
# Compute number of implementation error by classes.
|
|
284 |
verify_errors_count = {} |
|
285 |
for interface_errors in self.broken_instances.values(): |
|
286 |
for broken_classes in interface_errors.values(): |
|
287 |
for class_name in broken_classes.keys(): |
|
288 |
verify_errors_count.setdefault(class_name, 0) |
|
289 |
verify_errors_count[class_name] += 1 |
|
290 |
||
291 |
# Compute number of instances with validation error.
|
|
292 |
validation_errors_count = {} |
|
293 |
for instances in self.validation_errors.values(): |
|
294 |
for class_name, object_id, errors in instances: |
|
295 |
validation_errors_count.setdefault(class_name, 0) |
|
296 |
validation_errors_count[class_name] += 1 |
|
297 |
||
298 |
classes_with_errors = set(short_tables.keys()) |
|
299 |
classes_with_errors.update(verify_errors_count.keys()) |
|
300 |
classes_with_errors.update(validation_errors_count.keys()) |
|
301 |
||
302 |
print dedent("""\ |
|
303 |
%d Classes with errors: |
|
304 |
-----------------------""" % len(classes_with_errors)) |
|
305 |
for class_name in sorted(classes_with_errors): |
|
306 |
errors = [] |
|
307 |
if class_name in short_tables: |
|
308 |
errors.append('%d rows' % short_tables[class_name]) |
|
309 |
if class_name in verify_errors_count: |
|
310 |
errors.append( |
|
311 |
'%d verify errors' % verify_errors_count[class_name]) |
|
312 |
if class_name in validation_errors_count: |
|
313 |
errors.append( |
|
314 |
'%d validation errors' % |
|
315 |
validation_errors_count[class_name]) |
|
316 |
print "%s: %s" % (class_name, ", ".join(errors)) |
|
317 |
||
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
318 |
def run(self): |
319 |
"""Check and report on sample data."""
|
|
320 |
self.fetchTableRowsCount() |
|
321 |
self.checkSampleDataInterfaces() |
|
322 |
print dedent("""\ |
|
323 |
Verified %d content classes. |
|
324 |
============================
|
|
325 |
""" % len(self.table_rows_count)) |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
326 |
if self.only_summary: |
327 |
self.reportSummary() |
|
328 |
else: |
|
329 |
self.reportShortTables() |
|
330 |
print
|
|
331 |
self.reportInterfaceErrors() |
|
332 |
print
|
|
333 |
self.reportValidationErrors() |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
334 |
print
|
335 |
self.reportErrors() |
|
336 |
self.txn.abort() |
|
337 |
||
338 |
||
339 |
if __name__ == '__main__': |
|
340 |
parser = OptionParser() |
|
341 |
parser.add_option('-d', '--database', action="store", type="string", |
|
342 |
default="launchpad_ftest_template", |
|
343 |
help="Database to connect to for testing.") |
|
344 |
parser.add_option('-u', '--user', action="store", type="string", |
|
345 |
default="launchpad", |
|
346 |
help="Username to connect with.") |
|
347 |
parser.add_option('-i', '--table-filter', dest="table_filter", |
|
348 |
action="store", type="string", default=None, |
|
349 |
help="Limit classes to test using a regular expression.") |
|
350 |
parser.add_option('-m', '--min-rows', dest="min_rows", |
|
351 |
action="store", type="int", default=10, |
|
352 |
help="Minimum number of rows a table is expected to have.") |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
353 |
parser.add_option('-s', '--summary', |
354 |
action='store_true', dest="summary", default=False, |
|
355 |
help=( |
|
356 |
"Only report the name of the classes with "
|
|
357 |
"validation errors.")) |
|
3894.2.3
by Francis J. Lacoste
Add check-sampledata script. |
358 |
options, arguments = parser.parse_args() |
359 |
SampleDataVerification( |
|
360 |
dbname=options.database, |
|
361 |
dbuser=options.user, |
|
362 |
table_filter=options.table_filter, |
|
3894.2.7
by Francis J. Lacoste
Add summary option. |
363 |
min_rows=options.min_rows, |
364 |
only_summary=options.summary).run() |