]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- add support for tags, including include/exclude support.
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 27 Jul 2014 22:46:20 +0000 (18:46 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 27 Jul 2014 22:46:20 +0000 (18:46 -0400)
simplify tox again now that we can exclude tests more easily

14 files changed:
README.unittests.rst
lib/sqlalchemy/testing/config.py
lib/sqlalchemy/testing/exclusions.py
lib/sqlalchemy/testing/plugin/noseplugin.py
lib/sqlalchemy/testing/plugin/plugin_base.py
lib/sqlalchemy/testing/plugin/pytestplugin.py
lib/sqlalchemy/testing/requirements.py
test/aaa_profiling/test_memusage.py
test/dialect/postgresql/test_dialect.py
test/dialect/postgresql/test_types.py
test/engine/test_pool.py
test/requirements.py
test/sql/test_types.py
tox.ini

index 375d0737ce2ad3b0ddbfe8614182d20a371065cb..209eefe0dd43219878cb3c3f1e7ab4a97ab8124c 100644 (file)
@@ -305,12 +305,8 @@ Environments include::
 
     "full" - runs a full py.test
 
-    "coverage" - runs a full py.test plus coverage, minus memusage
-
-    "lightweight" - runs tests without the very heavy "memusage" tests, without
-    coverage.  Suitable running tests against pypy and for parallel testing.
-
-    "memusage" - runs only the memusage tests (very slow and heavy)
+    "coverage" - runs a py.test plus coverage, skipping memory/timing
+    intensive tests
 
     "pep8" - runs flake8 against the codebase (useful with --diff to check
     against a patch)
@@ -325,7 +321,7 @@ for the database should have CREATE DATABASE and DROP DATABASE privileges.
 After installing pytest-xdist, testing is run adding the -n<num> option.
 For example, to run against sqlite, mysql, postgresql with four processes::
 
-    tox -e lightweight -- -n 4 --db sqlite --db postgresql --db mysql
+    tox -e -- -n 4 --exclude-tags memory-intensive --db sqlite --db postgresql --db mysql
 
 Each backend has a different scheme for setting up the database.  Postgresql
 still needs the "test_schema" and "test_schema_2" schemas present, as the
index b24483bb72d77706a80e5fa19b9304e88b42fa6f..6832eab74c29477044f9a5c061884687b297e38f 100644 (file)
@@ -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
index f6ef72408c6c3787a9b51f8835371d04fe718840..283d89e3688ef8fd1e4c49a7b530bef72cf34b10 100644 (file)
@@ -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)
index e362d61417f903ce0ce693ec5fd61d4b6aa138fb..ac2248400bd6c7c17d53817b03813fc6b5cb69ea 100644 (file)
@@ -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)
index 095e3f3697e8535f40cab5a613fbddf165f1b8d1..ec081af2b56a9ea40ba57925a5f9663acc0c3da9 100644 (file)
@@ -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,13 +228,18 @@ def post(fn):
     return fn
 
 
-
 @pre
 def _setup_options(opt, file_config):
     global options
     options = opt
 
 
+@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:
@@ -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
 
 
index 7671c800c33316639055eef79c3ded85ee7dcddb..fd06163276adf6fdbbf98b276ef11f3cbe58fed8 100644 (file)
@@ -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 []
index fbb0d63e2b1a73472e183f2e7257ca72a4216934..a04bcbbdd4d4833343ed14b5a581aef1cba6b0ff 100644 (file)
@@ -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
 
index 9e139124a40de95a5531a804064fabdc9624dc30..675c2e7be03372a3c9011b2342f06188d0515952 100644 (file)
@@ -111,6 +111,7 @@ class EnsureZeroed(fixtures.ORMTest):
 
 class MemUsageTest(EnsureZeroed):
 
+    __tags__ = 'memory_intensive',
     __requires__ = 'cpython',
     __backend__ = True
 
index a0f9e6895945ca614cd47d61e4500876c42972d5..11b277b66d08d2931ff04f14a03f1b11c1c7edc3 100644 (file)
@@ -67,6 +67,7 @@ class MiscTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiledSQL):
 
     # currently not passing with pg 9.3 that does not seem to generate
     # any notices here, would rather find a way to mock this
+    @testing.requires.no_coverage
     @testing.only_on('postgresql+psycopg2', 'psycopg2-specific feature')
     def _test_notice_logging(self):
         log = logging.getLogger('sqlalchemy.dialects.postgresql')
index 457ddce0d891629e420365323c87c4414e39b765..c87b559c4662f073b15b75906d0627e0d01e99a0 100644 (file)
@@ -915,8 +915,7 @@ class SpecialTypesTest(fixtures.TestBase, ComparesTables, AssertsCompiledSQL):
 
     """test DDL and reflection of PG-specific types """
 
-    __only_on__ = 'postgresql'
-    __excluded_on__ = (('postgresql', '<', (8, 3, 0)),)
+    __only_on__ = 'postgresql >= 8.3.0',
     __backend__ = True
 
     @classmethod
index 29f3697530a377a9ae96b9ab5f6c45741e23a33f..7b56e15f549b67595658b671de6a5a8ecffb5521 100644 (file)
@@ -528,6 +528,7 @@ class PoolEventsTest(PoolTestBase):
 class PoolFirstConnectSyncTest(PoolTestBase):
     # test [ticket:2964]
 
+    @testing.requires.timing_intensive
     def test_sync(self):
         pool = self._queuepool_fixture(pool_size=3, max_overflow=0)
 
@@ -806,11 +807,8 @@ class QueuePoolTest(PoolTestBase):
                            max_overflow=-1)
 
         def status(pool):
-            tup = pool.size(), pool.checkedin(), pool.overflow(), \
+            return pool.size(), pool.checkedin(), pool.overflow(), \
                 pool.checkedout()
-            print('Pool size: %d  Connections in pool: %d Current '\
-                'Overflow: %d Current Checked out connections: %d' % tup)
-            return tup
 
         c1 = p.connect()
         self.assert_(status(p) == (3, 0, -2, 1))
@@ -853,6 +851,7 @@ class QueuePoolTest(PoolTestBase):
         lazy_gc()
         assert not pool._refs
 
+    @testing.requires.timing_intensive
     def test_timeout(self):
         p = self._queuepool_fixture(pool_size=3,
                            max_overflow=0,
@@ -868,6 +867,7 @@ class QueuePoolTest(PoolTestBase):
             assert int(time.time() - now) == 2
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_timeout_race(self):
         # test a race condition where the initial connecting threads all race
         # to queue.Empty, then block on the mutex.  each thread consumes a
@@ -967,6 +967,7 @@ class QueuePoolTest(PoolTestBase):
         eq_(p._overflow, 1)
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_hanging_connect_within_overflow(self):
         """test that a single connect() call which is hanging
         does not block other connections from proceeding."""
@@ -1028,6 +1029,7 @@ class QueuePoolTest(PoolTestBase):
 
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_waiters_handled(self):
         """test that threads waiting for connections are
         handled when the pool is replaced.
@@ -1079,6 +1081,7 @@ class QueuePoolTest(PoolTestBase):
         eq_(len(success), 12, "successes: %s" % success)
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_notify_waiters(self):
         dbapi = MockDBAPI()
 
@@ -1149,10 +1152,12 @@ class QueuePoolTest(PoolTestBase):
         assert c3.connection is c2_con
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_no_overflow(self):
         self._test_overflow(40, 0)
 
     @testing.requires.threading_with_mock
+    @testing.requires.timing_intensive
     def test_max_overflow(self):
         self._test_overflow(40, 5)
 
@@ -1254,6 +1259,7 @@ class QueuePoolTest(PoolTestBase):
         c3 = p.connect()
         assert id(c3.connection) != c_id
 
+    @testing.requires.timing_intensive
     def test_recycle_on_invalidate(self):
         p = self._queuepool_fixture(pool_size=1,
                            max_overflow=0)
@@ -1300,14 +1306,16 @@ class QueuePoolTest(PoolTestBase):
         c1.close()
         self._assert_cleanup_on_pooled_reconnect(dbapi, p)
 
+    @testing.requires.timing_intensive
     def test_error_on_pooled_reconnect_cleanup_recycle(self):
         dbapi, p = self._queuepool_dbapi_fixture(pool_size=1,
                                         max_overflow=2, recycle=1)
         c1 = p.connect()
         c1.close()
-        time.sleep(1)
+        time.sleep(1.5)
         self._assert_cleanup_on_pooled_reconnect(dbapi, p)
 
+    @testing.requires.timing_intensive
     def test_recycle_pool_no_race(self):
         def slow_close():
             slow_closing_connection._slow_close()
index 24984b062c7977fea7a0489b1abf45a5a60053a8..e8705d145231b0fad6a23b025f00edeb0a68dfca 100644 (file)
@@ -18,7 +18,8 @@ from sqlalchemy.testing.exclusions import \
      succeeds_if,\
      SpecPredicate,\
      against,\
-     LambdaPredicate
+     LambdaPredicate,\
+     requires_tag
 
 def no_support(db, reason):
     return SpecPredicate(db, description=reason)
@@ -745,17 +746,6 @@ class DefaultRequirements(SuiteRequirements):
                 "Not supported on MySQL + Windows"
             )
 
-    @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 skip_if(
-                lambda config: util.py3k and
-                    config.options.has_coverage,
-                "Stability issues with coverage + py3k"
-            )
 
     @property
     def selectone(self):
index b88edbe59072951f4f57602078ed14317d9a5491..03d399763c3d863f6017ddae9c694df703d443b1 100644 (file)
@@ -1164,9 +1164,6 @@ binary_table = MyPickleType = metadata = None
 
 
 class BinaryTest(fixtures.TestBase, AssertsExecutionResults):
-    __excluded_on__ = (
-        ('mysql', '<', (4, 1, 1)),  # screwy varbinary types
-    )
 
     @classmethod
     def setup_class(cls):
diff --git a/tox.ini b/tox.ini
index 836831d31d75710fbef5771b5c4739e1bca81e60..aedd87a03f62cbeb4eb94408e4e2419c63ff03eb 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = coverage, full, lightweight, memusage
+envlist = full
 
 [testenv]
 deps=pytest
@@ -17,19 +17,12 @@ envdir=pytest
 [testenv:full]
 
 
-[testenv:memusage]
-commands=
-  python -m pytest test/aaa_profiling/test_memusage.py {posargs}
-
-[testenv:lightweight]
-commands=
-  python -m pytest -k "not memusage" {posargs}
-
 [testenv:coverage]
 commands=
   python -m pytest \
         --cov=lib/sqlalchemy \
-        -k "not memusage" \
+        --exclude-tag memory-intensive \
+        --exclude-tag timing-intensive \
         {posargs}
   python -m coverage xml --include=lib/sqlalchemy/*