1# Copyright (C) 2012 Google, Inc. 2# Copyright (C) 2010 Chris Jerdonek (cjerdonek@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 26import logging 27import re 28 29 30_log = logging.getLogger(__name__) 31 32 33class _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 75class 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