summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.unittests.rst10
-rw-r--r--lib/sqlalchemy/testing/config.py3
-rw-r--r--lib/sqlalchemy/testing/exclusions.py34
-rw-r--r--lib/sqlalchemy/testing/plugin/noseplugin.py13
-rw-r--r--lib/sqlalchemy/testing/plugin/plugin_base.py113
-rw-r--r--lib/sqlalchemy/testing/plugin/pytestplugin.py9
-rw-r--r--lib/sqlalchemy/testing/requirements.py33
-rw-r--r--test/aaa_profiling/test_memusage.py1
-rw-r--r--test/dialect/postgresql/test_dialect.py1
-rw-r--r--test/dialect/postgresql/test_types.py3
-rw-r--r--test/engine/test_pool.py18
-rw-r--r--test/requirements.py14
-rw-r--r--test/sql/test_types.py3
-rw-r--r--tox.ini13
14 files changed, 192 insertions, 76 deletions
diff --git a/README.unittests.rst b/README.unittests.rst
index 375d0737c..209eefe0d 100644
--- a/README.unittests.rst
+++ b/README.unittests.rst
@@ -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
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
diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py
index 9e139124a..675c2e7be 100644
--- a/test/aaa_profiling/test_memusage.py
+++ b/test/aaa_profiling/test_memusage.py
@@ -111,6 +111,7 @@ class EnsureZeroed(fixtures.ORMTest):
class MemUsageTest(EnsureZeroed):
+ __tags__ = 'memory_intensive',
__requires__ = 'cpython',
__backend__ = True
diff --git a/test/dialect/postgresql/test_dialect.py b/test/dialect/postgresql/test_dialect.py
index a0f9e6895..11b277b66 100644
--- a/test/dialect/postgresql/test_dialect.py
+++ b/test/dialect/postgresql/test_dialect.py
@@ -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')
diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py
index 457ddce0d..c87b559c4 100644
--- a/test/dialect/postgresql/test_types.py
+++ b/test/dialect/postgresql/test_types.py
@@ -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
diff --git a/test/engine/test_pool.py b/test/engine/test_pool.py
index 29f369753..7b56e15f5 100644
--- a/test/engine/test_pool.py
+++ b/test/engine/test_pool.py
@@ -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()
diff --git a/test/requirements.py b/test/requirements.py
index 24984b062..e8705d145 100644
--- a/test/requirements.py
+++ b/test/requirements.py
@@ -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):
diff --git a/test/sql/test_types.py b/test/sql/test_types.py
index b88edbe59..03d399763 100644
--- a/test/sql/test_types.py
+++ b/test/sql/test_types.py
@@ -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 836831d31..aedd87a03 100644
--- 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/*