runner 12.9 KB
Newer Older
1
#! /usr/bin/env python2.4
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#
# Copyright (C) 2009  Nexedi SA
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

19
import optparse
20
import unittest
21
import tempfile
22 23
import logging
import time
Grégory Wisniewski's avatar
Grégory Wisniewski committed
24
import sys
25
import os
26

27 28
# list of test modules
# each of them have to import its TestCase classes
29
UNIT_TEST_MODULES = [ 
30 31 32 33 34 35 36
    # generic parts
    'neo.tests.testBootstrap',
    'neo.tests.testConnection',
    'neo.tests.testEvent',
    'neo.tests.testHandler',
    'neo.tests.testNodes',
    'neo.tests.testProtocol',
37
    'neo.tests.testDispatcher',
38
    'neo.tests.testUtil',
39 40 41 42 43 44
    'neo.tests.testPT',
    # master application
    'neo.tests.master.testClientHandler',
    'neo.tests.master.testElectionHandler',
    'neo.tests.master.testMasterApp',
    'neo.tests.master.testMasterPT',
45
    'neo.tests.master.testRecovery',
46
    'neo.tests.master.testStorageHandler',
47
    'neo.tests.master.testVerification',
48
    'neo.tests.master.testTransactions',
49 50 51 52 53 54 55 56
    # storage application
    'neo.tests.storage.testClientHandler',
    'neo.tests.storage.testInitializationHandler',
    'neo.tests.storage.testMasterHandler',
    'neo.tests.storage.testStorageApp',
    'neo.tests.storage.testStorageHandler',
    'neo.tests.storage.testStorageMySQLdb',
    'neo.tests.storage.testVerificationHandler',
57
    'neo.tests.storage.testIdentificationHandler',
58
    'neo.tests.storage.testTransactions',
59 60
    'neo.tests.storage.testReplicationHandler',
    'neo.tests.storage.testReplicator',
61 62
    # client application
    'neo.tests.client.testClientApp',
63 64
    'neo.tests.client.testMasterHandler',
    'neo.tests.client.testStorageHandler',
65
    'neo.tests.client.testConnectionPool',
66 67
]

68
FUNC_TEST_MODULES = [
69
    'neo.tests.functional.testMaster',
70
    'neo.tests.functional.testClient',
71
    'neo.tests.functional.testCluster',
72
    'neo.tests.functional.testStorage',
73 74
]

75
ZODB_TEST_MODULES = [
76 77 78 79 80
    ('neo.tests.zodb.testBasic', 'check'),
    ('neo.tests.zodb.testConflict', 'check'),
    ('neo.tests.zodb.testHistory', 'check'),
    ('neo.tests.zodb.testIterator', 'check'),
    ('neo.tests.zodb.testMT', 'check'),
81
    ('neo.tests.zodb.testPack', 'check'),
82 83 84
    ('neo.tests.zodb.testPersistent', 'check'),
    ('neo.tests.zodb.testReadOnly', 'check'),
    ('neo.tests.zodb.testRevision', 'check'),
85
    #('neo.tests.zodb.testRecovery', 'check'),
86 87 88
    ('neo.tests.zodb.testSynchronization', 'check'),
    # ('neo.tests.zodb.testVersion', 'check'),
    ('neo.tests.zodb.testUndo', 'check'),
89 90 91
    ('neo.tests.zodb.testZODB', 'check'),
]

92
# configuration 
93 94 95
CONSOLE_LOG = False
ATTACH_LOG = False # for ZODB test, only the client side is logged
LOG_FILE = 'neo.log' 
96 97 98 99 100 101 102 103 104

# override logging configuration to send all messages to a file
logger = logging.getLogger()
logger.setLevel(logging.INFO)
handler = logging.FileHandler(LOG_FILE, 'w+')
format='[%(module)12s:%(levelname)s:%(lineno)3d] %(message)s'
formatter = logging.Formatter(format)
handler.setFormatter(formatter)
logger.addHandler(handler)
105
# enabled console logging if desired
106 107 108 109 110
if CONSOLE_LOG:
    handler = logging.StreamHandler()
    handler.setFormatter(formatter)
    logger.addHandler(handler)

111
class NeoTestRunner(unittest.TestResult):
112 113
    """ Custom result class to build report with statistics per module """

114 115 116
    def __init__(self):
        unittest.TestResult.__init__(self)
        self.modulesStats = {}
117
        self.failedImports = {}
118
        self.lastStart = None
119 120 121
        self.temp_directory = tempfile.mkdtemp(prefix='neo_')
        os.environ['TEMP'] = self.temp_directory
        print "Base directory : %s" % (self.temp_directory, )
122

123
    def run(self, name, modules):
124
        print '\n', name
125 126 127
        suite = unittest.TestSuite()
        loader = unittest.defaultTestLoader
        for test_module in modules:
128 129 130 131 132 133
            # load prefix if supplied
            if isinstance(test_module, tuple):
                test_module, prefix = test_module
                loader.testMethodPrefix = prefix
            else:
                loader.testMethodPrefix = 'test'
134 135 136
            try:
                test_module = __import__(test_module, globals(), locals(), ['*'])
            except ImportError, err:
137
                self.failedImports[test_module] = err
138 139 140 141 142
                print "Import of %s failed : %s" % (test_module, err)
                continue
            suite.addTests(loader.loadTestsFromModule(test_module))
        suite.run(self)

143
    class ModuleStats(object):
144
        run = 0
145 146 147 148 149 150
        errors = 0
        success = 0
        failures = 0
        time = 0.0

    def _getModuleStats(self, test):
151 152
        module = test.__class__.__module__
        module = tuple(module.split('.'))
153 154 155 156 157 158 159 160 161 162
        try:
            return self.modulesStats[module] 
        except KeyError:
            self.modulesStats[module] = self.ModuleStats()
            return self.modulesStats[module]

    def _updateTimer(self, stats):
        stats.time += time.time() - self.lastStart

    def startTest(self, test):
163 164 165
        module = test.__class__.__name__
        method = test.id()
        sys.stdout.write(' * %s ' % (method, ))
166
        sys.stdout.flush()
167 168
        unittest.TestResult.startTest(self, test)
        logging.info(" * TEST %s" % test)
169 170
        stats = self._getModuleStats(test)
        stats.run += 1
171 172 173
        self.lastStart = time.time()

    def addSuccess(self, test):
174
        print "OK"
175 176 177 178 179 180
        unittest.TestResult.addSuccess(self, test)
        stats = self._getModuleStats(test)
        stats.success += 1
        self._updateTimer(stats)

    def addError(self, test, err):
181
        print "ERROR"
182 183 184 185 186 187
        unittest.TestResult.addError(self, test, err)
        stats = self._getModuleStats(test)
        stats.errors += 1
        self._updateTimer(stats)

    def addFailure(self, test, err):
188
        print "FAIL"
189 190 191 192 193
        unittest.TestResult.addFailure(self, test, err)
        stats = self._getModuleStats(test)
        stats.failures += 1
        self._updateTimer(stats)

194 195 196
    def _buildSystemInfo(self):
        import platform
        import datetime
197
        success = self.testsRun - len(self.errors) - len(self.failures)
198 199 200 201 202
        s = """
    Date        : %s
    Node        : %s
    Processor   : %s (%s)
    System      : %s (%s)
203
    Directory   : %s
204
    Status      : %7.3f%%
205 206 207 208 209 210 211
        """ % (
            datetime.date.today().isoformat(),
            platform.node(),
            platform.processor(),
            platform.architecture()[0],
            platform.system(),
            platform.release(),
212
            self.temp_directory,
213
            success * 100.0 / self.testsRun,
214 215 216
        )
        return s

217
    def _buildSummary(self):
218 219 220 221 222 223
        # visual 
        header       = "%25s |   run   | success |  errors |  fails  |   time   \n" % 'Test Module'
        separator    = "%25s-+---------+---------+---------+---------+----------\n" % ('-' * 25)
        format       = "%25s |   %3s   |   %3s   |   %3s   |   %3s   | %6.2fs   \n"
        group_f      = "%25s |         |         |         |         |          \n" 
        # header
224
        s = ' ' * 30 + ' NEO TESTS REPORT'
225
        s += '\n\n'
226
        s += self._buildSystemInfo()
227 228 229 230 231 232 233 234 235 236 237 238
        s += '\n' + header + separator
        group = None
        t_success = 0
        # for each test case
        for k, v in sorted(self.modulesStats.items()):
            # display group below its content
            _group = '.'.join(k[:-1])
            if group is None:
                group = _group
            if _group != group:
                s += separator + group_f % group + separator
                group = _group
239 240 241 242 243 244 245
            # test case stats
            t_success += v.success
            run, success = v.run or '.', v.success or '.'
            errors, failures = v.errors or '.', v.failures or '.'
            name = k[-1].lstrip('test')
            args = (name, run, success, errors, failures, v.time)
            s += format % args
246 247 248
        # the last group
        s += separator  + group_f % group + separator
        # the final summary
249
        errors, failures = len(self.errors) or '.', len(self.failures) or '.'
250 251
        args = ("Summary", self.testsRun, t_success, errors, failures, self.time)
        s += format % args + separator + '\n'
252 253 254 255
        return s

    def _buildErrors(self):
        s = '\n'
256
        test_formatter = lambda t: t.id()
257 258 259
        if len(self.errors):
            s += '\nERRORS:\n'
            for test, trace in self.errors:
260
                s += "%s\n" % test_formatter(test)
261
                s += "-------------------------------------------------------------\n"
262
                s += trace
263
                s += "-------------------------------------------------------------\n"
264 265 266 267
                s += '\n'
        if len(self.failures):
            s += '\nFAILURES:\n'
            for test, trace in self.failures:
268
                s += "%s\n" % test_formatter(test)
269
                s += "-------------------------------------------------------------\n"
270
                s += trace
271
                s += "-------------------------------------------------------------\n"
272 273 274
                s += '\n'
        return s

275 276 277 278 279 280 281 282 283
    def _buildWarnings(self):
        s = '\n'
        if self.failedImports:
            s += 'Failed imports :\n'
            for module, err in self.failedImports.items():
                s += '%s:\n%s' % (module, err)
        s += '\n'
        return s

284 285 286 287
    def build(self):
        self.time = sum([s.time for s in self.modulesStats.values()])
        args = (self.testsRun, len(self.errors), len(self.failures))
        self.subject = "Neo : %s Tests, %s Errors, %s Failures" % args
288 289 290
        self._summary = self._buildSummary()
        self._errors = self._buildErrors()
        self._warnings = self._buildWarnings()
291

Grégory Wisniewski's avatar
Grégory Wisniewski committed
292
    def sendReport(self, smtp_server, sender, recipients):
293 294 295 296 297 298 299 300 301
        """ Send a mail with the report summary """

        import smtplib
        from email.MIMEMultipart import MIMEMultipart
        from email.MIMEText import MIMEText

        # build the email
        msg = MIMEMultipart()
        msg['Subject'] = self.subject
Grégory Wisniewski's avatar
Grégory Wisniewski committed
302 303
        msg['From']    = sender
        msg['To']      = ', '.join(recipients)
304 305 306 307 308 309 310 311 312
        #msg.preamble = self.subject
        msg.epilogue = ''

        # Add custom headers for client side filtering
        msg['X-ERP5-Tests'] = 'NEO'
        if self.wasSuccessful():
          msg['X-ERP5-Tests-Status'] = 'OK'

        # write the body
313
        body = MIMEText(self._summary + self._warnings + self._errors)
314 315 316 317 318 319 320 321
        msg.attach(body)

        # attach the log file
        if ATTACH_LOG:
            log = MIMEText(file(LOG_FILE, 'r').read())
            log.add_header('Content-Disposition', 'attachment', filename=LOG_FILE)
            msg.attach(log)

Grégory Wisniewski's avatar
Grégory Wisniewski committed
322
        # Send the email via a smtp server
323
        s = smtplib.SMTP()
Grégory Wisniewski's avatar
Grégory Wisniewski committed
324
        s.connect(*mail_server)
325
        mail = msg.as_string()
Grégory Wisniewski's avatar
Grégory Wisniewski committed
326
        for recipient in recipients:
327
            try:
Grégory Wisniewski's avatar
Grégory Wisniewski committed
328
                s.sendmail(sender, recipient, mail)
329 330
            except smtplib.SMTPRecipientsRefused, e:
                print "Mail for %s fails : %s" % (recipient, e)
331
        s.close()
332 333

if __name__ == "__main__":
334

335 336 337 338
    # handle command line options
    parser = optparse.OptionParser()
    parser.add_option('-f', '--functional', action='store_true')
    parser.add_option('-u', '--unit', action='store_true')
339
    parser.add_option('-z', '--zodb', action='store_true')
Grégory Wisniewski's avatar
Grégory Wisniewski committed
340 341 342
    parser.add_option('', '--recipient', action='append')
    parser.add_option('', '--sender')
    parser.add_option('', '--server')
343 344
    (options, args) = parser.parse_args()

Grégory Wisniewski's avatar
Grégory Wisniewski committed
345 346 347
    # check arguments
    if bool(options.sender) ^ bool(options.recipient):
        sys.exit('Need a sender and recipients to mail report')
348 349
    if not (options.unit or options.functional or options.zodb or args):
        sys.exit('Nothing to run, please give one of -f, -u, -z')
Grégory Wisniewski's avatar
Grégory Wisniewski committed
350 351
    mail_server = options.server or '127.0.0.1:25'
    mail_server = mail_server.split(':')
352

Grégory Wisniewski's avatar
Grégory Wisniewski committed
353
    # run requested tests
354
    runner = NeoTestRunner()
355 356 357 358 359 360 361 362 363
    try:
        if options.unit:
            runner.run('Unit tests', UNIT_TEST_MODULES)
        if options.functional:
            runner.run('Functional tests', FUNC_TEST_MODULES)
        if options.zodb:
            runner.run('ZODB tests', ZODB_TEST_MODULES)
    except KeyboardInterrupt:
        options.sender = False
Grégory Wisniewski's avatar
Grégory Wisniewski committed
364 365

    # build report
366
    runner.build()
367 368 369
    print runner._errors
    print runner._warnings
    print runner._summary
Grégory Wisniewski's avatar
Grégory Wisniewski committed
370

371
    # send a mail
Grégory Wisniewski's avatar
Grégory Wisniewski committed
372 373
    if options.sender:
        runner.sendReport(mail_server, options.sender, options.recipient)
374 375 376
    if not runner.wasSuccessful():
        sys.exit(1)
    sys.exit(0)