diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-07-27 18:46:20 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-07-27 18:46:20 -0400 |
| commit | 54592942c4a9c3d6d891519082555f8081026445 (patch) | |
| tree | 29abeff4dee0960284e03558ff464ffbf41cc784 /lib/sqlalchemy/testing | |
| parent | 35551841c522d8eb20f8e20243a5510de9d95dfc (diff) | |
| download | sqlalchemy-54592942c4a9c3d6d891519082555f8081026445.tar.gz | |
- add support for tags, including include/exclude support.
simplify tox again now that we can exclude tests more easily
Diffstat (limited to 'lib/sqlalchemy/testing')
| -rw-r--r-- | lib/sqlalchemy/testing/config.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/exclusions.py | 34 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/noseplugin.py | 13 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/plugin_base.py | 113 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/plugin/pytestplugin.py | 9 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/requirements.py | 33 |
6 files changed, 168 insertions, 37 deletions
diff --git a/lib/sqlalchemy/testing/config.py b/lib/sqlalchemy/testing/config.py index b24483bb7..6832eab74 100644 --- a/lib/sqlalchemy/testing/config.py +++ b/lib/sqlalchemy/testing/config.py @@ -45,9 +45,10 @@ class Config(object): @classmethod def set_as_current(cls, config, namespace): - global db, _current, db_url, test_schema, test_schema_2 + global db, _current, db_url, test_schema, test_schema_2, db_opts _current = config db_url = config.db.url + db_opts = config.db_opts test_schema = config.test_schema test_schema_2 = config.test_schema_2 namespace.db = db = config.db diff --git a/lib/sqlalchemy/testing/exclusions.py b/lib/sqlalchemy/testing/exclusions.py index f6ef72408..283d89e36 100644 --- a/lib/sqlalchemy/testing/exclusions.py +++ b/lib/sqlalchemy/testing/exclusions.py @@ -33,6 +33,7 @@ class compound(object): def __init__(self): self.fails = set() self.skips = set() + self.tags = set() def __add__(self, other): return self.add(other) @@ -41,15 +42,18 @@ class compound(object): copy = compound() copy.fails.update(self.fails) copy.skips.update(self.skips) + copy.tags.update(self.tags) for other in others: copy.fails.update(other.fails) copy.skips.update(other.skips) + copy.tags.update(other.tags) return copy def not_(self): copy = compound() copy.fails.update(NotPredicate(fail) for fail in self.fails) copy.skips.update(NotPredicate(skip) for skip in self.skips) + copy.tags.update(self.tags) return copy @property @@ -70,23 +74,29 @@ class compound(object): if predicate(config) ] + def include_test(self, include_tags, exclude_tags): + return bool( + not self.tags.intersection(exclude_tags) and + (not include_tags or self.tags.intersection(include_tags)) + ) + + def _extend(self, other): + self.skips.update(other.skips) + self.fails.update(other.fails) + self.tags.update(other.tags) + def __call__(self, fn): if hasattr(fn, '_sa_exclusion_extend'): - fn._sa_exclusion_extend(self) + fn._sa_exclusion_extend._extend(self) return fn - def extend(other): - self.skips.update(other.skips) - self.fails.update(other.fails) - @decorator def decorate(fn, *args, **kw): return self._do(config._current, fn, *args, **kw) decorated = decorate(fn) - decorated._sa_exclusion_extend = extend + decorated._sa_exclusion_extend = self return decorated - @contextlib.contextmanager def fail_if(self): all_fails = compound() @@ -144,6 +154,16 @@ class compound(object): ) +def requires_tag(tagname): + return tags([tagname]) + + +def tags(tagnames): + comp = compound() + comp.tags.update(tagnames) + return comp + + def only_if(predicate, reason=None): predicate = _as_predicate(predicate) return skip_if(NotPredicate(predicate), reason) diff --git a/lib/sqlalchemy/testing/plugin/noseplugin.py b/lib/sqlalchemy/testing/plugin/noseplugin.py index e362d6141..ac2248400 100644 --- a/lib/sqlalchemy/testing/plugin/noseplugin.py +++ b/lib/sqlalchemy/testing/plugin/noseplugin.py @@ -18,6 +18,7 @@ import sys from nose.plugins import Plugin fixtures = None +py3k = sys.version_info >= (3, 0) # no package imports yet! this prevents us from tripping coverage # too soon. path = os.path.join(os.path.dirname(__file__), "plugin_base.py") @@ -67,10 +68,14 @@ class NoseSQLAlchemy(Plugin): return "" def wantFunction(self, fn): - if fn.__module__ is None: - return False - if fn.__module__.startswith('sqlalchemy.testing'): - return False + return False + + def wantMethod(self, fn): + if py3k: + cls = fn.__self__.cls + else: + cls = fn.im_class + return plugin_base.want_method(cls, fn) def wantClass(self, cls): return plugin_base.want_class(cls) diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index 095e3f369..ec081af2b 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -49,6 +49,8 @@ file_config = None logging = None db_opts = {} +include_tags = set() +exclude_tags = set() options = None @@ -87,8 +89,13 @@ def setup_options(make_option): dest="cdecimal", default=False, help="Monkeypatch the cdecimal library into Python 'decimal' " "for all tests") - make_option("--serverside", action="callback", - callback=_server_side_cursors, + make_option("--include-tag", action="callback", callback=_include_tag, + type="string", + help="Include tests with tag <tag>") + make_option("--exclude-tag", action="callback", callback=_exclude_tag, + type="string", + help="Exclude tests with tag <tag>") + make_option("--serverside", action="store_true", help="Turn on server side cursors for PG") make_option("--mysql-engine", action="store", dest="mysql_engine", default=None, @@ -102,10 +109,46 @@ def setup_options(make_option): def configure_follower(follower_ident): + """Configure required state for a follower. + + This invokes in the parent process and typically includes + database creation. + + """ global FOLLOWER_IDENT FOLLOWER_IDENT = follower_ident +def memoize_important_follower_config(dict_): + """Store important configuration we will need to send to a follower. + + This invokes in the parent process after normal config is set up. + + This is necessary as py.test seems to not be using forking, so we + start with nothing in memory, *but* it isn't running our argparse + callables, so we have to just copy all of that over. + + """ + dict_['memoized_config'] = { + 'db_opts': db_opts, + 'include_tags': include_tags, + 'exclude_tags': exclude_tags + } + + +def restore_important_follower_config(dict_): + """Restore important configuration needed by a follower. + + This invokes in the follower process. + + """ + global db_opts, include_tags, exclude_tags + db_opts.update(dict_['memoized_config']['db_opts']) + include_tags.update(dict_['memoized_config']['include_tags']) + exclude_tags.update(dict_['memoized_config']['exclude_tags']) + print "EXCLUDE TAGS!!!!!", exclude_tags + + def read_config(): global file_config file_config = configparser.ConfigParser() @@ -141,7 +184,6 @@ def post_begin(): from sqlalchemy import util - def _log(opt_str, value, parser): global logging if not logging: @@ -161,14 +203,17 @@ def _list_dbs(*args): sys.exit(0) -def _server_side_cursors(opt_str, value, parser): - db_opts['server_side_cursors'] = True - - def _requirements_opt(opt_str, value, parser): _setup_requirements(value) +def _exclude_tag(opt_str, value, parser): + exclude_tags.add(value.replace('-', '_')) + + +def _include_tag(opt_str, value, parser): + include_tags.add(value.replace('-', '_')) + pre_configure = [] post_configure = [] @@ -183,7 +228,6 @@ def post(fn): return fn - @pre def _setup_options(opt, file_config): global options @@ -191,6 +235,12 @@ def _setup_options(opt, file_config): @pre +def _server_side_cursors(options, file_config): + if options.serverside: + db_opts['server_side_cursors'] = True + + +@pre def _monkeypatch_cdecimal(options, file_config): if options.cdecimal: import cdecimal @@ -199,8 +249,9 @@ def _monkeypatch_cdecimal(options, file_config): @post def _engine_uri(options, file_config): - from sqlalchemy.testing import engines, config + from sqlalchemy.testing import config from sqlalchemy import testing + from sqlalchemy.testing.plugin import provision if options.dburi: db_urls = list(options.dburi) @@ -221,8 +272,6 @@ def _engine_uri(options, file_config): if not db_urls: db_urls.append(file_config.get('db', 'default')) - from . import provision - for db_url in db_urls: cfg = provision.setup_config( db_url, db_opts, options, file_config, FOLLOWER_IDENT) @@ -230,10 +279,6 @@ def _engine_uri(options, file_config): if not config._current: cfg.set_as_current(cfg, testing) - config.db_opts = db_opts - - - @post def _engine_pool(options, file_config): @@ -361,6 +406,35 @@ def want_class(cls): return True +def want_method(cls, fn): + if cls.__name__ == 'PoolFirstConnectSyncTest' and fn.__name__ == 'test_sync': + assert exclude_tags + assert hasattr(fn, '_sa_exclusion_extend') + assert not fn._sa_exclusion_extend.include_test(include_tags, exclude_tags) + + if fn.__module__ is None: + return False + elif fn.__module__.startswith('sqlalchemy.testing'): + return False + elif include_tags: + return ( + hasattr(cls, '__tags__') and + exclusions.tags(cls.__tags__).include_test( + include_tags, exclude_tags) + ) or ( + hasattr(fn, '_sa_exclusion_extend') and + fn._sa_exclusion_extend.include_test( + include_tags, exclude_tags) + ) + elif exclude_tags and hasattr(cls, '__tags__'): + return exclusions.tags(cls.__tags__).include_test( + include_tags, exclude_tags) + elif exclude_tags and hasattr(fn, '_sa_exclusion_extend'): + return fn._sa_exclusion_extend.include_test(include_tags, exclude_tags) + else: + return fn.__name__.startswith("test_") + + def generate_sub_tests(cls, module): if getattr(cls, '__backend__', False): for cfg in _possible_configs_for_cls(cls): @@ -423,11 +497,13 @@ def after_test(test): def _possible_configs_for_cls(cls, reasons=None): all_configs = set(config.Config.all_configs()) + if cls.__unsupported_on__: spec = exclusions.db_spec(*cls.__unsupported_on__) for config_obj in list(all_configs): if spec(config_obj): all_configs.remove(config_obj) + if getattr(cls, '__only_on__', None): spec = exclusions.db_spec(*util.to_list(cls.__only_on__)) for config_obj in list(all_configs): @@ -459,13 +535,6 @@ def _possible_configs_for_cls(cls, reasons=None): if all_configs.difference(non_preferred): all_configs.difference_update(non_preferred) - for db_spec, op, spec in getattr(cls, '__excluded_on__', ()): - for config_obj in list(all_configs): - if not exclusions.skip_if( - exclusions.SpecPredicate(db_spec, op, spec) - ).enabled_for_config(config_obj): - all_configs.remove(config_obj) - return all_configs diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index 7671c800c..fd0616327 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -32,6 +32,7 @@ def pytest_addoption(parser): def pytest_configure(config): if hasattr(config, "slaveinput"): + plugin_base.restore_important_follower_config(config.slaveinput) plugin_base.configure_follower( config.slaveinput["follower_ident"] ) @@ -49,6 +50,9 @@ if has_xdist: def pytest_configure_node(node): # the master for each node fills slaveinput dictionary # which pytest-xdist will transfer to the subprocess + + plugin_base.memoize_important_follower_config(node.slaveinput) + node.slaveinput["follower_ident"] = "test_%s" % next(_follower_count) from . import provision provision.create_follower_db(node.slaveinput["follower_ident"]) @@ -100,12 +104,11 @@ def pytest_collection_modifyitems(session, config, items): def pytest_pycollect_makeitem(collector, name, obj): - if inspect.isclass(obj) and plugin_base.want_class(obj): return pytest.Class(name, parent=collector) elif inspect.isfunction(obj) and \ - name.startswith("test_") and \ - isinstance(collector, pytest.Instance): + isinstance(collector, pytest.Instance) and \ + plugin_base.want_method(collector.cls, obj): return pytest.Function(name, parent=collector) else: return [] diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index fbb0d63e2..a04bcbbdd 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -16,6 +16,7 @@ to provide specific inclusion/exclusions. """ from . import exclusions +from .. import util class Requirements(object): @@ -617,6 +618,38 @@ class SuiteRequirements(Requirements): return exclusions.skip_if( lambda config: config.options.low_connections) + @property + def timing_intensive(self): + return exclusions.requires_tag("timing_intensive") + + @property + def memory_intensive(self): + return exclusions.requires_tag("memory_intensive") + + @property + def threading_with_mock(self): + """Mark tests that use threading and mock at the same time - stability + issues have been observed with coverage + python 3.3 + + """ + return exclusions.skip_if( + lambda config: util.py3k and config.options.has_coverage, + "Stability issues with coverage + py3k" + ) + + @property + def no_coverage(self): + """Test should be skipped if coverage is enabled. + + This is to block tests that exercise libraries that seem to be + sensitive to coverage, such as Postgresql notice logging. + + """ + return exclusions.skip_if( + lambda config: config.options.has_coverage, + "Issues observed when coverage is enabled" + ) + def _has_mysql_on_windows(self, config): return False |
