Home | History | Annotate | Download | only in test
      1 # Copyright (C) 2012 Google, Inc.
      2 # Copyright (C) 2010 Chris Jerdonek (cjerdonek (at] webkit.org)
      3 #
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions
      6 # are met:
      7 # 1.  Redistributions of source code must retain the above copyright
      8 #     notice, this list of conditions and the following disclaimer.
      9 # 2.  Redistributions in binary form must reproduce the above copyright
     10 #     notice, this list of conditions and the following disclaimer in the
     11 #     documentation and/or other materials provided with the distribution.
     12 #
     13 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
     14 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     15 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     16 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
     17 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     18 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
     19 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
     20 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
     21 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     22 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     23 
     24 """unit testing code for webkitpy."""
     25 
     26 import logging
     27 import multiprocessing
     28 import optparse
     29 import os
     30 import StringIO
     31 import sys
     32 import time
     33 import traceback
     34 import unittest
     35 
     36 from webkitpy.common.webkit_finder import WebKitFinder
     37 from webkitpy.common.system.filesystem import FileSystem
     38 from webkitpy.test.finder import Finder
     39 from webkitpy.test.printer import Printer
     40 from webkitpy.test.runner import Runner, unit_test_name
     41 
     42 _log = logging.getLogger(__name__)
     43 
     44 
     45 up = os.path.dirname
     46 webkit_root = up(up(up(up(up(os.path.abspath(__file__))))))
     47 
     48 
     49 def main():
     50     filesystem = FileSystem()
     51     wkf = WebKitFinder(filesystem)
     52     tester = Tester(filesystem, wkf)
     53     tester.add_tree(wkf.path_from_webkit_base('Tools', 'Scripts'), 'webkitpy')
     54 
     55     tester.skip(('webkitpy.common.checkout.scm.scm_unittest',), 'are really, really, slow', 31818)
     56     if sys.platform == 'win32':
     57         tester.skip(('webkitpy.common.checkout', 'webkitpy.common.config', 'webkitpy.tool', 'webkitpy.w3c', 'webkitpy.layout_tests.layout_package.bot_test_expectations'), 'fail horribly on win32', 54526)
     58 
     59     # This only needs to run on Unix, so don't worry about win32 for now.
     60     appengine_sdk_path = '/usr/local/google_appengine'
     61     if os.path.exists(appengine_sdk_path):
     62         if not appengine_sdk_path in sys.path:
     63             sys.path.append(appengine_sdk_path)
     64         import dev_appserver
     65         from google.appengine.dist import use_library
     66         use_library('django', '1.2')
     67         dev_appserver.fix_sys_path()
     68         tester.add_tree(wkf.path_from_webkit_base('Tools', 'TestResultServer'))
     69     else:
     70         _log.info('Skipping TestResultServer tests; the Google AppEngine Python SDK is not installed.')
     71 
     72     return not tester.run()
     73 
     74 
     75 class Tester(object):
     76     def __init__(self, filesystem=None, webkit_finder=None):
     77         self.filesystem = filesystem or FileSystem()
     78         self.finder = Finder(self.filesystem)
     79         self.printer = Printer(sys.stderr)
     80         self.webkit_finder = webkit_finder or WebKitFinder(self.filesystem)
     81         self._options = None
     82 
     83     def add_tree(self, top_directory, starting_subdirectory=None):
     84         self.finder.add_tree(top_directory, starting_subdirectory)
     85 
     86     def skip(self, names, reason, bugid):
     87         self.finder.skip(names, reason, bugid)
     88 
     89     def _parse_args(self, argv=None):
     90         parser = optparse.OptionParser(usage='usage: %prog [options] [args...]')
     91         parser.add_option('-a', '--all', action='store_true', default=False,
     92                           help='run all the tests')
     93         parser.add_option('-c', '--coverage', action='store_true', default=False,
     94                           help='generate code coverage info')
     95         parser.add_option('-i', '--integration-tests', action='store_true', default=False,
     96                           help='run integration tests as well as unit tests'),
     97         parser.add_option('-j', '--child-processes', action='store', type='int', default=(1 if sys.platform == 'win32' else multiprocessing.cpu_count()),
     98                           help='number of tests to run in parallel (default=%default)')
     99         parser.add_option('-p', '--pass-through', action='store_true', default=False,
    100                           help='be debugger friendly by passing captured output through to the system')
    101         parser.add_option('-q', '--quiet', action='store_true', default=False,
    102                           help='run quietly (errors, warnings, and progress only)')
    103         parser.add_option('-t', '--timing', action='store_true', default=False,
    104                           help='display per-test execution time (implies --verbose)')
    105         parser.add_option('-v', '--verbose', action='count', default=0,
    106                           help='verbose output (specify once for individual test results, twice for debug messages)')
    107 
    108         parser.epilog = ('[args...] is an optional list of modules, test_classes, or individual tests. '
    109                          'If no args are given, all the tests will be run.')
    110 
    111         return parser.parse_args(argv)
    112 
    113     def run(self):
    114         self._options, args = self._parse_args()
    115         self.printer.configure(self._options)
    116 
    117         self.finder.clean_trees()
    118 
    119         names = self.finder.find_names(args, self._options.all)
    120         if not names:
    121             _log.error('No tests to run')
    122             return False
    123 
    124         return self._run_tests(names)
    125 
    126     def _run_tests(self, names):
    127         # Make sure PYTHONPATH is set up properly.
    128         sys.path = self.finder.additional_paths(sys.path) + sys.path
    129 
    130         # FIXME: unittest2 and coverage need to be in sys.path for their internal imports to work.
    131         thirdparty_path = self.webkit_finder.path_from_webkit_base('Tools', 'Scripts', 'webkitpy', 'thirdparty')
    132         if not thirdparty_path in sys.path:
    133             sys.path.append(thirdparty_path)
    134 
    135         if self._options.coverage:
    136             _log.warning("Checking code coverage, so running things serially")
    137             self._options.child_processes = 1
    138 
    139             import coverage
    140             cov = coverage.coverage(omit=["/usr/*", "*/webkitpy/thirdparty/*", "/Library/*"])
    141             cov.start()
    142 
    143         self.printer.write_update("Checking imports ...")
    144         if not self._check_imports(names):
    145             return False
    146 
    147         self.printer.write_update("Finding the individual test methods ...")
    148         loader = _Loader()
    149         parallel_tests, serial_tests = self._test_names(loader, names)
    150 
    151         self.printer.write_update("Running the tests ...")
    152         self.printer.num_tests = len(parallel_tests) + len(serial_tests)
    153         start = time.time()
    154         test_runner = Runner(self.printer, loader, self.webkit_finder)
    155         test_runner.run(parallel_tests, self._options.child_processes)
    156         test_runner.run(serial_tests, 1)
    157 
    158         self.printer.print_result(time.time() - start)
    159 
    160         if self._options.coverage:
    161             cov.stop()
    162             cov.save()
    163             cov.report(show_missing=False)
    164 
    165         return not self.printer.num_errors and not self.printer.num_failures
    166 
    167     def _check_imports(self, names):
    168         for name in names:
    169             if self.finder.is_module(name):
    170                 # if we failed to load a name and it looks like a module,
    171                 # try importing it directly, because loadTestsFromName()
    172                 # produces lousy error messages for bad modules.
    173                 try:
    174                     __import__(name)
    175                 except ImportError:
    176                     _log.fatal('Failed to import %s:' % name)
    177                     self._log_exception()
    178                     return False
    179         return True
    180 
    181     def _test_names(self, loader, names):
    182         parallel_test_method_prefixes = ['test_']
    183         serial_test_method_prefixes = ['serial_test_']
    184         if self._options.integration_tests:
    185             parallel_test_method_prefixes.append('integration_test_')
    186             serial_test_method_prefixes.append('serial_integration_test_')
    187 
    188         parallel_tests = []
    189         loader.test_method_prefixes = parallel_test_method_prefixes
    190         for name in names:
    191             parallel_tests.extend(self._all_test_names(loader.loadTestsFromName(name, None)))
    192 
    193         serial_tests = []
    194         loader.test_method_prefixes = serial_test_method_prefixes
    195         for name in names:
    196             serial_tests.extend(self._all_test_names(loader.loadTestsFromName(name, None)))
    197 
    198         # loader.loadTestsFromName() will not verify that names begin with one of the test_method_prefixes
    199         # if the names were explicitly provided (e.g., MainTest.test_basic), so this means that any individual
    200         # tests will be included in both parallel_tests and serial_tests, and we need to de-dup them.
    201         serial_tests = list(set(serial_tests).difference(set(parallel_tests)))
    202 
    203         return (parallel_tests, serial_tests)
    204 
    205     def _all_test_names(self, suite):
    206         names = []
    207         if hasattr(suite, '_tests'):
    208             for t in suite._tests:
    209                 names.extend(self._all_test_names(t))
    210         else:
    211             names.append(unit_test_name(suite))
    212         return names
    213 
    214     def _log_exception(self):
    215         s = StringIO.StringIO()
    216         traceback.print_exc(file=s)
    217         for l in s.buflist:
    218             _log.error('  ' + l.rstrip())
    219 
    220 
    221 class _Loader(unittest.TestLoader):
    222     test_method_prefixes = []
    223 
    224     def getTestCaseNames(self, testCaseClass):
    225         def isTestMethod(attrname, testCaseClass=testCaseClass):
    226             if not hasattr(getattr(testCaseClass, attrname), '__call__'):
    227                 return False
    228             return (any(attrname.startswith(prefix) for prefix in self.test_method_prefixes))
    229         testFnNames = filter(isTestMethod, dir(testCaseClass))
    230         testFnNames.sort()
    231         return testFnNames
    232 
    233 
    234 if __name__ == '__main__':
    235     sys.exit(main())
    236