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 |