Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import contextlib | 5 import contextlib |
| 6 import ConfigParser | 6 import ConfigParser |
| 7 import glob | 7 import glob |
| 8 import imp | 8 import imp |
| 9 import inspect | 9 import inspect |
| 10 import itertools | |
| 10 import logging | 11 import logging |
| 11 import multiprocessing | 12 import multiprocessing |
| 12 import os | 13 import os |
| 13 import Queue | 14 import Queue |
| 14 import re | 15 import re |
| 15 import signal | 16 import signal |
| 16 import sys | 17 import sys |
| 17 import tempfile | 18 import tempfile |
| 18 import traceback | 19 import traceback |
| 19 | 20 |
| (...skipping 25 matching lines...) Expand all Loading... | |
| 45 def __init__(self): | 46 def __init__(self): |
| 46 self._stream = StringIO() | 47 self._stream = StringIO() |
| 47 | 48 |
| 48 def reset(self): | 49 def reset(self): |
| 49 self._stream = StringIO() | 50 self._stream = StringIO() |
| 50 | 51 |
| 51 def __getattr__(self, key): | 52 def __getattr__(self, key): |
| 52 return getattr(self._stream, key) | 53 return getattr(self._stream, key) |
| 53 | 54 |
| 54 | 55 |
| 56 def get_python_root(path): | |
| 57 """Get the lowest directory with no __init__.py file. | |
| 58 | |
| 59 When ``path`` is pointing inside a Python package, this function returns the | |
| 60 directory directly containing this package. If ``path`` points outside of | |
| 61 a Python package, the it returns ``path``. | |
| 62 | |
| 63 Args: | |
| 64 path (str): arbitrary path | |
| 65 Returns: | |
| 66 root (str): ancestor directory, with no __init__.py file in it. | |
| 67 """ | |
| 68 if not os.path.exists(path): | |
| 69 raise ValueError('path must exist: %s') | |
| 70 | |
| 71 while path != os.path.dirname(path): | |
| 72 if not os.path.exists(os.path.join(path, '__init__.py')): | |
| 73 return path | |
| 74 path = os.path.dirname(path) | |
| 75 | |
| 76 # This is not supposed to happen, but in case somebody adds a __init__.py | |
| 77 # at the filesystem root ... | |
|
agable
2014/11/12 03:15:09
Love the French spacing between words and punctuat
pgervais
2014/11/13 00:28:26
ALERT: troll detected in comment line 77.
| |
| 78 raise IOError("Unable to find a python root for %s" % path) | |
| 79 | |
| 80 | |
| 55 def get_package_path(package_name, path): | 81 def get_package_path(package_name, path): |
| 56 """Return path toward 'package_name'. | 82 """Return path toward 'package_name'. |
| 57 | 83 |
| 58 If path is None, search for a package in sys.path. | 84 If path is None, search for a package in sys.path. |
| 59 Otherwise, look for a direct subdirectory of path. | 85 Otherwise, look for a direct subdirectory of path. |
| 60 | 86 |
| 61 If no package is found, returns None. | 87 If no package is found, returns None. |
| 62 """ | 88 """ |
| 63 if path is None: | 89 if path is None: |
| 64 _, package_path, _ = imp.find_module(package_name) | 90 _, package_path, _ = imp.find_module(package_name) |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 107 | 133 |
| 108 Args: | 134 Args: |
| 109 filename (str): path to a python file. | 135 filename (str): path to a python file. |
| 110 | 136 |
| 111 Returns: | 137 Returns: |
| 112 is_test (boolean): True if filename points to a test file. | 138 is_test (boolean): True if filename points to a test file. |
| 113 """ | 139 """ |
| 114 return filename.endswith('_test.py') | 140 return filename.endswith('_test.py') |
| 115 | 141 |
| 116 | 142 |
| 117 def walk_package(package_name, path): | 143 def walk_package(package_name, path, subpath=None): |
| 118 """Return all test files inside a single package. | 144 """Return all test files inside a single package. |
| 119 | 145 |
| 120 In all cases, this function returns the full package name of files ending | 146 In all cases, this function returns the full package name of files ending |
| 121 in '_test.py' found either under the package called <package_name>. | 147 in '_test.py' found either under the package called <package_name>. |
| 122 Example: 'test_package.foo_test' for 'test_package/foo_test.py'. | 148 Example: 'test_package.foo_test' for 'test_package/foo_test.py'. |
| 123 | 149 |
| 124 Provided that <path> is in sys.path, calling __import__ with one of the | 150 Provided that <path> is in sys.path, calling __import__ with one of the |
| 125 strings returned by this function works. | 151 strings returned by this function works. |
| 126 | 152 |
| 127 If a config file is present somewhere in the search hierarchy, | 153 If a config file is present somewhere in the search hierarchy, |
| 128 it is interpreted as a list of subdirectories to ignore. This is the way to | 154 it is interpreted as a list of subdirectories to ignore. This is the way to |
| 129 make this function ignore some subpackages. | 155 make this function ignore some subpackages. |
| 130 | 156 |
| 131 This function has several behaviors depending on the arguments: | 157 This function has several behaviors depending on the arguments: |
| 132 - if path is None: search for package_name under any path in sys.path. | 158 - if path is None: search for package_name under any path in sys.path. |
| 133 - if path is not None: search for a package called package_name directly | 159 - if path is not None: search for a package called package_name directly |
| 134 under <path> (sys.path is not used). | 160 under <path> (sys.path is not used). |
| 135 | 161 |
| 136 It is not necessary to change sys.path for the present function to work (it | 162 It is not necessary to change sys.path for the present function to work (it |
| 137 does not actually import anything). | 163 does not actually import anything). |
| 138 | 164 |
| 139 Args: | 165 Args: |
| 140 package_name (str): name of the package, as expected by import. | 166 package_name (str): name of the package, as expected by import. |
| 141 path (str): path containing the above module (optional) | 167 path (str): path containing the above module (optional) |
| 168 subpath (str, optional): path inside the package, pointing to a subpackage. | |
| 169 This is used to restrict the listing to part of the package. | |
| 142 | 170 |
| 143 Returns: | 171 Returns: |
| 144 test_modules (list of str): name of modules containing tests. Each element is | 172 test_modules (list of str): name of modules containing tests. Each element is |
| 145 a period-separated string ending with '_test', | 173 a period-separated string ending with '_test', |
| 146 e.g. shiny_package.subpackage.feature_test | 174 e.g. shiny_package.subpackage.feature_test |
| 147 | 175 |
| 148 Example: | 176 Example: |
| 149 modules = walk_package('shiny_package', 'some/directory') | 177 modules = walk_package('shiny_package', 'some/directory') |
| 150 sys.path.insert(0, 'some/directory') | 178 sys.path.insert(0, 'some/directory') |
| 151 __import__(modules[0]) | 179 __import__(modules[0]) |
| 152 | 180 |
| 153 the first line assumes that the directory 'some/directory' is in the | 181 the first line assumes that the directory 'some/directory' is in the |
| 154 current working directory. | 182 current working directory. |
| 155 """ | 183 """ |
| 156 assert package_name, 'walk_package needs a package_name.' | 184 assert package_name, 'walk_package needs a package_name.' |
| 157 | 185 |
| 158 test_modules = [] | 186 test_modules = [] |
| 159 package_path = get_package_path(package_name, path) | 187 package_path = get_package_path(package_name, path) |
| 160 assert package_path, 'no package found.' | 188 assert package_path, 'no package found.' |
| 161 | 189 |
| 162 base_path = os.path.split(package_path.rstrip(os.path.sep))[0] | 190 base_path = os.path.split(package_path.rstrip(os.path.sep))[0] |
| 163 assert package_path.startswith(base_path) | 191 assert package_path.startswith(base_path) |
| 164 | 192 |
| 165 explored = set() | 193 explored = set() |
| 166 | 194 |
| 167 for dirpath, dirnames, filenames in os.walk(package_path, followlinks=True): | 195 if subpath: |
| 196 start_path = os.path.join(package_path, subpath) | |
| 197 if not os.path.exists(os.path.join(start_path)): | |
|
agable
2014/11/12 03:15:10
os.path.join not necessary
pgervais
2014/11/13 00:28:26
Done.
| |
| 198 raise ValueError('Provided subpath does not exist: %s' % start_path) | |
| 199 else: | |
| 200 start_path = package_path | |
| 201 | |
| 202 for dirpath, dirnames, filenames in os.walk(start_path, followlinks=True): | |
| 168 # Keep only submodules not blacklisted, break symlink cycles | 203 # Keep only submodules not blacklisted, break symlink cycles |
| 169 blacklist = get_config(dirpath) | 204 blacklist = get_config(dirpath) |
| 170 dirnames[:] = [d for d in dirnames | 205 dirnames[:] = [d for d in dirnames |
| 171 if d not in blacklist and | 206 if d not in blacklist and |
| 172 os.path.isfile(os.path.join(dirpath, d, '__init__.py')) and | 207 os.path.isfile(os.path.join(dirpath, d, '__init__.py')) and |
| 173 os.path.realpath(os.path.join(dirpath, d)) not in explored] | 208 os.path.realpath(os.path.join(dirpath, d)) not in explored] |
| 174 realpaths = [os.path.realpath(os.path.join(dirpath, d)) for d in dirnames] | 209 realpaths = [os.path.realpath(os.path.join(dirpath, d)) for d in dirnames] |
| 175 explored.update(realpaths) | 210 explored.update(realpaths) |
| 176 | 211 |
| 177 assert dirpath.startswith(package_path) | 212 assert dirpath.startswith(start_path) |
| 178 base_module_name = os.path.relpath(dirpath, base_path).split(os.path.sep) | 213 base_module_name = os.path.relpath(dirpath, base_path).split(os.path.sep) |
| 179 test_modules.extend(['.'.join(base_module_name | 214 test_modules.extend(['.'.join(base_module_name |
| 180 + [inspect.getmodulename(filename)]) | 215 + [inspect.getmodulename(filename)]) |
| 181 for filename in filenames | 216 for filename in filenames |
| 182 if is_test_file(filename)]) | 217 if is_test_file(filename)]) |
| 183 | 218 |
| 184 return test_modules | 219 return test_modules |
| 185 | 220 |
| 186 | 221 |
| 187 def load_module(modname): | 222 def load_module(modname): |
| 188 """Import and return the specified module. | 223 """Import and return the specified module. |
| 189 | 224 |
| 190 Uses __import__ instead of pkgutil's PEP302-style find_module/load_module | 225 Uses __import__ instead of pkgutil's PEP302-style find_module/load_module |
| 191 because those just don't work. The tradeoff is that we have to walk down the | 226 because those just don't work. The tradeoff is that we have to walk down the |
| 192 package hierarchy to find the leaf module (since __import__ returns the | 227 package hierarchy to find the leaf module (since __import__ returns the |
| 193 topmost parent package), but at least this behaves deterministically. | 228 topmost parent package), but at least this behaves deterministically. |
| 194 """ | 229 """ |
| 195 mod = __import__(modname) | 230 mod = __import__(modname) |
| 196 | 231 |
| 197 for part in modname.split('.')[1:]: | 232 for part in modname.split('.')[1:]: |
| 198 mod = getattr(mod, part) | 233 mod = getattr(mod, part) |
| 199 return mod | 234 return mod |
| 200 | 235 |
| 201 | 236 |
| 202 def get_test_gens_directory(path, cwd): | 237 def get_test_gens_package(testing_context, subpath=None): |
| 203 """Given a path, return list of MultiTest or Test instances. | 238 """Given a testing context, return list of generators of *Test instances. |
| 204 | 239 |
| 205 See UnittestTestCase for possible return values. | 240 See UnittestTestCase for possible return values. |
| 206 | 241 |
| 207 This function loads modules, thus no two conflicting packages (like appengine) | |
| 208 can be loaded at the same time: use separate processes for that. | |
| 209 """ | |
| 210 assert isinstance(path, basestring), 'path must be a string' | |
| 211 assert os.path.isdir(path), 'path is not a directory: %s' % path | |
| 212 sys.path.insert(0, os.path.abspath(path)) | |
| 213 | |
| 214 test_gens = [] | |
| 215 black_list = get_config(path) | |
| 216 | |
| 217 for filename in filter(lambda x: x not in black_list, os.listdir(path)): | |
| 218 abs_filename = os.path.join(path, filename) | |
| 219 if (os.path.isdir(abs_filename) | |
| 220 and os.path.isfile(os.path.join(abs_filename, '__init__.py'))): | |
| 221 test_gens += get_test_gens_package(abs_filename, cwd, | |
| 222 update_syspath=False) | |
| 223 return test_gens | |
| 224 | |
| 225 | |
| 226 def get_test_gens_package(package, cwd, update_syspath=True): | |
| 227 """Given a path, return list of MultiTest or Test instances. | |
| 228 | |
| 229 See UnittestTestCase for possible return values. | |
| 230 | |
| 231 This function loads modules, thus no two conflicting packages (like appengine) | 242 This function loads modules, thus no two conflicting packages (like appengine) |
| 232 should be loaded at the same time: use separate processes for that. | 243 should be loaded at the same time: use separate processes for that. |
| 233 | 244 |
| 234 Args: | 245 Args: |
| 235 package (str): path to a Python package. | 246 testing_context (PackageTestingContext): what to test. |
| 236 update_syspath (boolean): if True, the parent directory of 'package' is | 247 subpath (str): relative path in the tested package to restrict the search |
| 237 prepended to sys.path. | 248 to. Relative to |
| 249 os.path.join(testing_context.cwd, testing_context.package_name) | |
|
agable
2014/11/12 03:15:09
Haven't read all the way down yet, but it seems li
| |
| 250 | |
| 251 Returns: | |
| 252 gens_list (list of generator of tests): tests are instances of Test | |
| 253 or MultiTest. | |
| 238 """ | 254 """ |
| 239 assert isinstance(package, basestring), "package name should be a string." | 255 test_gens = [] |
| 240 assert os.path.isfile(os.path.join(package, '__init__.py')), \ | |
| 241 "'package' is not pointing to a package. It must be a " + \ | |
| 242 "path to a directory containing a __init__.py file." | |
| 243 | 256 |
| 244 test_gens = [] | 257 # TODO(pgervais) add filtering on test names (use testing_context.filters) |
| 245 path = os.path.abspath(os.path.dirname(package)) | 258 for modname in walk_package(testing_context.package_name, |
| 246 if update_syspath: | 259 testing_context.cwd, subpath=subpath): |
| 247 sys.path.insert(0, path) | 260 with use_chdir(testing_context.cwd): |
| 248 | |
| 249 package_name = os.path.split(package.rstrip(os.path.sep))[-1] | |
| 250 | |
| 251 for modname in walk_package(package_name, path): | |
| 252 with use_chdir(cwd): | |
| 253 mod = load_module(modname) | 261 mod = load_module(modname) |
| 254 for obj in mod.__dict__.values(): | 262 for obj in mod.__dict__.values(): |
| 255 if util.is_test_generator(obj): | 263 if util.is_test_generator(obj): |
| 256 test_gens.append(obj) | 264 test_gens.append(obj) |
| 257 elif _is_unittest(obj): | 265 elif _is_unittest(obj): |
| 258 test_gens.append(UnittestTestCase(obj)) | 266 test_gens.append(UnittestTestCase(obj)) |
| 259 return test_gens | 267 return test_gens |
| 260 | 268 |
| 261 | 269 |
| 262 def gen_loop_process(gens, test_queue, result_queue, opts, kill_switch, | 270 def gen_loop_process(testing_contexts, test_queue, result_queue, opts, |
| 263 cover_ctx, temp_dir): | 271 kill_switch, cover_ctx, temp_dir): |
| 264 """Generate `Test`s from |gens|, and feed them into |test_queue|. | 272 """Generate `Test`s from |gens|, and feed them into |test_queue|. |
| 265 | 273 |
| 266 Non-Test instances will be translated into `UnknownError` objects. | 274 Non-Test instances will be translated into `UnknownError` objects. |
| 267 | 275 |
| 268 Args: | 276 Args: |
| 269 gens: list of generators yielding Test() instances. | 277 testing_contexts (list of PackageTestingContext): describe tests to |
| 278 process. | |
|
agable
2014/11/12 03:15:09
Indent +4 for continuation? I dunno.
| |
| 270 test_queue (multiprocessing.Queue): | 279 test_queue (multiprocessing.Queue): |
|
agable
2014/11/12 03:15:10
Love these args with no descriptions
iannucci
2014/11/12 20:26:24
I think this was my fault :)
pgervais
2014/11/13 00:28:26
This time I think it was mine, I added them last t
| |
| 271 result_queue (multiprocessing.Queue): | 280 result_queue (multiprocessing.Queue): |
| 272 opts (argparse.Namespace): | 281 opts (argparse.Namespace): |
| 273 kill_switch (multiprocessing.Event): | 282 kill_switch (multiprocessing.Event): |
| 274 cover_ctx (cover.CoverageContext().create_subprocess_context) | 283 cover_ctx (cover.CoverageContext().create_subprocess_context) |
| 275 """ | 284 """ |
| 285 | |
| 286 SENTINEL = object() | |
| 276 tempfile.tempdir = temp_dir | 287 tempfile.tempdir = temp_dir |
| 277 | 288 |
| 278 # Implicitly append '*' to globs that don't specify it. | |
| 279 globs = ['%s%s' % (g, '*' if '*' not in g else '') for g in opts.test_glob] | |
| 280 | |
| 281 matcher = re.compile( | |
| 282 '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g) | |
| 283 for g in globs if g[0] != '-')) | |
| 284 if matcher.pattern == '^$': | |
| 285 matcher = re.compile('^.*$') | |
| 286 | |
| 287 neg_matcher = re.compile( | |
| 288 '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g[1:]) | |
| 289 for g in globs if g[0] == '-')) | |
| 290 | |
| 291 SENTINEL = object() | |
| 292 | |
| 293 def generate_tests(): | 289 def generate_tests(): |
| 294 paths_seen = set() | |
| 295 seen_tests = False | 290 seen_tests = False |
| 296 try: | 291 try: |
| 297 for gen in gens: | 292 for testing_context in testing_contexts: |
| 298 gen_cover_ctx = cover_ctx(include=util.get_cover_list(gen)) | 293 for subpath, matcher in testing_context.itermatchers(): |
| 294 paths_seen = set() | |
| 299 | 295 |
| 300 with gen_cover_ctx: | 296 with cover_ctx: |
| 301 gen_inst = gen() | 297 gens = get_test_gens_package(testing_context, subpath=subpath) |
| 302 | 298 |
| 303 while not kill_switch.is_set(): | 299 for gen in gens: |
| 304 with gen_cover_ctx: | 300 gen_cover_ctx = cover_ctx(include=util.get_cover_list(gen)) |
| 305 root_test = next(gen_inst, SENTINEL) | |
| 306 | 301 |
| 307 if root_test is SENTINEL: | 302 with gen_cover_ctx: |
| 308 break | 303 gen_inst = gen() |
| 309 | 304 |
| 310 if kill_switch.is_set(): | 305 while not kill_switch.is_set(): |
| 311 break | 306 with gen_cover_ctx: |
| 307 root_test = next(gen_inst, SENTINEL) | |
| 312 | 308 |
| 313 ok_tests = [] | 309 if root_test is SENTINEL: |
| 310 break | |
| 314 | 311 |
| 315 if isinstance(root_test, MultiTest): | 312 if kill_switch.is_set(): |
|
agable
2014/11/12 03:15:09
I know this is copied, but doesn't it make sense t
iannucci
2014/11/12 20:26:25
We do that in the loop condition.
| |
| 316 subtests = root_test.tests | 313 break |
| 317 else: | |
| 318 subtests = [root_test] | |
| 319 | 314 |
| 320 for subtest in subtests: | 315 ok_tests = [] |
| 321 if not isinstance(subtest, Test): | |
| 322 result_queue.put_nowait( | |
| 323 UnknownError( | |
| 324 'Got non-[Multi]Test isinstance from generator: %r.' | |
| 325 % subtest)) | |
| 326 continue | |
| 327 | 316 |
| 328 test_path = subtest.expect_path() | 317 if isinstance(root_test, MultiTest): |
| 329 if test_path is not None and test_path in paths_seen: | 318 subtests = root_test.tests |
| 330 result_queue.put_nowait( | 319 else: |
| 331 TestError(subtest, 'Duplicate expectation path.')) | 320 subtests = [root_test] |
| 332 else: | |
| 333 if test_path is not None: | |
| 334 paths_seen.add(test_path) | |
| 335 name = subtest.name | |
| 336 if not neg_matcher.match(name) and matcher.match(name): | |
| 337 ok_tests.append(subtest) | |
| 338 | 321 |
| 339 if ok_tests: | 322 for subtest in subtests: |
| 340 seen_tests = True | 323 if not isinstance(subtest, Test): |
| 341 yield root_test.restrict(ok_tests) | 324 result_queue.put_nowait( |
| 325 UnknownError( | |
| 326 'Got non-[Multi]Test isinstance from generator: %r.' | |
| 327 % subtest)) | |
| 328 continue | |
| 329 | |
| 330 test_path = subtest.expect_path() | |
| 331 if test_path is not None and test_path in paths_seen: | |
| 332 result_queue.put_nowait( | |
| 333 TestError(subtest, 'Duplicate expectation path.')) | |
| 334 else: | |
| 335 if test_path is not None: | |
| 336 paths_seen.add(test_path) | |
| 337 name = subtest.name | |
| 338 # if not neg_matcher.match(name) and matcher.match(name): | |
| 339 if matcher.match(name): | |
| 340 ok_tests.append(subtest) | |
| 341 | |
| 342 if ok_tests: | |
| 343 seen_tests = True | |
| 344 yield root_test.restrict(ok_tests) | |
| 342 | 345 |
| 343 if not seen_tests: | 346 if not seen_tests: |
| 344 result_queue.put_nowait(NoMatchingTestsError()) | 347 result_queue.put_nowait(NoMatchingTestsError()) |
| 348 | |
| 345 except KeyboardInterrupt: | 349 except KeyboardInterrupt: |
| 346 pass | 350 pass |
| 347 | 351 |
| 348 next_stage = (result_queue if opts.handler.SKIP_RUNLOOP else test_queue) | 352 next_stage = (result_queue if opts.handler.SKIP_RUNLOOP else test_queue) |
| 349 opts.handler.gen_stage_loop(opts, generate_tests(), next_stage.put_nowait, | 353 opts.handler.gen_stage_loop(opts, generate_tests(), next_stage.put_nowait, |
| 350 result_queue.put_nowait) | 354 result_queue.put_nowait) |
| 351 | 355 |
| 352 | 356 |
| 353 def run_loop_process(test_queue, result_queue, opts, | 357 def run_loop_process(test_queue, result_queue, opts, |
| 354 kill_switch, test_gen_finished, | 358 kill_switch, test_gen_finished, |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 432 logstream.getvalue().splitlines())) | 436 logstream.getvalue().splitlines())) |
| 433 except KeyboardInterrupt: | 437 except KeyboardInterrupt: |
| 434 pass | 438 pass |
| 435 | 439 |
| 436 opts.handler.run_stage_loop( | 440 opts.handler.run_stage_loop( |
| 437 opts, | 441 opts, |
| 438 generate_tests_results(opts.handler.run_stage_loop_ctx), | 442 generate_tests_results(opts.handler.run_stage_loop_ctx), |
| 439 result_queue.put_nowait) | 443 result_queue.put_nowait) |
| 440 | 444 |
| 441 | 445 |
| 442 def result_loop_single_path(cover_ctx, kill_switch, result_queue, opts, | 446 def result_loop_single_context(cover_ctx, kill_switch, result_queue, opts, |
| 443 path, path_is_package): | 447 processing_context): |
| 444 """Run the specified operation on a single path. | 448 """Run the specified operation on a single path. |
| 445 | 449 |
| 446 The path provided by the `path` argument is considered to be either a Python | 450 The path provided by the `path` argument is considered to be either a Python |
| 447 package or a directory containing Python packages depending on the value of | 451 package or a directory containing Python packages depending on the value of |
| 448 the `path_is_package` flag. | 452 the `path_is_package` flag. |
| 449 | 453 |
| 450 The current working directory is changed (`os.chdir`) to either `path` or the | 454 The current working directory is changed (`os.chdir`) to either `path` or the |
| 451 parent of `path` whether `path_is_package` is False or True respectively. | 455 parent of `path` whether `path_is_package` is False or True respectively. |
| 452 | 456 |
| 453 This function is meant to be run as a dedicated process. Calling it twice | 457 This function is meant to be run as a dedicated process. Calling it twice |
| 454 in the same process is not supported. | 458 in the same process is not supported. |
| 455 | 459 |
| 456 Args: | 460 Args: |
| 457 cover_ctx: | 461 cover_ctx: |
| 458 kill_switch (multiprocessing.Event): | 462 kill_switch (multiprocessing.Event): |
| 459 result_queue (multiprocessing.Queue): | 463 result_queue (multiprocessing.Queue): |
| 460 opts: output of argparse.ArgumentParser.parse_args (see main.py) | 464 opts: output of argparse.ArgumentParser.parse_args (see main.py) |
| 461 path (str): path a a Python package or a directory containing Python | 465 processing_context (ProcessingContext): the task to perform. |
| 462 packages. | 466 """ |
| 463 path_is_package (boolean): tells whether 'path' is a package or not. | 467 sys.path.insert(0, processing_context.cwd) |
| 464 """ | |
| 465 assert isinstance(path, basestring), 'path must be a string' | |
| 466 | |
| 467 if path_is_package: | |
| 468 work_path = os.path.dirname(os.path.abspath(path)) | |
| 469 else: | |
| 470 work_path = path | |
| 471 | |
| 472 with cover_ctx: | |
| 473 if path_is_package: | |
| 474 test_gens = get_test_gens_package(path, work_path) | |
| 475 else: | |
| 476 test_gens = get_test_gens_directory(path, work_path) | |
| 477 | 468 |
| 478 # This flag is set when test generation has finished. | 469 # This flag is set when test generation has finished. |
| 479 test_gen_finished = multiprocessing.Event() | 470 test_gen_finished = multiprocessing.Event() |
| 480 test_queue = multiprocessing.Queue() | 471 test_queue = multiprocessing.Queue() |
| 481 | 472 |
| 482 with TempDir() as temp_dir: | 473 with TempDir() as temp_dir: |
| 483 test_gen_args = ( | 474 test_gen_args = ( |
| 484 test_gens, test_queue, result_queue, opts, | 475 processing_context.testing_contexts, test_queue, result_queue, opts, |
| 485 kill_switch, cover_ctx, temp_dir | 476 kill_switch, cover_ctx, temp_dir |
| 486 ) | 477 ) |
| 487 | 478 |
| 488 procs = [] | 479 procs = [] |
| 489 if opts.handler.SKIP_RUNLOOP: | 480 if opts.handler.SKIP_RUNLOOP: |
| 490 gen_loop_process(*test_gen_args) | 481 gen_loop_process(*test_gen_args) |
| 491 else: | 482 else: |
| 492 procs = [ | 483 procs = [ |
| 493 multiprocessing.Process( | 484 multiprocessing.Process( |
| 494 target=run_loop_process, | 485 target=run_loop_process, |
| 495 args=(test_queue, result_queue, opts, | 486 args=(test_queue, result_queue, opts, |
| 496 kill_switch, test_gen_finished, cover_ctx, temp_dir, | 487 kill_switch, test_gen_finished, cover_ctx, temp_dir, |
| 497 work_path), | 488 processing_context.cwd), |
| 498 name='run_loop_process %d' % job_num) | 489 name='run_loop_process %d' % job_num) |
| 499 for job_num in xrange(opts.jobs) | 490 for job_num in xrange(opts.jobs) |
| 500 ] | 491 ] |
| 501 | 492 |
| 502 for p in procs: | 493 for p in procs: |
| 503 p.daemon = True | 494 p.daemon = True |
| 504 p.start() | 495 p.start() |
| 505 | 496 |
| 506 gen_loop_process(*test_gen_args) | 497 gen_loop_process(*test_gen_args) |
| 507 # Signal all run_loop_process that they can exit. | 498 # Signal all run_loop_process that they can exit. |
| 508 test_gen_finished.set() | 499 test_gen_finished.set() |
| 509 | 500 |
| 510 for p in procs: | 501 for p in procs: |
| 511 p.join() | 502 p.join() |
| 512 | 503 |
| 513 | 504 |
| 505 def parse_test_glob(test_glob): | |
| 506 """A test glob is composed of a path and a glob expression like: | |
| 507 '<path>:<glob>'. The path should point to a directory or a file inside | |
| 508 a Python package (it can be the root directory of that package). | |
| 509 The glob is a Python name used to filter tests. | |
| 510 | |
| 511 Example: | |
| 512 'my/nice/package/test1/:TestA*', the package root being 'my/nice/package': | |
| 513 this matches all tests whose name starts with 'TestA' inside all files | |
| 514 matching test1/*_test.py. | |
| 515 | |
| 516 Args: | |
| 517 test_glob (str): a test glob | |
| 518 Returns: | |
| 519 (path, test_filter): absolute path and test filter glob. | |
| 520 """ | |
| 521 parts = test_glob.split(':') | |
| 522 if len(parts) > 2: | |
| 523 raise ValueError('A test_glob should contain at most one colon (got %s)' | |
| 524 % test_glob) | |
| 525 if len(parts) == 2: | |
| 526 path, test_filter = parts | |
| 527 if '/' in test_filter: | |
| 528 raise ValueError('A test filter cannot contain a slash (got %s)', | |
| 529 test_filter) | |
| 530 | |
| 531 if not test_filter: # empty string case | |
| 532 test_filter = '*' | |
| 533 else: | |
| 534 path, test_filter = parts[0], '*' | |
| 535 | |
| 536 path = os.path.abspath(path) | |
| 537 return path, test_filter | |
| 538 | |
| 539 | |
| 540 class PackageTestingContext(object): | |
| 541 def __init__(self, cwd, package_name, filters): | |
| 542 """Information to run a set of tests in a single package. | |
| 543 | |
| 544 See also parse_test_glob. | |
| 545 """ | |
| 546 self.cwd = cwd | |
| 547 self.package_name = package_name | |
| 548 # list of (path, filter) pairs. | |
| 549 # The path is a relative path to a subdirectory of | |
| 550 # os.path.join(self.cwd, self.package_name) where to look for tests for. | |
|
agable
2014/11/12 03:15:10
"in which to look for tests"
pgervais
2014/11/13 00:28:26
Done. Thanks for the English review!
| |
| 551 # Only tests whose name matches the glob are kept. | |
| 552 self.filters = filters | |
| 553 | |
| 554 def itermatchers(self): | |
| 555 """Iterate over all filters, and yield matchers for each of them. | |
| 556 | |
| 557 Returns: | |
|
agable
2014/11/12 03:15:09
"Returns" isn't exactly accurate for a generator.
pgervais
2014/11/13 00:28:26
Done.
| |
| 558 path (str): restrict test listing to this subpackage. | |
| 559 matcher (SRE_Pattern): whitelist matcher | |
| 560 """ | |
| 561 for filt in self.filters: | |
| 562 # Implicitely surround globs with * | |
| 563 | |
| 564 one_glob = '%s%s' % (filt[1], '*' if '*' not in filt[1] else '') | |
| 565 matcher = re.compile('^(?:%s)$' % glob.fnmatch.translate(one_glob)) | |
| 566 | |
| 567 ## matcher = re.compile( | |
|
agable
2014/11/12 03:15:09
Not sure what these commented-out segments are/wer
iannucci
2014/11/12 20:26:24
I think they were copied from above and then trans
pgervais
2014/11/13 00:28:26
Removed them.
| |
| 568 ## '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g) | |
| 569 ## for g in globs if g[0] != '-')) | |
| 570 if matcher.pattern == '^$': | |
| 571 matcher = re.compile('^.*$') | |
| 572 | |
| 573 ## neg_matcher = re.compile( | |
| 574 ## '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g[1:]) | |
| 575 ## for g in globs if g[0] == '-')) | |
| 576 yield filt[0], matcher | |
| 577 | |
| 578 @classmethod | |
| 579 def from_path(cls, path, filters='*'): | |
| 580 path = os.path.abspath(path) | |
| 581 if not os.path.exists(path): | |
| 582 raise ValueError('Path does not exist: %s' % path) | |
| 583 cwd = get_python_root(path) | |
| 584 package_name = os.path.relpath(path, cwd).split(os.path.sep)[0] | |
| 585 # list of (path, filter) pairs. | |
|
agable
2014/11/12 03:15:09
This comment is out of place. I think it belongs a
pgervais
2014/11/13 00:28:26
Dropped
| |
| 586 # The path is where to look for tests for. Only tests whose name matches the | |
|
agable
2014/11/12 03:15:09
"where to look for tests for" again
pgervais
2014/11/13 00:28:26
Done.
| |
| 587 # glob are kept. | |
| 588 relpath = os.path.relpath(path, os.path.join(cwd, package_name)) | |
| 589 | |
| 590 if isinstance(filters, basestring): | |
| 591 filters = [(relpath, filters)] | |
| 592 else: | |
| 593 filters = [(relpath, filt) for filt in filters] | |
| 594 | |
| 595 return cls(cwd, package_name, filters) | |
| 596 | |
| 597 @classmethod | |
| 598 def from_context_list(cls, contexts): | |
| 599 """Merge several PackageTestingContext pointing to the same package.""" | |
| 600 cwd = set(context.cwd for context in contexts) | |
| 601 assert len(cwd) == 1, \ | |
|
agable
2014/11/12 03:15:10
Not clear why here you use assert, while above and
pgervais
2014/11/13 00:28:26
Good catch. Fixed.
| |
| 602 'from_context_list processes contexts with the same working '\ | |
|
agable
2014/11/12 03:15:09
See comment below; this should be something like '
pgervais
2014/11/13 00:28:26
Done.
| |
| 603 'directory only.' | |
| 604 | |
| 605 package_name = set(context.package_name for context in contexts) | |
| 606 assert len(package_name) == 1, \ | |
| 607 'from_context_list processes contexts with the same package '\ | |
| 608 'name only.' | |
| 609 | |
| 610 filters = [] | |
| 611 for context in contexts: | |
| 612 filters.extend(context.filters) | |
| 613 | |
| 614 return cls(cwd.pop(), package_name.pop(), filters) | |
| 615 | |
| 616 | |
| 617 class ProcessingContext(object): | |
| 618 def __init__(self, testing_contexts): | |
| 619 """Information to run a set of tasks in a given working directory. | |
| 620 | |
| 621 Args: | |
| 622 testing_contexts (list): list of PackageTestingContext instances | |
| 623 """ | |
| 624 self.cwd = testing_contexts[0].cwd | |
| 625 | |
| 626 # Merge testing_contexts by package | |
| 627 groups = {} | |
| 628 for context in testing_contexts: | |
| 629 if context.cwd != self.cwd: | |
| 630 raise ValueError('All package must have the same value for "cwd"') | |
|
agable
2014/11/12 03:15:10
I don't think that ValueError is the right thing t
pgervais
2014/11/13 00:28:26
I've fixed the error messages in the above two pla
| |
| 631 groups.setdefault(context.package_name, []).append(context) | |
| 632 | |
| 633 self.testing_contexts = [PackageTestingContext.from_context_list(contexts) | |
| 634 for contexts in groups.itervalues()] | |
| 635 | |
| 636 | |
| 637 def get_runtime_contexts(test_globs): | |
| 638 """Compute the list of packages/filters to get tests from.""" | |
| 639 # Step 1: compute list of packages + subtree | |
| 640 testing_contexts = [] | |
| 641 for test_glob in test_globs: | |
| 642 path, test_filter = parse_test_glob(test_glob) | |
| 643 if os.path.exists(os.path.join(path, '__init__.py')): | |
| 644 testing_contexts.append( | |
| 645 PackageTestingContext.from_path(path, test_filter)) | |
| 646 else: | |
| 647 # Look for all packages in path. | |
| 648 subpaths = [] | |
| 649 black_list = get_config(path) | |
| 650 | |
| 651 for filename in filter(lambda x: x not in black_list, os.listdir(path)): | |
| 652 abs_filename = os.path.join(path, filename) | |
| 653 if (os.path.isdir(abs_filename) | |
| 654 and os.path.isfile(os.path.join(abs_filename, '__init__.py'))): | |
| 655 subpaths.append(abs_filename) | |
| 656 | |
| 657 testing_contexts.extend( | |
| 658 [PackageTestingContext.from_path(subpath, test_filter) | |
| 659 for subpath in subpaths]) | |
| 660 | |
| 661 # Step 2: group by working directory - one process per wd. | |
| 662 groups = {} | |
| 663 for context in testing_contexts: | |
| 664 groups.setdefault(context.cwd, []).append(context) | |
| 665 return [ProcessingContext(contexts) for contexts in groups.itervalues()] | |
| 666 | |
| 667 | |
| 514 def result_loop(cover_ctx, opts): | 668 def result_loop(cover_ctx, opts): |
| 515 """Run the specified operation in all paths in parallel. | 669 """Run the specified operation in all paths in parallel. |
| 516 | 670 |
| 517 Directories and packages to process are defined in opts.directory and | 671 Directories and packages to process are defined in opts.directory and |
| 518 opts.package. | 672 opts.package. |
| 519 | 673 |
| 520 The operation to perform (list/test/debug/train) is defined by opts.handler. | 674 The operation to perform (list/test/debug/train) is defined by opts.handler. |
| 521 """ | 675 """ |
| 522 | 676 |
| 677 processing_contexts = get_runtime_contexts(opts.test_glob) | |
| 678 | |
| 523 def ensure_echo_on(): | 679 def ensure_echo_on(): |
| 524 """Restore echo on in the terminal. | 680 """Restore echo on in the terminal. |
| 525 | 681 |
| 526 This is useful when killing a pdb session with C-c. | 682 This is useful when killing a pdb session with C-c. |
| 527 """ | 683 """ |
| 528 try: | 684 try: |
| 529 import termios | 685 import termios |
| 530 except ImportError: | 686 except ImportError: |
| 531 termios = None | 687 termios = None |
| 532 if termios: | 688 if termios: |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 549 signal.signal(signal.SIGINT, signal.SIG_DFL) | 705 signal.signal(signal.SIGINT, signal.SIG_DFL) |
| 550 signal.signal(signal.SIGTERM, signal.SIG_DFL) | 706 signal.signal(signal.SIGTERM, signal.SIG_DFL) |
| 551 | 707 |
| 552 signal.signal(signal.SIGINT, handle_killswitch) | 708 signal.signal(signal.SIGINT, handle_killswitch) |
| 553 signal.signal(signal.SIGTERM, handle_killswitch) | 709 signal.signal(signal.SIGTERM, handle_killswitch) |
| 554 | 710 |
| 555 result_queue = multiprocessing.Queue() | 711 result_queue = multiprocessing.Queue() |
| 556 | 712 |
| 557 procs = [ | 713 procs = [ |
| 558 multiprocessing.Process( | 714 multiprocessing.Process( |
| 559 target=result_loop_single_path, | 715 target=result_loop_single_context, |
| 560 args=(cover_ctx, kill_switch, result_queue, opts, os.path.abspath(p), | 716 args=(cover_ctx, kill_switch, result_queue, opts, c) |
| 561 False) | |
| 562 ) | 717 ) |
| 563 for p in opts.directory | 718 for c in processing_contexts |
| 564 ] + [ | |
| 565 multiprocessing.Process( | |
| 566 target=result_loop_single_path, | |
| 567 args=(cover_ctx, kill_switch, result_queue, opts, os.path.abspath(p), | |
| 568 True) | |
| 569 ) | |
| 570 for p in opts.package | |
| 571 ] | 719 ] |
| 572 | 720 |
| 573 error = False | 721 error = False |
| 574 | 722 |
| 575 try: | 723 try: |
| 576 def generate_objects(procs): | 724 def generate_objects(procs): |
| 577 for p in procs: | 725 for p in procs: |
| 578 p.start() | 726 p.start() |
| 579 while not kill_switch.is_set(): | 727 while not kill_switch.is_set(): |
| 580 try: | 728 try: |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 603 | 751 |
| 604 if procs: | 752 if procs: |
| 605 error = opts.handler.result_stage_loop(opts, generate_objects(procs)) | 753 error = opts.handler.result_stage_loop(opts, generate_objects(procs)) |
| 606 except ResultStageAbort: | 754 except ResultStageAbort: |
| 607 pass | 755 pass |
| 608 | 756 |
| 609 if not kill_switch.is_set() and not result_queue.empty(): | 757 if not kill_switch.is_set() and not result_queue.empty(): |
| 610 error = True | 758 error = True |
| 611 | 759 |
| 612 return error, kill_switch.is_set() | 760 return error, kill_switch.is_set() |
| OLD | NEW |