3
# Copyright 2009 Canonical Ltd. This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
7
check-sampledata.py - Perform various checks on Sample Data
9
= Launchpad Sample Data Consistency Checks =
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.
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
18
Finally, it can also be used to report about sample data lacking in breadth.
27
from optparse import OptionParser
29
from textwrap import dedent
31
from psycopg2 import ProgrammingError
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
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
45
def get_class_name(cls):
46
"""Return the class name without its package prefix."""
47
return cls.__name__.split('.')[-1]
51
"""Convert an exception to a proper error.
53
It make sure that the exception type is in the message and takes care
54
of possible unicode conversion error.
57
return "%s: %s" % (get_class_name(error.__class__), str(error))
58
except UnicodeEncodeError:
59
return "UnicodeEncodeError in str(%s)" % error.__class__.__name__
62
class SampleDataVerification:
63
"""Runs various checks on sample data and report about them."""
65
def __init__(self, dbname="launchpad_ftest_template", dbuser="launchpad",
66
table_filter=None, min_rows=10, only_summary=False):
67
"""Initialize the verification object.
69
:param dbname: The database which contains the sample data to check.
70
:param dbuser: The user to connect as.
72
self.txn = initZopeless(dbname=dbname, dbuser=dbuser)
73
execute_zcml_for_scripts()
74
self.classes_with_error = {}
76
self.table_filter = table_filter
77
self.min_rows = min_rows
78
self.only_summary = only_summary
80
def findSQLBaseClasses(self):
81
"""Return an iterator over the classes in canonical.launchpad.database
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):
89
cls = getattr(canonical.launchpad.database, class_name)
90
if inspect.isclass(cls) and issubclass(cls, SQLBase):
93
def fetchTableRowsCount(self):
94
"""Fetch the number of rows of each tables.
96
The count are stored in the table_rows_count attribute.
98
self.table_rows_count = {}
99
for cls in self.findSQLBaseClasses():
100
class_name = get_class_name(cls)
102
self.table_rows_count[class_name] = cls.select().count()
103
except ProgrammingError, error:
104
self.classes_with_error[class_name] = str(error)
105
# Transaction is borked, start another one.
108
def checkSampleDataInterfaces(self):
109
"""Check that all sample data objects complies with the interfaces it
112
self.validation_errors = {}
113
self.broken_instances= {}
114
for cls in self.findSQLBaseClasses():
115
class_name = get_class_name(cls)
116
if class_name in self.classes_with_error:
119
for object in cls.select():
120
self.checkObjectInterfaces(object)
121
self.validateObjectSchemas(object)
122
except ProgrammingError, error:
123
self.classes_with_error[get_class_name(cls)] = str(error)
124
# Transaction is borked, start another one.
127
def checkObjectInterfaces(self, object):
128
"""Check that object provides every attributes in its declared interfaces.
130
Collect errors in broken_instances dictionary attribute.
132
for interface in providedBy(object):
133
interface_name = get_class_name(interface)
135
result = verifyObject(interface, object)
136
except BrokenImplementation, error:
137
self.setInterfaceError(
138
interface, object, "missing attribute %s" % error.name)
139
except BrokenMethodImplementation, error:
140
self.setInterfaceError(
142
"invalid method %s: %s" % (error.method, error.mess))
144
def setInterfaceError(self, interface, object, error_msg):
145
"""Store an error about an interface in the broken_instances dictionary
147
The errors data structure looks like:
151
class_name: [instance_id...]}}}
153
interface_errors = self.broken_instances.setdefault(
154
get_class_name(interface), {})
155
classes_with_error = interface_errors.setdefault(error_msg, {})
156
object_ids_with_error = classes_with_error.setdefault(
157
get_class_name(object.__class__), [])
158
object_ids_with_error.append(object.id)
160
def validateObjectSchemas(self, object):
161
"""Check that object validates with the schemas it says it provides.
163
Collect errors in validation_errors. Data structure format is
165
[[class_name, object_id,
166
[(field, error), ...]],
169
for schema in providedBy(object):
171
for name in schema.names(all=True):
172
description = schema[name]
173
if not IField.providedBy(description):
176
value = getattr(object, name)
177
except AttributeError:
178
# This is an already reported verifyObject failures.
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.
187
except ProgrammingError, error:
188
field_errors.append((name, error_msg(error)))
189
# We need to restart the transaction after these errors.
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)))
197
schema_errors= self.validation_errors.setdefault(
198
get_class_name(schema), [])
199
schema_errors.append([
200
get_class_name(object.__class__), object.id,
203
def getShortTables(self):
204
"""Return a list of tables which have less rows than self.min_rows.
206
:return: [(table, rows_count)...]
210
for table, rows_count in self.table_rows_count.items()
211
if rows_count < self.min_rows]
213
def reportShortTables(self):
214
"""Report about tables with less than self.min_rows."""
215
short_tables = self.getShortTables()
217
print """All tables have more than %d rows!!!""" % self.min_rows
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)
227
def reportErrors(self):
228
"""Report about classes with database error.
230
This will usually be classes without a database table.
232
if not self.classes_with_error:
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)
240
def reportInterfaceErrors(self):
241
"""Report objects failing the verifyObject and schema validation."""
242
if not self.broken_instances:
243
print "All sample data comply with its provided interfaces!!!"
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()):
257
class_name, ", ".join([
258
str(id) for id in sorted(object_ids)]))
260
def reportValidationErrors(self):
261
"""Report object that fails their validation."""
262
if not self.validation_errors:
263
print "All sample data pass validation!!!"
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))
272
for class_name, object_id, errors in sorted(instances):
273
print " <%s %s> (%d errors):" % (
274
class_name, object_id, len(errors))
275
for field, error in sorted(errors):
276
print " %s: %s" % (field, error)
278
def reportSummary(self):
279
"""Only report the name of the classes with errors."""
281
short_tables = dict(self.getShortTables())
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
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
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())
303
%d Classes with errors:
304
-----------------------""" % len(classes_with_errors))
305
for class_name in sorted(classes_with_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:
311
'%d verify errors' % verify_errors_count[class_name])
312
if class_name in validation_errors_count:
314
'%d validation errors' %
315
validation_errors_count[class_name])
316
print "%s: %s" % (class_name, ", ".join(errors))
319
"""Check and report on sample data."""
320
self.fetchTableRowsCount()
321
self.checkSampleDataInterfaces()
323
Verified %d content classes.
324
============================
325
""" % len(self.table_rows_count))
326
if self.only_summary:
329
self.reportShortTables()
331
self.reportInterfaceErrors()
333
self.reportValidationErrors()
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",
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.")
353
parser.add_option('-s', '--summary',
354
action='store_true', dest="summary", default=False,
356
"Only report the name of the classes with "
357
"validation errors."))
358
options, arguments = parser.parse_args()
359
SampleDataVerification(
360
dbname=options.database,
362
table_filter=options.table_filter,
363
min_rows=options.min_rows,
364
only_summary=options.summary).run()