blob: e50204fbab00b3d57f46a58994891d0b85d5278a [file] [log] [blame]
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""unit testing code for webkitpy."""
import logging
import optparse
import os
import StringIO
import sys
import traceback
import unittest
# NOTE: We intentionally do not depend on anything else in webkitpy here to avoid breaking test-webkitpy.
_log = logging.getLogger(__name__)
class Tester(object):
@staticmethod
def clean_packages(dirs):
"""Delete all .pyc files under dirs that have no .py file."""
for dir_to_clean in dirs:
_log.debug("Cleaning orphaned *.pyc files from: %s" % dir_to_clean)
for dir_path, dir_names, file_names in os.walk(dir_to_clean):
for file_name in file_names:
if file_name.endswith(".pyc") and file_name[:-1] not in file_names:
file_path = os.path.join(dir_path, file_name)
_log.info("Deleting orphan *.pyc file: %s" % file_path)
os.remove(file_path)
def __init__(self):
self._verbosity = 1
def parse_args(self, argv):
parser = optparse.OptionParser(usage='usage: %prog [options] [args...]')
parser.add_option('-a', '--all', action='store_true', default=False,
help='run all the tests'),
parser.add_option('-c', '--coverage', action='store_true', default=False,
help='generate code coverage info (requires http://pypi.python.org/pypi/coverage)'),
parser.add_option('-n', '--dryrun', action='store_true', default=False,
help='do not actually run the tests'),
parser.add_option('-q', '--quiet', action='store_true', default=False,
help='run quietly (errors, warnings, and progress only)'),
parser.add_option('-s', '--silent', action='store_true', default=False,
help='run silently (errors and warnings only)'),
parser.add_option('-x', '--xml', action='store_true', default=False,
help='output xUnit-style XML output')
parser.add_option('-v', '--verbose', action='count', default=0,
help='verbose output (specify once for individual test results, twice for debug messages)')
parser.add_option('--skip-integrationtests', action='store_true', default=False,
help='do not run the integration tests')
parser.epilog = ('[args...] is an optional list of modules, test_classes, or individual tests. '
'If no args are given, all the tests will be run.')
self.progName = os.path.basename(argv[0])
return parser.parse_args(argv[1:])
def configure(self, options):
self._options = options
if options.silent:
self._verbosity = 0
self._configure_logging(logging.WARNING)
elif options.quiet:
self._verbosity = 1
self._configure_logging(logging.WARNING)
elif options.verbose == 0:
self._verbosity = 1
self._configure_logging(logging.INFO)
elif options.verbose == 1:
self._verbosity = 2
self._configure_logging(logging.INFO)
elif options.verbose == 2:
self._verbosity = 2
self._configure_logging(logging.DEBUG)
def _configure_logging(self, log_level):
"""Configure the root logger.
Configure the root logger not to log any messages from webkitpy --
except for messages from the autoinstall module. Also set the
logging level as described below.
"""
handler = logging.StreamHandler(sys.stderr)
# We constrain the level on the handler rather than on the root
# logger itself. This is probably better because the handler is
# configured and known only to this module, whereas the root logger
# is an object shared (and potentially modified) by many modules.
# Modifying the handler, then, is less intrusive and less likely to
# interfere with modifications made by other modules (e.g. in unit
# tests).
handler.setLevel(log_level)
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.NOTSET)
# Filter out most webkitpy messages.
#
# Messages can be selectively re-enabled for this script by updating
# this method accordingly.
def filter(record):
"""Filter out autoinstall and non-third-party webkitpy messages."""
# FIXME: Figure out a way not to use strings here, for example by
# using syntax like webkitpy.test.__name__. We want to be
# sure not to import any non-Python 2.4 code, though, until
# after the version-checking code has executed.
if (record.name.startswith("webkitpy.common.system.autoinstall") or
record.name.startswith("webkitpy.test")):
return True
if record.name.startswith("webkitpy"):
return False
return True
testing_filter = logging.Filter()
testing_filter.filter = filter
# Display a message so developers are not mystified as to why
# logging does not work in the unit tests.
_log.info("Suppressing most webkitpy logging while running unit tests.")
handler.addFilter(testing_filter)
def run(self, dirs, args):
<<<<<<< HEAD
return not self._run_tests(dirs, self._find_modules(dirs, args))
def _find_modules(self, dirs, modules):
# FIXME: We should consider moving webkitpy off of using "webkitpy." to prefix
# all includes. If we did that, then this would use path instead of dirname(path).
# QueueStatusServer.__init__ has a sys.path import hack due to this code.
sys.path.extend(set(os.path.dirname(path) for path in dirs))
if modules:
return modules
=======
args = args or self._find_modules(dirs)
return self._run_tests(dirs, args)
>>>>>>> test-webkitpy: clean up handling of test directories, QueueStatusServer
def _find_modules(self, dirs):
modules = []
for dir_to_search in dirs:
modules.extend(self._find_modules_under(dir_to_search, '_unittest.py'))
if not self._options.skip_integrationtests:
modules.extend(self._find_modules_under(dir_to_search, '_integrationtest.py'))
modules.sort()
for module in modules:
_log.debug("Found: %s" % module)
# FIXME: Figure out how to move this to test-webkitpy in order to to make this file more generic.
if not self._options.all:
slow_tests = ('webkitpy.common.checkout.scm.scm_unittest',)
self._exclude(modules, slow_tests, 'are really, really slow', 31818)
if sys.platform == 'win32':
win32_blacklist = ('webkitpy.common.checkout',
'webkitpy.common.config',
'webkitpy.tool')
self._exclude(modules, win32_blacklist, 'fail horribly on win32', 54526)
return modules
def _exclude(self, modules, module_prefixes, reason, bugid):
_log.info('Skipping tests in the following modules or packages because they %s:' % reason)
for prefix in module_prefixes:
_log.info(' %s' % prefix)
modules_to_exclude = filter(lambda m: m.startswith(prefix), modules)
for m in modules_to_exclude:
if len(modules_to_exclude) > 1:
_log.debug(' %s' % m)
modules.remove(m)
_log.info(' (https://bugs.webkit.org/show_bug.cgi?id=%d; use --all to include)' % bugid)
_log.info('')
def _find_modules_under(self, dir_to_search, suffix):
def to_package(dir_path):
return dir_path.replace(dir_to_search + os.sep, '').replace(os.sep, '.')
def to_module(filename, package):
return package + '.' + filename.replace('.py', '')
modules = []
for dir_path, _, filenames in os.walk(dir_to_search):
package = to_package(dir_path)
modules.extend(to_module(f, package) for f in filenames if f.endswith(suffix))
return modules
def _run_tests(self, dirs, args):
# FIXME: implement a more useful dryrun that recurses into the test suite.
if self._options.dryrun:
return True
if self._options.coverage:
try:
import coverage
except ImportError, e:
_log.error("Failed to import 'coverage'; can't generate coverage numbers.")
return False
cov = coverage.coverage()
cov.start()
_log.debug("Loading the tests...")
loader = unittest.defaultTestLoader
suites = []
for name in args:
if self._is_module(dirs, name):
# import modules explicitly before loading their tests because
# loadTestsFromName() produces lousy error messages for bad modules.
try:
__import__(name)
except ImportError, e:
_log.fatal('Failed to import %s:' % name)
self._log_exception()
return False
suites.append(loader.loadTestsFromName(name, None))
test_suite = unittest.TestSuite(suites)
if self._options.xml:
from webkitpy.thirdparty.autoinstalled.xmlrunner import XMLTestRunner
test_runner = XMLTestRunner(output='test-webkitpy-xml-reports')
else:
test_runner = unittest.TextTestRunner(verbosity=self._verbosity)
_log.debug("Running the tests.")
result = test_runner.run(test_suite)
if self._options.coverage:
cov.stop()
cov.save()
return result.wasSuccessful()
def _is_module(self, dirs, name):
relpath = name.replace('.', os.sep) + '.py'
return any(os.path.exists(os.path.join(d, relpath)) for d in dirs)
def _log_exception(self):
s = StringIO.StringIO()
traceback.print_exc(file=s)
for l in s.buflist:
_log.error(' ' + l.rstrip())