From: Mike Bayer Date: Sun, 23 Sep 2012 17:36:28 +0000 (-0400) Subject: - upgrade to 0.8's exclusion mechanism for test compatibility X-Git-Tag: rel_0_7_9~16 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5008021a09d53cfbb5eb680e2c78931f5680cf45;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - upgrade to 0.8's exclusion mechanism for test compatibility --- diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index ac5de443be..10b640223a 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -5,8 +5,8 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php from compat import callable, cmp, reduce, defaultdict, py25_dict, \ - threading, py3k_warning, jython, pypy, win32, set_types, buffer, pickle, \ - update_wrapper, partial, md5_hex, decode_slice, dottedgetter,\ + threading, py3k_warning, jython, pypy, cpython, win32, set_types, buffer, \ + pickle, update_wrapper, partial, md5_hex, decode_slice, dottedgetter,\ parse_qsl, any, contextmanager, next from _collections import NamedTuple, ImmutableContainer, immutabledict, \ diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index cf750b850f..068c13ee4e 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -19,6 +19,7 @@ py3k_warning = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0) jython = sys.platform.startswith('java') pypy = hasattr(sys, 'pypy_version_info') win32 = sys.platform.startswith('win') +cpython = not pypy and not jython # TODO: something better for this ? if py3k_warning: set_types = set diff --git a/test/lib/exclusions.py b/test/lib/exclusions.py new file mode 100644 index 0000000000..b09441ed2e --- /dev/null +++ b/test/lib/exclusions.py @@ -0,0 +1,269 @@ +import operator +from nose import SkipTest +from sqlalchemy.util import decorator +from test.bootstrap import config +from sqlalchemy import util + + +def fails_if(predicate, reason=None): + predicate = _as_predicate(predicate) + + @decorator + def decorate(fn, *args, **kw): + if not predicate(): + return fn(*args, **kw) + else: + try: + fn(*args, **kw) + except Exception, ex: + print ("'%s' failed as expected (%s): %s " % ( + fn.__name__, predicate, str(ex))) + return True + else: + raise AssertionError( + "Unexpected success for '%s' (%s)" % + (fn.__name__, predicate)) + return decorate + +def skip_if(predicate, reason=None): + predicate = _as_predicate(predicate) + + @decorator + def decorate(fn, *args, **kw): + if predicate(): + if reason: + msg = "'%s' : %s" % ( + fn.__name__, + reason + ) + else: + msg = "'%s': %s" % ( + fn.__name__, predicate + ) + raise SkipTest(msg) + else: + return fn(*args, **kw) + return decorate + +def only_if(predicate, reason=None): + predicate = _as_predicate(predicate) + return skip_if(NotPredicate(predicate), reason) + +def succeeds_if(predicate, reason=None): + predicate = _as_predicate(predicate) + return fails_if(NotPredicate(predicate), reason) + +class Predicate(object): + @classmethod + def as_predicate(cls, predicate): + if isinstance(predicate, Predicate): + return predicate + elif isinstance(predicate, list): + return OrPredicate([cls.as_predicate(pred) for pred in predicate]) + elif isinstance(predicate, tuple): + return SpecPredicate(*predicate) + elif isinstance(predicate, basestring): + return SpecPredicate(predicate, None, None) + elif util.callable(predicate): + return LambdaPredicate(predicate) + else: + assert False, "unknown predicate type: %s" % predicate + +class SpecPredicate(Predicate): + def __init__(self, db, op=None, spec=None, description=None): + self.db = db + self.op = op + self.spec = spec + self.description = description + + _ops = { + '<': operator.lt, + '>': operator.gt, + '==': operator.eq, + '!=': operator.ne, + '<=': operator.le, + '>=': operator.ge, + 'in': operator.contains, + 'between': lambda val, pair: val >= pair[0] and val <= pair[1], + } + + def __call__(self, engine=None): + if engine is None: + engine = config.db + + if "+" in self.db: + dialect, driver = self.db.split('+') + else: + dialect, driver = self.db, None + + if dialect and engine.name != dialect: + return False + if driver is not None and engine.driver != driver: + return False + + if self.op is not None: + assert driver is None, "DBAPI version specs not supported yet" + + version = _server_version() + oper = hasattr(self.op, '__call__') and self.op \ + or self._ops[self.op] + return oper(version, self.spec) + else: + return True + + def _as_string(self, negate=False): + if self.description is not None: + return self.description + elif self.op is None: + if negate: + return "not %s" % self.db + else: + return "%s" % self.db + else: + if negate: + return "not %s %s %s" % ( + self.db, + self.op, + self.spec + ) + else: + return "%s %s %s" % ( + self.db, + self.op, + self.spec + ) + + def __str__(self): + return self._as_string() + +class LambdaPredicate(Predicate): + def __init__(self, lambda_, description=None, args=None, kw=None): + self.lambda_ = lambda_ + self.args = args or () + self.kw = kw or {} + if description: + self.description = description + elif lambda_.__doc__: + self.description = lambda_.__doc__ + else: + self.description = "custom function" + + def __call__(self): + return self.lambda_(*self.args, **self.kw) + + def _as_string(self, negate=False): + if negate: + return "not " + self.description + else: + return self.description + + def __str__(self): + return self._as_string() + +class NotPredicate(Predicate): + def __init__(self, predicate): + self.predicate = predicate + + def __call__(self, *arg, **kw): + return not self.predicate(*arg, **kw) + + def __str__(self): + return self.predicate._as_string(True) + +class OrPredicate(Predicate): + def __init__(self, predicates, description=None): + self.predicates = predicates + self.description = description + + def __call__(self, *arg, **kw): + for pred in self.predicates: + if pred(*arg, **kw): + self._str = pred + return True + return False + + _str = None + + def _eval_str(self, negate=False): + if self._str is None: + if negate: + conjunction = " and " + else: + conjunction = " or " + return conjunction.join(p._as_string(negate=negate) + for p in self.predicates) + else: + return self._str._as_string(negate=negate) + + def _negation_str(self): + if self.description is not None: + return "Not " + (self.description % {"spec": self._str}) + else: + return self._eval_str(negate=True) + + def _as_string(self, negate=False): + if negate: + return self._negation_str() + else: + if self.description is not None: + return self.description % {"spec": self._str} + else: + return self._eval_str() + + def __str__(self): + return self._as_string() + +_as_predicate = Predicate.as_predicate + +def _is_excluded(db, op, spec): + return SpecPredicate(db, op, spec)() + +def _server_version(bind=None): + """Return a server_version_info tuple.""" + + if bind is None: + bind = config.db + + # force metadata to be retrieved + conn = bind.connect() + version = getattr(bind.dialect, 'server_version_info', ()) + conn.close() + return version + +def db_spec(*dbs): + return OrPredicate( + Predicate.as_predicate(db) for db in dbs + ) + +@decorator +def future(fn, *args, **kw): + return fails_if(LambdaPredicate(fn, *args, **kw), "Future feature") + +def fails_on(db, reason): + return fails_if(SpecPredicate(db), reason) + +def fails_on_everything_except(*dbs): + return succeeds_if( + OrPredicate([ + SpecPredicate(db) for db in dbs + ]) + ) + +def skip(db, reason): + return skip_if(SpecPredicate(db), reason) + +def only_on(dbs, reason): + return only_if( + OrPredicate([SpecPredicate(db) for db in util.to_list(dbs)]) + ) + + +def exclude(db, op, spec, reason): + return skip_if(SpecPredicate(db, op, spec), reason) + + +def against(*queries): + return OrPredicate([ + Predicate.as_predicate(query) + for query in queries + ])() diff --git a/test/lib/requires.py b/test/lib/requires.py index 1fe55df39e..a7a7552daa 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -5,58 +5,78 @@ target database. """ -from testing import \ - _block_unconditionally as no_support, \ - _chain_decorators_on, \ - exclude, \ - emits_warning_on,\ +from exclusions import \ + skip, \ skip_if,\ + only_if,\ only_on,\ fails_on,\ fails_on_everything_except,\ - fails_if + fails_if,\ + SpecPredicate + +def no_support(db, reason): + return SpecPredicate(db, description=reason) + +def exclude(db, op, spec, description=None): + return SpecPredicate(db, op, spec, description=description) + from sqlalchemy import util from test.lib import config import testing import sys +crashes = skip + + +def _chain_decorators_on(fn, *decorators): + for decorator in reversed(decorators): + fn = decorator(fn) + return fn + def deferrable_or_no_constraints(fn): """Target database must support derferable constraints.""" - return _chain_decorators_on( - fn, + + return skip_if([ no_support('firebird', 'not supported by database'), no_support('mysql', 'not supported by database'), no_support('mssql', 'not supported by database'), - ) + ])(fn) def foreign_keys(fn): """Target database must support foreign keys.""" - return _chain_decorators_on( - fn, - no_support('sqlite', 'not supported by database'), - ) + + return skip_if( + no_support('sqlite', 'not supported by database') + )(fn) def unbounded_varchar(fn): """Target database must support VARCHAR with no length""" - return _chain_decorators_on( - fn, - no_support('firebird', 'not supported by database'), - no_support('oracle', 'not supported by database'), - no_support('mysql', 'not supported by database'), - ) + + return skip_if([ + "firebird", "oracle", "mysql" + ], "not supported by database" + )(fn) def boolean_col_expressions(fn): """Target database must support boolean expressions as columns""" - return _chain_decorators_on( - fn, + return skip_if([ no_support('firebird', 'not supported by database'), no_support('oracle', 'not supported by database'), no_support('mssql', 'not supported by database'), no_support('sybase', 'not supported by database'), no_support('maxdb', 'FIXME: verify not supported by database'), no_support('informix', 'not supported by database'), - ) + ])(fn) + +def standalone_binds(fn): + """target database/driver supports bound parameters as column expressions + without being in the context of a typed column. + + """ + return skip_if(["firebird", "mssql+mxodbc"], + "not supported by driver")(fn) def identity(fn): """Target database must support GENERATED AS IDENTITY or a facsimile. @@ -66,260 +86,246 @@ def identity(fn): without requiring pre-execution of a SEQUENCE or other artifact. """ - return _chain_decorators_on( - fn, - no_support('firebird', 'not supported by database'), - no_support('oracle', 'not supported by database'), - no_support('postgresql', 'not supported by database'), - no_support('sybase', 'not supported by database'), - ) + return skip_if(["firebird", "oracle", "postgresql", "sybase"], + "not supported by database" + )(fn) + +def reflectable_autoincrement(fn): + """Target database must support tables that can automatically generate + PKs assuming they were reflected. + + this is essentially all the DBs in "identity" plus Postgresql, which + has SERIAL support. FB and Oracle (and sybase?) require the Sequence to + be explicitly added, including if the table was reflected. + """ + return skip_if(["firebird", "oracle", "sybase"], + "not supported by database" + )(fn) + +def binary_comparisons(fn): + """target database/driver can allow BLOB/BINARY fields to be compared + against a bound parameter value. + """ + return skip_if(["oracle", "mssql"], + "not supported by database/driver" + )(fn) def independent_cursors(fn): - """Target must support simultaneous, independent database cursors on a single connection.""" + """Target must support simultaneous, independent database cursors + on a single connection.""" - return _chain_decorators_on( - fn, - no_support('mssql+pyodbc', 'no driver support'), - no_support('mssql+mxodbc', 'no driver support'), - ) + return skip_if(["mssql+pyodbc", "mssql+mxodbc"], "no driver support") def independent_connections(fn): """Target must support simultaneous, independent database connections.""" # This is also true of some configurations of UnixODBC and probably win32 # ODBC as well. - return _chain_decorators_on( - fn, - no_support('sqlite', 'Independent connections disabled when ' - ':memory: connections are used'), - exclude('mssql', '<', (9, 0, 0), - 'SQL Server 2005+ is required for independent connections'), - ) + return skip_if([ + no_support("sqlite", + "independent connections disabled " + "when :memory: connections are used"), + exclude("mssql", "<", (9, 0, 0), + "SQL Server 2005+ is required for " + "independent connections" + ) + ] + )(fn) def updateable_autoincrement_pks(fn): """Target must support UPDATE on autoincrement/integer primary key.""" - return _chain_decorators_on( - fn, - no_support('mssql', "IDENTITY cols can't be updated"), - no_support('sybase', "IDENTITY cols can't be updated"), - ) + + return skip_if(["mssql", "sybase"], + "IDENTITY columns can't be updated")(fn) def isolation_level(fn): return _chain_decorators_on( fn, - only_on(('postgresql', 'sqlite', 'mysql'), "DBAPI has no isolation level support"), + only_on(('postgresql', 'sqlite', 'mysql'), + "DBAPI has no isolation level support"), fails_on('postgresql+pypostgresql', 'pypostgresql bombs on multiple isolation level calls') ) def row_triggers(fn): """Target must support standard statement-running EACH ROW triggers.""" - return _chain_decorators_on( - fn, + + return skip_if([ # no access to same table no_support('mysql', 'requires SUPER priv'), exclude('mysql', '<', (5, 0, 10), 'not supported by database'), # huh? TODO: implement triggers for PG tests, remove this - no_support('postgresql', 'PG triggers need to be implemented for tests'), - ) + no_support('postgresql', + 'PG triggers need to be implemented for tests'), + ])(fn) def correlated_outer_joins(fn): - """Target must support an outer join to a subquery which correlates to the parent.""" + """Target must support an outer join to a subquery which + correlates to the parent.""" - return _chain_decorators_on( - fn, - no_support('oracle', 'Raises "ORA-01799: a column may not be outer-joined to a subquery"') - ) + return skip_if("oracle", 'Raises "ORA-01799: a column may not be ' + 'outer-joined to a subquery"')(fn) def update_from(fn): """Target must support UPDATE..FROM syntax""" - return _chain_decorators_on( - fn, - only_on(('postgresql', 'mssql', 'mysql'), - "Backend does not support UPDATE..FROM") - ) + + return only_on(['postgresql', 'mssql', 'mysql'], + "Backend does not support UPDATE..FROM")(fn) + def savepoints(fn): """Target database must support savepoints.""" - return _chain_decorators_on( - fn, - no_support('access', 'savepoints not supported'), - no_support('sqlite', 'savepoints not supported'), - no_support('sybase', 'savepoints not supported'), - exclude('mysql', '<', (5, 0, 3), 'savepoints not supported'), - exclude('informix', '<', (11, 55, 'xC3'), 'savepoints not supported'), - ) + + return skip_if([ + "access", + "sqlite", + "sybase", + ("mysql", "<", (5, 0, 3)), + ("informix", "<", (11, 55, "xC3")) + ], "savepoints not supported")(fn) def denormalized_names(fn): - """Target database must have 'denormalized', i.e. UPPERCASE as case insensitive names.""" + """Target database must have 'denormalized', i.e. + UPPERCASE as case insensitive names.""" return skip_if( lambda: not testing.db.dialect.requires_name_normalize, - "Backend does not require denomralized names." + "Backend does not require denormalized names." )(fn) def schemas(fn): - """Target database must support external schemas, and have one named 'test_schema'.""" + """Target database must support external schemas, and have one + named 'test_schema'.""" - return _chain_decorators_on( - fn, - no_support('sqlite', 'no schema support'), - no_support('firebird', 'no schema support') - ) + return skip_if([ + "sqlte", + "firebird" + ], "no schema support") def sequences(fn): """Target database must support SEQUENCEs.""" - return _chain_decorators_on( - fn, - no_support('access', 'no SEQUENCE support'), - no_support('drizzle', 'no SEQUENCE support'), - no_support('mssql', 'no SEQUENCE support'), - no_support('mysql', 'no SEQUENCE support'), - no_support('sqlite', 'no SEQUENCE support'), - no_support('sybase', 'no SEQUENCE support'), - no_support('informix', 'no SEQUENCE support'), - ) + + return only_if([ + "postgresql", "firebird", "oracle" + ], "no SEQUENCE support")(fn) def update_nowait(fn): """Target database must support SELECT...FOR UPDATE NOWAIT""" - return _chain_decorators_on( - fn, - no_support('access', 'no FOR UPDATE NOWAIT support'), - no_support('firebird', 'no FOR UPDATE NOWAIT support'), - no_support('mssql', 'no FOR UPDATE NOWAIT support'), - no_support('mysql', 'no FOR UPDATE NOWAIT support'), - no_support('sqlite', 'no FOR UPDATE NOWAIT support'), - no_support('sybase', 'no FOR UPDATE NOWAIT support'), - ) + return skip_if(["access", "firebird", "mssql", "mysql", "sqlite", "sybase"], + "no FOR UPDATE NOWAIT support" + )(fn) def subqueries(fn): """Target database must support subqueries.""" - return _chain_decorators_on( - fn, - exclude('mysql', '<', (4, 1, 1), 'no subquery support'), - ) + + return skip_if(exclude('mysql', '<', (4, 1, 1)), 'no subquery support')(fn) def intersect(fn): """Target database must support INTERSECT or equivalent.""" - return _chain_decorators_on( - fn, - fails_on('firebird', 'no support for INTERSECT'), - fails_on('mysql', 'no support for INTERSECT'), - fails_on('sybase', 'no support for INTERSECT'), - fails_on('informix', 'no support for INTERSECT'), - ) + + return fails_if([ + "firebird", "mysql", "sybase", "informix" + ], 'no support for INTERSECT')(fn) def except_(fn): """Target database must support EXCEPT or equivalent (i.e. MINUS).""" - return _chain_decorators_on( - fn, - fails_on('firebird', 'no support for EXCEPT'), - fails_on('mysql', 'no support for EXCEPT'), - fails_on('sybase', 'no support for EXCEPT'), - fails_on('informix', 'no support for EXCEPT'), - ) + return fails_if([ + "firebird", "mysql", "sybase", "informix" + ], 'no support for EXCEPT')(fn) def offset(fn): - """Target database must support some method of adding OFFSET or equivalent to a result set.""" - return _chain_decorators_on( - fn, - fails_on('sybase', 'no support for OFFSET or equivalent'), - ) + """Target database must support some method of adding OFFSET or + equivalent to a result set.""" + return fails_if([ + "sybase" + ], 'no support for OFFSET or equivalent')(fn) def window_functions(fn): - return _chain_decorators_on( - fn, - only_on(('postgresql', 'mssql', 'oracle'), - "Backend does not support window functions"), - ) + return only_if([ + "postgresql", "mssql", "oracle" + ], "Backend does not support window functions")(fn) def returning(fn): - return _chain_decorators_on( - fn, - no_support('access', "'returning' not supported by database"), - no_support('sqlite', "'returning' not supported by database"), - no_support('mysql', "'returning' not supported by database"), - no_support('maxdb', "'returning' not supported by database"), - no_support('sybase', "'returning' not supported by database"), - no_support('informix', "'returning' not supported by database"), - ) + return only_if(["postgresql", "mssql", "oracle", "firebird"], + "'returning' not supported by database" + )(fn) def two_phase_transactions(fn): """Target database must support two-phase transactions.""" - return _chain_decorators_on( - fn, - no_support('access', 'not supported by database'), + + return skip_if([ + no_support('access', 'two-phase xact not supported by database'), no_support('firebird', 'no SA implementation'), - no_support('maxdb', 'not supported by database'), - no_support('mssql', 'FIXME: guessing, needs confirmation'), - no_support('oracle', 'no SA implementation'), - no_support('drizzle', 'not supported by database'), - no_support('sqlite', 'not supported by database'), - no_support('sybase', 'FIXME: guessing, needs confirmation'), - no_support('postgresql+zxjdbc', 'FIXME: JDBC driver confuses the transaction state, may ' + no_support('maxdb', 'two-phase xact not supported by database'), + no_support('mssql', 'two-phase xact not supported by drivers'), + no_support('oracle', 'two-phase xact not implemented in SQLA/oracle'), + no_support('drizzle', 'two-phase xact not supported by database'), + no_support('sqlite', 'two-phase xact not supported by database'), + no_support('sybase', 'two-phase xact not supported by drivers/SQLA'), + no_support('postgresql+zxjdbc', + 'FIXME: JDBC driver confuses the transaction state, may ' 'need separate XA implementation'), - exclude('mysql', '<', (5, 0, 3), 'not supported by database'), - ) + exclude('mysql', '<', (5, 0, 3), + 'two-phase xact not supported by database'), + ])(fn) def views(fn): """Target database must support VIEWs.""" - return _chain_decorators_on( - fn, - no_support('drizzle', 'no VIEW support'), - ) + + return skip_if("drizzle", "no VIEW support")(fn) def unicode_connections(fn): """Target driver must support some encoding of Unicode across the wire.""" # TODO: expand to exclude MySQLdb versions w/ broken unicode - return _chain_decorators_on( - fn, + return skip_if([ exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'), - ) + ])(fn) def unicode_ddl(fn): """Target driver must support some encoding of Unicode across the wire.""" # TODO: expand to exclude MySQLdb versions w/ broken unicode - return _chain_decorators_on( - fn, + return skip_if([ no_support('maxdb', 'database support flakey'), no_support('oracle', 'FIXME: no support in database?'), no_support('sybase', 'FIXME: guessing, needs confirmation'), no_support('mssql+pymssql', 'no FreeTDS support'), exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'), - ) + ])(fn) def sane_rowcount(fn): - return _chain_decorators_on( - fn, - skip_if(lambda: not testing.db.dialect.supports_sane_rowcount) - ) + return skip_if( + lambda: not testing.db.dialect.supports_sane_rowcount, + "driver doesn't support 'sane' rowcount" + )(fn) def cextensions(fn): - return _chain_decorators_on( - fn, - skip_if(lambda: not _has_cextensions(), "C extensions not installed") - ) + return skip_if( + lambda: not _has_cextensions(), "C extensions not installed" + )(fn) +def emulated_lastrowid(fn): + """"target dialect retrieves cursor.lastrowid or an equivalent + after an insert() construct executes. + """ + return fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', + 'sqlite+pysqlite', 'mysql+pymysql', + 'mssql+pyodbc', 'mssql+mxodbc')(fn) + def dbapi_lastrowid(fn): - if util.pypy: - return _chain_decorators_on( - fn, - fails_if(lambda:True) - ) - else: - return _chain_decorators_on( - fn, - fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', - 'sqlite+pysqlite', 'mysql+pymysql'), - ) + """"target backend includes a 'lastrowid' accessor on the DBAPI + cursor object. + + """ + return fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', + 'sqlite+pysqlite', 'mysql+pymysql')(fn) def sane_multi_rowcount(fn): - return _chain_decorators_on( - fn, - skip_if(lambda: not testing.db.dialect.supports_sane_multi_rowcount) - ) + return skip_if( + lambda: not testing.db.dialect.supports_sane_multi_rowcount, + "driver doesn't support 'sane' multi row count" + ) def nullsordering(fn): """Target backends that support nulls ordering.""" @@ -374,33 +380,17 @@ def python25(fn): def cpython(fn): return _chain_decorators_on( fn, - skip_if(lambda: util.jython or util.pypy, + only_if(lambda: util.cpython, "cPython interpreter needed" ) ) -def _has_cextensions(): - try: - from sqlalchemy import cresultproxy, cprocessors - return True - except ImportError: - return False - -def _has_sqlite(): - from sqlalchemy import create_engine - try: - e = create_engine('sqlite://') - return True - except ImportError: - return False - -def _has_mysql_on_windows(): - return testing.against('mysql') and \ - testing.db.dialect._server_casing == 1 +def predictable_gc(fn): + """target platform must remove all cycles unconditionally when + gc.collect() is called, as well as clean out unreferenced subclasses. -def _has_mysql_fully_case_sensitive(): - return testing.against('mysql') and \ - testing.db.dialect._server_casing == 0 + """ + return cpython(fn) def sqlite(fn): return _chain_decorators_on( @@ -437,3 +427,35 @@ def english_locale_on_postgresql(fn): skip_if(lambda: testing.against('postgresql') \ and not testing.db.scalar('SHOW LC_COLLATE').startswith('en')) ) + +def selectone(fn): + """target driver must support the literal statement 'select 1'""" + return _chain_decorators_on( + fn, + skip_if(lambda: testing.against('oracle'), + "non-standard SELECT scalar syntax") + ) + +def _has_cextensions(): + try: + from sqlalchemy import cresultproxy, cprocessors + return True + except ImportError: + return False + +def _has_sqlite(): + from sqlalchemy import create_engine + try: + e = create_engine('sqlite://') + return True + except ImportError: + return False + +def _has_mysql_on_windows(): + return testing.against('mysql') and \ + testing.db.dialect._detect_casing(testing.db) == 1 + +def _has_mysql_fully_case_sensitive(): + return testing.against('mysql') and \ + testing.db.dialect._detect_casing(testing.db) == 0 + diff --git a/test/lib/testing.py b/test/lib/testing.py index 92817b6040..f1caaaf304 100644 --- a/test/lib/testing.py +++ b/test/lib/testing.py @@ -1,7 +1,6 @@ """TestCase and TestSuite artifacts and testing decorators.""" import itertools -import operator import re import sys import types @@ -16,18 +15,11 @@ from engines import drop_all_tables from sqlalchemy import exc as sa_exc, util, types as sqltypes, schema, \ pool, orm from sqlalchemy.engine import default -from nose import SkipTest +from exclusions import db_spec, _is_excluded, fails_if, skip_if, future,\ + fails_on, fails_on_everything_except, skip, only_on, exclude, against,\ + _server_version - -_ops = { '<': operator.lt, - '>': operator.gt, - '==': operator.eq, - '!=': operator.ne, - '<=': operator.le, - '>=': operator.ge, - 'in': operator.contains, - 'between': lambda val, pair: val >= pair[0] and val <= pair[1], - } +crashes = skip # sugar ('testing.db'); set here by config() at runtime db = None @@ -35,266 +27,6 @@ db = None # more sugar, installed by __init__ requires = None -def fails_if(callable_, reason=None): - """Mark a test as expected to fail if callable_ returns True. - - If the callable returns false, the test is run and reported as normal. - However if the callable returns true, the test is expected to fail and the - unit test logic is inverted: if the test fails, a success is reported. If - the test succeeds, a failure is reported. - """ - - docstring = getattr(callable_, '__doc__', None) or callable_.__name__ - description = docstring.split('\n')[0] - - @decorator - def decorate(fn, *args, **kw): - if not callable_(): - return fn(*args, **kw) - else: - try: - fn(*args, **kw) - except Exception, ex: - print ("'%s' failed as expected (condition: %s): %s " % ( - fn.__name__, description, str(ex))) - return True - else: - raise AssertionError( - "Unexpected success for '%s' (condition: %s)" % - (fn.__name__, description)) - return decorate - -@decorator -def future(fn, *args, **kw): - """Mark a test as expected to unconditionally fail. - - Takes no arguments, omit parens when using as a decorator. - """ - - try: - fn(*args, **kw) - except Exception, ex: - print ("Future test '%s' failed as expected: %s " % ( - fn.__name__, str(ex))) - return True - else: - raise AssertionError( - "Unexpected success for future test '%s'" % fn.__name__) - -def db_spec(*dbs): - dialects = set([x for x in dbs if '+' not in x]) - drivers = set([x[1:] for x in dbs if x.startswith('+')]) - specs = set([tuple(x.split('+')) for x in dbs if '+' in x and x not in drivers]) - - def check(engine): - return engine.name in dialects or \ - engine.driver in drivers or \ - (engine.name, engine.driver) in specs - - return check - - -def fails_on(dbs, reason): - """Mark a test as expected to fail on the specified database - implementation. - - Unlike ``crashes``, tests marked as ``fails_on`` will be run - for the named databases. The test is expected to fail and the unit test - logic is inverted: if the test fails, a success is reported. If the test - succeeds, a failure is reported. - """ - - spec = db_spec(dbs) - - @decorator - def decorate(fn, *args, **kw): - if not spec(config.db): - return fn(*args, **kw) - else: - try: - fn(*args, **kw) - except Exception, ex: - print ("'%s' failed as expected on DB implementation " - "'%s+%s': %s" % ( - fn.__name__, config.db.name, config.db.driver, reason)) - return True - else: - raise AssertionError( - "Unexpected success for '%s' on DB implementation '%s+%s'" % - (fn.__name__, config.db.name, config.db.driver)) - return decorate - -def fails_on_everything_except(*dbs): - """Mark a test as expected to fail on most database implementations. - - Like ``fails_on``, except failure is the expected outcome on all - databases except those listed. - """ - - spec = db_spec(*dbs) - - @decorator - def decorate(fn, *args, **kw): - if spec(config.db): - return fn(*args, **kw) - else: - try: - fn(*args, **kw) - except Exception, ex: - print ("'%s' failed as expected on DB implementation " - "'%s+%s': %s" % ( - fn.__name__, config.db.name, config.db.driver, str(ex))) - return True - else: - raise AssertionError( - "Unexpected success for '%s' on DB implementation '%s+%s'" % - (fn.__name__, config.db.name, config.db.driver)) - return decorate - -def crashes(db, reason): - """Mark a test as unsupported by a database implementation. - - ``crashes`` tests will be skipped unconditionally. Use for feature tests - that cause deadlocks or other fatal problems. - - """ - carp = _should_carp_about_exclusion(reason) - spec = db_spec(db) - @decorator - def decorate(fn, *args, **kw): - if spec(config.db): - msg = "'%s' unsupported on DB implementation '%s+%s': %s" % ( - fn.__name__, config.db.name, config.db.driver, reason) - print msg - if carp: - print >> sys.stderr, msg - return True - else: - return fn(*args, **kw) - return decorate - -def _block_unconditionally(db, reason): - """Mark a test as unsupported by a database implementation. - - Will never run the test against any version of the given database, ever, - no matter what. Use when your assumptions are infallible; past, present - and future. - - """ - carp = _should_carp_about_exclusion(reason) - spec = db_spec(db) - @decorator - def decorate(fn, *args, **kw): - if spec(config.db): - msg = "'%s' unsupported on DB implementation '%s+%s': %s" % ( - fn.__name__, config.db.name, config.db.driver, reason) - raise SkipTest(msg) - else: - return fn(*args, **kw) - return decorate - -def only_on(dbs, reason): - carp = _should_carp_about_exclusion(reason) - spec = db_spec(*util.to_list(dbs)) - @decorator - def decorate(fn, *args, **kw): - if spec(config.db): - return fn(*args, **kw) - else: - msg = "'%s' unsupported on DB implementation '%s+%s': %s" % ( - fn.__name__, config.db.name, config.db.driver, reason) - raise SkipTest(msg) - return decorate - -def exclude(db, op, spec, reason): - """Mark a test as unsupported by specific database server versions. - - Stackable, both with other excludes and other decorators. Examples:: - - # Not supported by mydb versions less than 1, 0 - @exclude('mydb', '<', (1,0)) - # Other operators work too - @exclude('bigdb', '==', (9,0,9)) - @exclude('yikesdb', 'in', ((0, 3, 'alpha2'), (0, 3, 'alpha3'))) - - """ - carp = _should_carp_about_exclusion(reason) - - @decorator - def decorate(fn, *args, **kw): - if _is_excluded(db, op, spec): - msg = "'%s' unsupported on DB %s version '%s': %s" % ( - fn.__name__, config.db.name, _server_version(), reason) - raise SkipTest(msg) - else: - return fn(*args, **kw) - return decorate - -def _should_carp_about_exclusion(reason): - """Guard against forgotten exclusions.""" - assert reason - for _ in ('todo', 'fixme', 'xxx'): - if _ in reason.lower(): - return True - else: - if len(reason) < 4: - return True - -def _is_excluded(db, op, spec): - """Return True if the configured db matches an exclusion specification. - - db: - A dialect name - op: - An operator or stringified operator, such as '==' - spec: - A value that will be compared to the dialect's server_version_info - using the supplied operator. - - Examples:: - # Not supported by mydb versions less than 1, 0 - _is_excluded('mydb', '<', (1,0)) - # Other operators work too - _is_excluded('bigdb', '==', (9,0,9)) - _is_excluded('yikesdb', 'in', ((0, 3, 'alpha2'), (0, 3, 'alpha3'))) - """ - - vendor_spec = db_spec(db) - - if not vendor_spec(config.db): - return False - - version = _server_version() - - oper = hasattr(op, '__call__') and op or _ops[op] - return oper(version, spec) - -def _server_version(bind=None): - """Return a server_version_info tuple.""" - - if bind is None: - bind = config.db - - # force metadata to be retrieved - conn = bind.connect() - version = getattr(bind.dialect, 'server_version_info', ()) - conn.close() - return version - -def skip_if(predicate, reason=None): - """Skip a test if predicate is true.""" - reason = reason or predicate.__name__ - carp = _should_carp_about_exclusion(reason) - - @decorator - def decorate(fn, *args, **kw): - if predicate(): - msg = "'%s' skipped on DB %s version '%s': %s" % ( - fn.__name__, config.db.name, _server_version(), reason) - raise SkipTest(msg) - else: - return fn(*args, **kw) - return decorate def emits_warning(*messages): """Mark a test as emitting a warning. @@ -442,40 +174,6 @@ def global_cleanup_assertions(): testutil.lazy_gc() assert not pool._refs, str(pool._refs) -def against(*queries): - """Boolean predicate, compares to testing database configuration. - - Given one or more dialect names, returns True if one is the configured - database engine. - - Also supports comparison to database version when provided with one or - more 3-tuples of dialect name, operator, and version specification:: - - testing.against('mysql', 'postgresql') - testing.against(('mysql', '>=', (5, 0, 0)) - """ - - for query in queries: - if isinstance(query, basestring): - if db_spec(query)(config.db): - return True - else: - name, op, spec = query - if not db_spec(name)(config.db): - continue - - have = _server_version() - - oper = hasattr(op, '__call__') and op or _ops[op] - if oper(have, spec): - return True - return False - -def _chain_decorators_on(fn, *decorators): - """Apply a series of decorators to fn, returning a decorated function.""" - for decorator in reversed(decorators): - fn = decorator(fn) - return fn def run_as_contextmanager(ctx, fn, *arg, **kw): """Run the given function under the given contextmanager,