OLD | NEW |
(Empty) | |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 |
| 6 import ConfigParser |
| 7 import glob |
| 8 import os |
| 9 import re |
| 10 |
| 11 |
| 12 CONFIG_FILE_NAME = '.expect_tests.cfg' |
| 13 |
| 14 |
| 15 def get_python_root(path): |
| 16 """Get the lowest directory with no __init__.py file. |
| 17 |
| 18 When ``path`` is pointing inside a Python package, this function returns the |
| 19 directory directly containing this package. If ``path`` points outside of |
| 20 a Python package, the it returns ``path``. |
| 21 |
| 22 Args: |
| 23 path (str): arbitrary path |
| 24 Returns: |
| 25 root (str): ancestor directory, with no __init__.py file in it. |
| 26 """ |
| 27 if not os.path.exists(path): |
| 28 raise ValueError('path must exist: %s') |
| 29 |
| 30 while path != os.path.dirname(path): |
| 31 if not os.path.exists(os.path.join(path, '__init__.py')): |
| 32 return path |
| 33 path = os.path.dirname(path) |
| 34 |
| 35 # This is not supposed to happen, but in case somebody adds a __init__.py |
| 36 # at the filesystem root ... |
| 37 raise IOError("Unable to find a python root for %s" % path) |
| 38 |
| 39 |
| 40 def parse_test_glob(test_glob): |
| 41 """A test glob is composed of a path and a glob expression like: |
| 42 '<path>:<glob>'. The path should point to a directory or a file inside |
| 43 a Python package (it can be the root directory of that package). |
| 44 The glob is a Python name used to filter tests. |
| 45 |
| 46 A test name is the name of the method prepended with the class name. |
| 47 |
| 48 Example: |
| 49 'my/nice/package/test1/:TestA*', the package root being 'my/nice/package': |
| 50 this matches all tests whose class name starts with 'TestA' inside all |
| 51 files matching test1/*_test.py, like |
| 52 ``TestABunchOfStuff.testCatFoodDeployment``. |
| 53 |
| 54 Args: |
| 55 test_glob (str): a test glob |
| 56 Returns: |
| 57 (path, test_filter): absolute path and test filter glob. |
| 58 """ |
| 59 parts = test_glob.split(':') |
| 60 if len(parts) > 2: |
| 61 raise ValueError('A test_glob should contain at most one colon (got %s)' |
| 62 % test_glob) |
| 63 if len(parts) == 2: |
| 64 path, test_filter = parts |
| 65 if '/' in test_filter: |
| 66 raise ValueError('A test filter cannot contain a slash (got %s)', |
| 67 test_filter) |
| 68 |
| 69 if not test_filter: # empty string case |
| 70 test_filter = ('*',) |
| 71 else: |
| 72 path, test_filter = parts[0], ('*',) |
| 73 |
| 74 path = os.path.abspath(path) |
| 75 return path, test_filter |
| 76 |
| 77 |
| 78 class PackageTestingContext(object): |
| 79 def __init__(self, cwd, package_name, filters): |
| 80 """Information to run a set of tests in a single package. |
| 81 |
| 82 See also parse_test_glob. |
| 83 """ |
| 84 # TODO(iannucci): let's scan packages too so that <path> can also be a |
| 85 # glob. Then expect_tests can use a default of '*:*' when no tests are |
| 86 # specified. |
| 87 |
| 88 self.cwd = cwd |
| 89 self.package_name = package_name |
| 90 # list of (path, filter) pairs. |
| 91 # The path is a relative path to a subdirectory of |
| 92 # os.path.join(self.cwd, self.package_name) in which to look for tests. |
| 93 # Only tests whose name matches the glob are kept. |
| 94 self.filters = filters |
| 95 |
| 96 def itermatchers(self): |
| 97 """Iterate over all filters, and yield matchers for each of them. |
| 98 |
| 99 Yields: |
| 100 path (str): restrict test listing to this subpackage. |
| 101 matcher (SRE_Pattern): whitelist matcher |
| 102 """ |
| 103 for filt in self.filters: |
| 104 # Implicitely append * to globs |
| 105 one_glob = '%s%s' % (filt[1], '*' if '*' not in filt[1] else '') |
| 106 matcher = re.compile('^(?:%s)$' % glob.fnmatch.translate(one_glob)) |
| 107 |
| 108 if matcher.pattern == '^$': |
| 109 matcher = re.compile('^.*$') |
| 110 |
| 111 yield filt[0], matcher |
| 112 |
| 113 @classmethod |
| 114 def from_path(cls, path, filters=('*',)): |
| 115 path = os.path.abspath(path) |
| 116 if not os.path.exists(path): |
| 117 raise ValueError('Path does not exist: %s' % path) |
| 118 cwd = get_python_root(path) |
| 119 package_name = os.path.relpath(path, cwd).split(os.path.sep)[0] |
| 120 # The path in which to look for tests. Only tests whose name matches the |
| 121 # glob are kept. |
| 122 relpath = os.path.relpath(path, os.path.join(cwd, package_name)) |
| 123 |
| 124 if not isinstance(filters, (list, tuple)): |
| 125 raise ValueError('the "filter" parameter must be a tuple or a list, ' |
| 126 'got %s' % type(filters).__name__) |
| 127 if len(filters) == 0: |
| 128 filters = [(relpath, '*')] |
| 129 else: |
| 130 filters = [(relpath, filt) for filt in filters] |
| 131 |
| 132 return cls(cwd, package_name, filters) |
| 133 |
| 134 @classmethod |
| 135 def from_context_list(cls, contexts): |
| 136 """Merge several PackageTestingContext pointing to the same package.""" |
| 137 cwd = set(context.cwd for context in contexts) |
| 138 if len(cwd) > 1: |
| 139 raise ValueError( |
| 140 'from_context_list() was given' |
| 141 'process contexts containing the following cwds, ' |
| 142 'but can only process contexts which all share a single cwd: ' |
| 143 '%s' % str(cwd)) |
| 144 |
| 145 package_name = set(context.package_name for context in contexts) |
| 146 if len(package_name) > 1: |
| 147 raise ValueError( |
| 148 'from_context_list() was given' |
| 149 'process contexts containing the following package_name, ' |
| 150 'but can only process contexts which all share a single package_name: ' |
| 151 '%s' % str(package_name)) |
| 152 |
| 153 filters = [] |
| 154 for context in contexts: |
| 155 filters.extend(context.filters) |
| 156 |
| 157 return cls(cwd.pop(), package_name.pop(), filters) |
| 158 |
| 159 |
| 160 class ProcessingContext(object): |
| 161 def __init__(self, testing_contexts): |
| 162 """Information to run a set of tasks in a given working directory. |
| 163 |
| 164 Args: |
| 165 testing_contexts (list): list of PackageTestingContext instances. |
| 166 """ |
| 167 self.cwd = testing_contexts[0].cwd |
| 168 |
| 169 # Merge testing_contexts by package |
| 170 groups = {} |
| 171 for context in testing_contexts: |
| 172 if context.cwd != self.cwd: |
| 173 raise ValueError('All package must have the same value for "cwd"') |
| 174 groups.setdefault(context.package_name, []).append(context) |
| 175 |
| 176 self.testing_contexts = [PackageTestingContext.from_context_list(contexts) |
| 177 for contexts in groups.itervalues()] |
| 178 |
| 179 |
| 180 def get_config(path): |
| 181 """Get configuration values |
| 182 |
| 183 Reads the config file in provided path, and returns content. |
| 184 See Python ConfigParser for general formatting syntax. |
| 185 |
| 186 Example: |
| 187 [expect_tests] |
| 188 skip=directory1 |
| 189 directory2 |
| 190 directory3 |
| 191 |
| 192 Args: |
| 193 path (str): path to a directory. |
| 194 |
| 195 Returns: |
| 196 black_list (set): blacklisted subdirectories. |
| 197 """ |
| 198 black_list = set() |
| 199 |
| 200 config_file_name = os.path.join(path, CONFIG_FILE_NAME) |
| 201 parser = ConfigParser.ConfigParser() |
| 202 parser.read([config_file_name]) |
| 203 |
| 204 if not parser.has_section('expect_tests'): |
| 205 return black_list |
| 206 |
| 207 if parser.has_option('expect_tests', 'skip'): |
| 208 black_list.update(parser.get('expect_tests', 'skip').splitlines()) |
| 209 return black_list |
| 210 |
| 211 |
| 212 def get_runtime_contexts(test_globs): |
| 213 """Compute the list of packages/filters to get tests from.""" |
| 214 # Step 1: compute list of packages + subtree |
| 215 testing_contexts = [] |
| 216 for test_glob in test_globs: |
| 217 path, test_filter = parse_test_glob(test_glob) |
| 218 if os.path.exists(os.path.join(path, '__init__.py')): |
| 219 testing_contexts.append( |
| 220 PackageTestingContext.from_path(path, test_filter)) |
| 221 else: |
| 222 # Look for all packages in path. |
| 223 subpaths = [] |
| 224 black_list = get_config(path) |
| 225 |
| 226 for filename in filter(lambda x: x not in black_list, os.listdir(path)): |
| 227 abs_filename = os.path.join(path, filename) |
| 228 if (os.path.isdir(abs_filename) |
| 229 and os.path.isfile(os.path.join(abs_filename, '__init__.py'))): |
| 230 subpaths.append(abs_filename) |
| 231 |
| 232 testing_contexts.extend( |
| 233 [PackageTestingContext.from_path(subpath, test_filter) |
| 234 for subpath in subpaths]) |
| 235 |
| 236 # Step 2: group by working directory - one process per wd. |
| 237 groups = {} |
| 238 for context in testing_contexts: |
| 239 groups.setdefault(context.cwd, []).append(context) |
| 240 return [ProcessingContext(contexts) for contexts in groups.itervalues()] |
OLD | NEW |