Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(177)

Side by Side Diff: expect_tests/pipeline.py

Issue 709853003: New expect_tests UI (Closed) Base URL: https://chromium.googlesource.com/infra/testing/expect_tests@shebang
Patch Set: Filtering on the CLI works. Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
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()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698