~launchpad-pqm/launchpad/devel

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()