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