Index: expect_tests/pipeline.py |
diff --git a/expect_tests/pipeline.py b/expect_tests/pipeline.py |
index 04da90309c81da86e7ef537d18c3d6e82ec0c68d..bb399c463dbfa35f3a9b2c92bf96c7a4cb30fd7b 100644 |
--- a/expect_tests/pipeline.py |
+++ b/expect_tests/pipeline.py |
@@ -3,15 +3,12 @@ |
# found in the LICENSE file. |
import contextlib |
-import ConfigParser |
-import glob |
import imp |
import inspect |
import logging |
import multiprocessing |
import os |
import Queue |
-import re |
import signal |
import sys |
import tempfile |
@@ -26,9 +23,7 @@ from expect_tests.type_definitions import ( |
from expect_tests.unittest_helper import _is_unittest, UnittestTestCase |
from expect_tests import util |
- |
- |
-CONFIG_FILE_NAME = '.expect_tests.cfg' |
+from expect_tests import listing |
@contextlib.contextmanager |
@@ -70,38 +65,6 @@ def get_package_path(package_name, path): |
return os.path.normpath(package_path) if ispkg else None |
-def get_config(path): |
- """Get configuration values |
- |
- Reads the config file in provided path, and returns content. |
- See Python ConfigParser for general formatting syntax. |
- |
- Example: |
- [expect_tests] |
- skip=directory1 |
- directory2 |
- directory3 |
- |
- Args: |
- path (str): path to a directory. |
- |
- Returns: |
- black_list (set): blacklisted subdirectories. |
- """ |
- black_list = set() |
- |
- config_file_name = os.path.join(path, CONFIG_FILE_NAME) |
- parser = ConfigParser.ConfigParser() |
- parser.read([config_file_name]) |
- |
- if not parser.has_section('expect_tests'): |
- return black_list |
- |
- if parser.has_option('expect_tests', 'skip'): |
- black_list.update(parser.get('expect_tests', 'skip').splitlines()) |
- return black_list |
- |
- |
def is_test_file(filename): |
"""Returns True if filename is supposed to contain tests. |
@@ -114,7 +77,7 @@ def is_test_file(filename): |
return filename.endswith('_test.py') |
-def walk_package(package_name, path): |
+def walk_package(package_name, path, subpath=None): |
"""Return all test files inside a single package. |
In all cases, this function returns the full package name of files ending |
@@ -139,6 +102,8 @@ def walk_package(package_name, path): |
Args: |
package_name (str): name of the package, as expected by import. |
path (str): path containing the above module (optional) |
+ subpath (str, optional): path inside the package, pointing to a subpackage. |
+ This is used to restrict the listing to part of the package. |
Returns: |
test_modules (list of str): name of modules containing tests. Each element is |
@@ -164,9 +129,16 @@ def walk_package(package_name, path): |
explored = set() |
- for dirpath, dirnames, filenames in os.walk(package_path, followlinks=True): |
+ if subpath: |
+ start_path = os.path.join(package_path, subpath) |
+ if not os.path.exists(start_path): |
+ raise ValueError('Provided subpath does not exist: %s' % start_path) |
+ else: |
+ start_path = package_path |
+ |
+ for dirpath, dirnames, filenames in os.walk(start_path, followlinks=True): |
# Keep only submodules not blacklisted, break symlink cycles |
- blacklist = get_config(dirpath) |
+ blacklist = listing.get_config(dirpath) |
dirnames[:] = [d for d in dirnames |
if d not in blacklist and |
os.path.isfile(os.path.join(dirpath, d, '__init__.py')) and |
@@ -174,7 +146,7 @@ def walk_package(package_name, path): |
realpaths = [os.path.realpath(os.path.join(dirpath, d)) for d in dirnames] |
explored.update(realpaths) |
- assert dirpath.startswith(package_path) |
+ assert dirpath.startswith(start_path) |
base_module_name = os.path.relpath(dirpath, base_path).split(os.path.sep) |
test_modules.extend(['.'.join(base_module_name |
+ [inspect.getmodulename(filename)]) |
@@ -199,32 +171,8 @@ def load_module(modname): |
return mod |
-def get_test_gens_directory(path, cwd): |
- """Given a path, return list of MultiTest or Test instances. |
- |
- See UnittestTestCase for possible return values. |
- |
- This function loads modules, thus no two conflicting packages (like appengine) |
- can be loaded at the same time: use separate processes for that. |
- """ |
- assert isinstance(path, basestring), 'path must be a string' |
- assert os.path.isdir(path), 'path is not a directory: %s' % path |
- sys.path.insert(0, os.path.abspath(path)) |
- |
- test_gens = [] |
- black_list = get_config(path) |
- |
- for filename in filter(lambda x: x not in black_list, os.listdir(path)): |
- abs_filename = os.path.join(path, filename) |
- if (os.path.isdir(abs_filename) |
- and os.path.isfile(os.path.join(abs_filename, '__init__.py'))): |
- test_gens += get_test_gens_package(abs_filename, cwd, |
- update_syspath=False) |
- return test_gens |
- |
- |
-def get_test_gens_package(package, cwd, update_syspath=True): |
- """Given a path, return list of MultiTest or Test instances. |
+def get_test_gens_package(testing_context, subpath=None): |
+ """Given a testing context, return list of generators of *Test instances. |
See UnittestTestCase for possible return values. |
@@ -232,24 +180,21 @@ def get_test_gens_package(package, cwd, update_syspath=True): |
should be loaded at the same time: use separate processes for that. |
Args: |
- package (str): path to a Python package. |
- update_syspath (boolean): if True, the parent directory of 'package' is |
- prepended to sys.path. |
- """ |
- assert isinstance(package, basestring), "package name should be a string." |
- assert os.path.isfile(os.path.join(package, '__init__.py')), \ |
- "'package' is not pointing to a package. It must be a " + \ |
- "path to a directory containing a __init__.py file." |
+ testing_context (PackageTestingContext): what to test. |
+ subpath (str): relative path in the tested package to restrict the search |
+ to. Relative to |
+ os.path.join(testing_context.cwd, testing_context.package_name) |
+ Returns: |
+ gens_list (list of generator of tests): tests are instances of Test |
+ or MultiTest. |
+ """ |
test_gens = [] |
- path = os.path.abspath(os.path.dirname(package)) |
- if update_syspath: |
- sys.path.insert(0, path) |
- |
- package_name = os.path.split(package.rstrip(os.path.sep))[-1] |
- for modname in walk_package(package_name, path): |
- with use_chdir(cwd): |
+ # TODO(pgervais) add filtering on test names (use testing_context.filters) |
+ for modname in walk_package(testing_context.package_name, |
+ testing_context.cwd, subpath=subpath): |
+ with use_chdir(testing_context.cwd): |
mod = load_module(modname) |
for obj in mod.__dict__.values(): |
if util.is_test_generator(obj): |
@@ -259,89 +204,85 @@ def get_test_gens_package(package, cwd, update_syspath=True): |
return test_gens |
-def gen_loop_process(gens, test_queue, result_queue, opts, kill_switch, |
- cover_ctx, temp_dir): |
+def gen_loop_process(testing_contexts, test_queue, result_queue, opts, |
+ kill_switch, cover_ctx, temp_dir): |
"""Generate `Test`s from |gens|, and feed them into |test_queue|. |
Non-Test instances will be translated into `UnknownError` objects. |
Args: |
- gens: list of generators yielding Test() instances. |
+ testing_contexts (list of PackageTestingContext): describe tests to |
+ process. |
test_queue (multiprocessing.Queue): |
result_queue (multiprocessing.Queue): |
opts (argparse.Namespace): |
kill_switch (multiprocessing.Event): |
cover_ctx (cover.CoverageContext().create_subprocess_context) |
""" |
- tempfile.tempdir = temp_dir |
- |
- # Implicitly append '*' to globs that don't specify it. |
- globs = ['%s%s' % (g, '*' if '*' not in g else '') for g in opts.test_glob] |
- |
- matcher = re.compile( |
- '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g) |
- for g in globs if g[0] != '-')) |
- if matcher.pattern == '^$': |
- matcher = re.compile('^.*$') |
- |
- neg_matcher = re.compile( |
- '^%s$' % '|'.join('(?:%s)' % glob.fnmatch.translate(g[1:]) |
- for g in globs if g[0] == '-')) |
SENTINEL = object() |
+ tempfile.tempdir = temp_dir |
def generate_tests(): |
- paths_seen = set() |
seen_tests = False |
try: |
- for gen in gens: |
- gen_cover_ctx = cover_ctx(include=util.get_cover_list(gen)) |
- |
- with gen_cover_ctx: |
- gen_inst = gen() |
- |
- while not kill_switch.is_set(): |
- with gen_cover_ctx: |
- root_test = next(gen_inst, SENTINEL) |
- |
- if root_test is SENTINEL: |
- break |
- |
- if kill_switch.is_set(): |
- break |
- |
- ok_tests = [] |
- |
- if isinstance(root_test, MultiTest): |
- subtests = root_test.tests |
- else: |
- subtests = [root_test] |
- |
- for subtest in subtests: |
- if not isinstance(subtest, Test): |
- result_queue.put_nowait( |
- UnknownError( |
- 'Got non-[Multi]Test isinstance from generator: %r.' |
- % subtest)) |
- continue |
- |
- test_path = subtest.expect_path() |
- if test_path is not None and test_path in paths_seen: |
- result_queue.put_nowait( |
- TestError(subtest, 'Duplicate expectation path.')) |
- else: |
- if test_path is not None: |
- paths_seen.add(test_path) |
- name = subtest.name |
- if not neg_matcher.match(name) and matcher.match(name): |
- ok_tests.append(subtest) |
- |
- if ok_tests: |
- seen_tests = True |
- yield root_test.restrict(ok_tests) |
+ for testing_context in testing_contexts: |
+ for subpath, matcher in testing_context.itermatchers(): |
+ paths_seen = set() |
+ |
+ with cover_ctx: |
+ gens = get_test_gens_package(testing_context, subpath=subpath) |
+ |
+ for gen in gens: |
+ gen_cover_ctx = cover_ctx(include=util.get_cover_list(gen)) |
+ |
+ with gen_cover_ctx: |
+ gen_inst = gen() |
+ |
+ while not kill_switch.is_set(): |
+ with gen_cover_ctx: |
+ root_test = next(gen_inst, SENTINEL) |
+ |
+ if root_test is SENTINEL: |
+ break |
+ |
+ if kill_switch.is_set(): |
+ break |
+ |
+ ok_tests = [] |
+ |
+ if isinstance(root_test, MultiTest): |
+ subtests = root_test.tests |
+ else: |
+ subtests = [root_test] |
+ |
+ for subtest in subtests: |
+ if not isinstance(subtest, Test): |
+ result_queue.put_nowait( |
+ UnknownError( |
+ 'Got non-[Multi]Test isinstance from generator: %r.' |
+ % subtest)) |
+ continue |
+ |
+ test_path = subtest.expect_path() |
+ if test_path is not None and test_path in paths_seen: |
+ result_queue.put_nowait( |
+ TestError(subtest, 'Duplicate expectation path.')) |
+ else: |
+ if test_path is not None: |
+ paths_seen.add(test_path) |
+ name = subtest.name |
+ # if not neg_matcher.match(name) and matcher.match(name): |
+ if matcher.match(name): |
+ ok_tests.append(subtest) |
+ |
+ if ok_tests: |
+ seen_tests = True |
+ yield root_test.restrict(ok_tests) |
if not seen_tests: |
result_queue.put_nowait(NoMatchingTestsError()) |
+ |
except KeyboardInterrupt: |
pass |
@@ -439,8 +380,8 @@ def run_loop_process(test_queue, result_queue, opts, |
result_queue.put_nowait) |
-def result_loop_single_path(cover_ctx, kill_switch, result_queue, opts, |
- path, path_is_package): |
+def result_loop_single_context(cover_ctx, kill_switch, result_queue, opts, |
+ processing_context): |
"""Run the specified operation on a single path. |
The path provided by the `path` argument is considered to be either a Python |
@@ -458,22 +399,9 @@ def result_loop_single_path(cover_ctx, kill_switch, result_queue, opts, |
kill_switch (multiprocessing.Event): |
result_queue (multiprocessing.Queue): |
opts: output of argparse.ArgumentParser.parse_args (see main.py) |
- path (str): path a a Python package or a directory containing Python |
- packages. |
- path_is_package (boolean): tells whether 'path' is a package or not. |
- """ |
- assert isinstance(path, basestring), 'path must be a string' |
- |
- if path_is_package: |
- work_path = os.path.dirname(os.path.abspath(path)) |
- else: |
- work_path = path |
- |
- with cover_ctx: |
- if path_is_package: |
- test_gens = get_test_gens_package(path, work_path) |
- else: |
- test_gens = get_test_gens_directory(path, work_path) |
+ processing_context (ProcessingContext): the task to perform. |
+ """ |
+ sys.path.insert(0, processing_context.cwd) |
# This flag is set when test generation has finished. |
test_gen_finished = multiprocessing.Event() |
@@ -481,7 +409,7 @@ def result_loop_single_path(cover_ctx, kill_switch, result_queue, opts, |
with TempDir() as temp_dir: |
test_gen_args = ( |
- test_gens, test_queue, result_queue, opts, |
+ processing_context.testing_contexts, test_queue, result_queue, opts, |
kill_switch, cover_ctx, temp_dir |
) |
@@ -494,7 +422,7 @@ def result_loop_single_path(cover_ctx, kill_switch, result_queue, opts, |
target=run_loop_process, |
args=(test_queue, result_queue, opts, |
kill_switch, test_gen_finished, cover_ctx, temp_dir, |
- work_path), |
+ processing_context.cwd), |
name='run_loop_process %d' % job_num) |
for job_num in xrange(opts.jobs) |
] |
@@ -520,6 +448,8 @@ def result_loop(cover_ctx, opts): |
The operation to perform (list/test/debug/train) is defined by opts.handler. |
""" |
+ processing_contexts = listing.get_runtime_contexts(opts.test_glob) |
+ |
def ensure_echo_on(): |
"""Restore echo on in the terminal. |
@@ -556,18 +486,10 @@ def result_loop(cover_ctx, opts): |
procs = [ |
multiprocessing.Process( |
- target=result_loop_single_path, |
- args=(cover_ctx, kill_switch, result_queue, opts, os.path.abspath(p), |
- False) |
- ) |
- for p in opts.directory |
- ] + [ |
- multiprocessing.Process( |
- target=result_loop_single_path, |
- args=(cover_ctx, kill_switch, result_queue, opts, os.path.abspath(p), |
- True) |
+ target=result_loop_single_context, |
+ args=(cover_ctx, kill_switch, result_queue, opts, c) |
) |
- for p in opts.package |
+ for c in processing_contexts |
] |
error = False |