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 """this module is responsible for finding python tests."""
     25 
     26 import logging
     27 import re
     28 
     29 
     30 _log = logging.getLogger(__name__)
     31 
     32 
     33 class _DirectoryTree(object):
     34     def __init__(self, filesystem, top_directory, starting_subdirectory):
     35         self.filesystem = filesystem
     36         self.top_directory = filesystem.realpath(top_directory)
     37         self.search_directory = self.top_directory
     38         self.top_package = ''
     39         if starting_subdirectory:
     40             self.top_package = starting_subdirectory.replace(filesystem.sep, '.') + '.'
     41             self.search_directory = filesystem.join(self.top_directory, starting_subdirectory)
     42 
     43     def find_modules(self, suffixes, sub_directory=None):
     44         if sub_directory:
     45             search_directory = self.filesystem.join(self.top_directory, sub_directory)
     46         else:
     47             search_directory = self.search_directory
     48 
     49         def file_filter(filesystem, dirname, basename):
     50             return any(basename.endswith(suffix) for suffix in suffixes)
     51 
     52         filenames = self.filesystem.files_under(search_directory, file_filter=file_filter)
     53         return [self.to_module(filename) for filename in filenames]
     54 
     55     def to_module(self, path):
     56         return path.replace(self.top_directory + self.filesystem.sep, '').replace(self.filesystem.sep, '.')[:-3]
     57 
     58     def subpath(self, path):
     59         """Returns the relative path from the top of the tree to the path, or None if the path is not under the top of the tree."""
     60         realpath = self.filesystem.realpath(self.filesystem.join(self.top_directory, path))
     61         if realpath.startswith(self.top_directory + self.filesystem.sep):
     62             return realpath.replace(self.top_directory + self.filesystem.sep, '')
     63         return None
     64 
     65     def clean(self):
     66         """Delete all .pyc files in the tree that have no matching .py file."""
     67         _log.debug("Cleaning orphaned *.pyc files from: %s" % self.search_directory)
     68         filenames = self.filesystem.files_under(self.search_directory)
     69         for filename in filenames:
     70             if filename.endswith(".pyc") and filename[:-1] not in filenames:
     71                 _log.info("Deleting orphan *.pyc file: %s" % filename)
     72                 self.filesystem.remove(filename)
     73 
     74 
     75 class Finder(object):
     76     def __init__(self, filesystem):
     77         self.filesystem = filesystem
     78         self.trees = []
     79         self._names_to_skip = []
     80 
     81     def add_tree(self, top_directory, starting_subdirectory=None):
     82         self.trees.append(_DirectoryTree(self.filesystem, top_directory, starting_subdirectory))
     83 
     84     def skip(self, names, reason, bugid):
     85         self._names_to_skip.append(tuple([names, reason, bugid]))
     86 
     87     def additional_paths(self, paths):
     88         return [tree.top_directory for tree in self.trees if tree.top_directory not in paths]
     89 
     90     def clean_trees(self):
     91         for tree in self.trees:
     92             tree.clean()
     93 
     94     def is_module(self, name):
     95         relpath = name.replace('.', self.filesystem.sep) + '.py'
     96         return any(self.filesystem.exists(self.filesystem.join(tree.top_directory, relpath)) for tree in self.trees)
     97 
     98     def is_dotted_name(self, name):
     99         return re.match(r'[a-zA-Z.][a-zA-Z0-9_.]*', name)
    100 
    101     def to_module(self, path):
    102         for tree in self.trees:
    103             if path.startswith(tree.top_directory):
    104                 return tree.to_module(path)
    105         return None
    106 
    107     def find_names(self, args, find_all):
    108         suffixes = ['_unittest.py', '_integrationtest.py']
    109         if args:
    110             names = []
    111             for arg in args:
    112                 names.extend(self._find_names_for_arg(arg, suffixes))
    113             return names
    114 
    115         return self._default_names(suffixes, find_all)
    116 
    117     def _find_names_for_arg(self, arg, suffixes):
    118         realpath = self.filesystem.realpath(arg)
    119         if self.filesystem.exists(realpath):
    120             names = self._find_in_trees(realpath, suffixes)
    121             if not names:
    122                 _log.error("%s is not in one of the test trees." % arg)
    123             return names
    124 
    125         # See if it's a python package in a tree (or a relative path from the top of a tree).
    126         names = self._find_in_trees(arg.replace('.', self.filesystem.sep), suffixes)
    127         if names:
    128             return names
    129 
    130         if self.is_dotted_name(arg):
    131             # The name may not exist, but that's okay; we'll find out later.
    132             return [arg]
    133 
    134         _log.error("%s is not a python name or an existing file or directory." % arg)
    135         return []
    136 
    137     def _find_in_trees(self, path, suffixes):
    138         for tree in self.trees:
    139             relpath = tree.subpath(path)
    140             if not relpath:
    141                 continue
    142             if self.filesystem.isfile(path):
    143                 return [tree.to_module(path)]
    144             else:
    145                 return tree.find_modules(suffixes, path)
    146         return []
    147 
    148     def _default_names(self, suffixes, find_all):
    149         modules = []
    150         for tree in self.trees:
    151             modules.extend(tree.find_modules(suffixes))
    152         modules.sort()
    153 
    154         for module in modules:
    155             _log.debug("Found: %s" % module)
    156 
    157         if not find_all:
    158             for (names, reason, bugid) in self._names_to_skip:
    159                 self._exclude(modules, names, reason, bugid)
    160 
    161         return modules
    162 
    163     def _exclude(self, modules, module_prefixes, reason, bugid):
    164         _log.info('Skipping tests in the following modules or packages because they %s:' % reason)
    165         for prefix in module_prefixes:
    166             _log.info('    %s' % prefix)
    167             modules_to_exclude = filter(lambda m: m.startswith(prefix), modules)
    168             for m in modules_to_exclude:
    169                 if len(modules_to_exclude) > 1:
    170                     _log.debug('        %s' % m)
    171                 modules.remove(m)
    172         _log.info('    (https://bugs.webkit.org/show_bug.cgi?id=%d; use --all to include)' % bugid)
    173         _log.info('')
    174