diff options
-rw-r--r-- | README.unittests.rst | 10 | ||||
-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 | ||||
-rw-r--r-- | test/aaa_profiling/test_memusage.py | 1 | ||||
-rw-r--r-- | test/dialect/postgresql/test_dialect.py | 1 | ||||
-rw-r--r-- | test/dialect/postgresql/test_types.py | 3 | ||||
-rw-r--r-- | test/engine/test_pool.py | 18 | ||||
-rw-r--r-- | test/requirements.py | 14 | ||||
-rw-r--r-- | test/sql/test_types.py | 3 | ||||
-rw-r--r-- | tox.ini | 13 |
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): @@ -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/* |