~ubuntu-branches/debian/jessie/sqlalchemy/jessie

« back to all changes in this revision

Viewing changes to lib/sqlalchemy/testing/plugin/noseplugin.py

  • Committer: Package Import Robot
  • Author(s): Piotr Ożarowski, Jakub Wilk, Piotr Ożarowski
  • Date: 2013-07-06 20:53:52 UTC
  • mfrom: (1.4.23) (16.1.17 experimental)
  • Revision ID: package-import@ubuntu.com-20130706205352-ryppl1eto3illd79
Tags: 0.8.2-1
[ Jakub Wilk ]
* Use canonical URIs for Vcs-* fields.

[ Piotr Ożarowski ]
* New upstream release
* Upload to unstable
* Build depend on python3-all instead of -dev, extensions are not built for
  Python 3.X 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Enhance nose with extra options and behaviors for running SQLAlchemy tests.
 
2
 
 
3
When running ./sqla_nose.py, this module is imported relative to the
 
4
"plugins" package as a top level package by the sqla_nose.py runner,
 
5
so that the plugin can be loaded with the rest of nose including the coverage
 
6
plugin before any of SQLAlchemy itself is imported, so that coverage works.
 
7
 
 
8
When third party libraries use this plugin, it can be imported
 
9
normally as "from sqlalchemy.testing.plugin import noseplugin".
 
10
 
 
11
"""
 
12
from __future__ import absolute_import
 
13
 
 
14
import os
 
15
import ConfigParser
 
16
 
 
17
from nose.plugins import Plugin
 
18
from nose import SkipTest
 
19
import time
 
20
import sys
 
21
import re
 
22
 
 
23
# late imports
 
24
fixtures = None
 
25
engines = None
 
26
exclusions = None
 
27
warnings = None
 
28
profiling = None
 
29
assertions = None
 
30
requirements = None
 
31
config = None
 
32
util = None
 
33
file_config = None
 
34
 
 
35
 
 
36
logging = None
 
37
db = None
 
38
db_label = None
 
39
db_url = None
 
40
db_opts = {}
 
41
options = None
 
42
_existing_engine = None
 
43
 
 
44
 
 
45
def _log(option, opt_str, value, parser):
 
46
    global logging
 
47
    if not logging:
 
48
        import logging
 
49
        logging.basicConfig()
 
50
 
 
51
    if opt_str.endswith('-info'):
 
52
        logging.getLogger(value).setLevel(logging.INFO)
 
53
    elif opt_str.endswith('-debug'):
 
54
        logging.getLogger(value).setLevel(logging.DEBUG)
 
55
 
 
56
 
 
57
def _list_dbs(*args):
 
58
    print "Available --db options (use --dburi to override)"
 
59
    for macro in sorted(file_config.options('db')):
 
60
        print "%20s\t%s" % (macro, file_config.get('db', macro))
 
61
    sys.exit(0)
 
62
 
 
63
 
 
64
def _server_side_cursors(options, opt_str, value, parser):
 
65
    db_opts['server_side_cursors'] = True
 
66
 
 
67
 
 
68
def _engine_strategy(options, opt_str, value, parser):
 
69
    if value:
 
70
        db_opts['strategy'] = value
 
71
 
 
72
pre_configure = []
 
73
post_configure = []
 
74
 
 
75
 
 
76
def pre(fn):
 
77
    pre_configure.append(fn)
 
78
    return fn
 
79
 
 
80
 
 
81
def post(fn):
 
82
    post_configure.append(fn)
 
83
    return fn
 
84
 
 
85
 
 
86
@pre
 
87
def _setup_options(opt, file_config):
 
88
    global options
 
89
    options = opt
 
90
 
 
91
 
 
92
@pre
 
93
def _monkeypatch_cdecimal(options, file_config):
 
94
    if options.cdecimal:
 
95
        import cdecimal
 
96
        sys.modules['decimal'] = cdecimal
 
97
 
 
98
 
 
99
@post
 
100
def _engine_uri(options, file_config):
 
101
    global db_label, db_url
 
102
 
 
103
    if options.dburi:
 
104
        db_url = options.dburi
 
105
        db_label = db_url[:db_url.index(':')]
 
106
    elif options.db:
 
107
        db_label = options.db
 
108
        db_url = None
 
109
 
 
110
    if db_url is None:
 
111
        if db_label not in file_config.options('db'):
 
112
            raise RuntimeError(
 
113
                "Unknown URI specifier '%s'.  Specify --dbs for known uris."
 
114
                        % db_label)
 
115
        db_url = file_config.get('db', db_label)
 
116
 
 
117
 
 
118
@post
 
119
def _require(options, file_config):
 
120
    if not(options.require or
 
121
           (file_config.has_section('require') and
 
122
            file_config.items('require'))):
 
123
        return
 
124
 
 
125
    try:
 
126
        import pkg_resources
 
127
    except ImportError:
 
128
        raise RuntimeError("setuptools is required for version requirements")
 
129
 
 
130
    cmdline = []
 
131
    for requirement in options.require:
 
132
        pkg_resources.require(requirement)
 
133
        cmdline.append(re.split('\s*(<!>=)', requirement, 1)[0])
 
134
 
 
135
    if file_config.has_section('require'):
 
136
        for label, requirement in file_config.items('require'):
 
137
            if not label == db_label or label.startswith('%s.' % db_label):
 
138
                continue
 
139
            seen = [c for c in cmdline if requirement.startswith(c)]
 
140
            if seen:
 
141
                continue
 
142
            pkg_resources.require(requirement)
 
143
 
 
144
 
 
145
@post
 
146
def _engine_pool(options, file_config):
 
147
    if options.mockpool:
 
148
        from sqlalchemy import pool
 
149
        db_opts['poolclass'] = pool.AssertionPool
 
150
 
 
151
 
 
152
@post
 
153
def _create_testing_engine(options, file_config):
 
154
    from sqlalchemy.testing import engines, config
 
155
    from sqlalchemy import testing
 
156
    global db
 
157
    config.db = testing.db = db = engines.testing_engine(db_url, db_opts)
 
158
    config.db.connect().close()
 
159
    config.db_opts = db_opts
 
160
    config.db_url = db_url
 
161
 
 
162
 
 
163
@post
 
164
def _prep_testing_database(options, file_config):
 
165
    from sqlalchemy.testing import engines
 
166
    from sqlalchemy import schema, inspect
 
167
 
 
168
    # also create alt schemas etc. here?
 
169
    if options.dropfirst:
 
170
        e = engines.utf8_engine()
 
171
        inspector = inspect(e)
 
172
 
 
173
        try:
 
174
            view_names = inspector.get_view_names()
 
175
        except NotImplementedError:
 
176
            pass
 
177
        else:
 
178
            for vname in view_names:
 
179
                e.execute(schema._DropView(schema.Table(vname, schema.MetaData())))
 
180
 
 
181
        try:
 
182
            view_names = inspector.get_view_names(schema="test_schema")
 
183
        except NotImplementedError:
 
184
            pass
 
185
        else:
 
186
            for vname in view_names:
 
187
                e.execute(schema._DropView(
 
188
                            schema.Table(vname,
 
189
                                        schema.MetaData(), schema="test_schema")))
 
190
 
 
191
        for tname in reversed(inspector.get_table_names(order_by="foreign_key")):
 
192
            e.execute(schema.DropTable(schema.Table(tname, schema.MetaData())))
 
193
 
 
194
        for tname in reversed(inspector.get_table_names(
 
195
                                order_by="foreign_key", schema="test_schema")):
 
196
            e.execute(schema.DropTable(
 
197
                schema.Table(tname, schema.MetaData(), schema="test_schema")))
 
198
 
 
199
        e.dispose()
 
200
 
 
201
 
 
202
@post
 
203
def _set_table_options(options, file_config):
 
204
    from sqlalchemy.testing import schema
 
205
 
 
206
    table_options = schema.table_options
 
207
    for spec in options.tableopts:
 
208
        key, value = spec.split('=')
 
209
        table_options[key] = value
 
210
 
 
211
    if options.mysql_engine:
 
212
        table_options['mysql_engine'] = options.mysql_engine
 
213
 
 
214
 
 
215
@post
 
216
def _reverse_topological(options, file_config):
 
217
    if options.reversetop:
 
218
        from sqlalchemy.orm.util import randomize_unitofwork
 
219
        randomize_unitofwork()
 
220
 
 
221
 
 
222
def _requirements_opt(options, opt_str, value, parser):
 
223
    _setup_requirements(value)
 
224
 
 
225
@post
 
226
def _requirements(options, file_config):
 
227
 
 
228
    requirement_cls = file_config.get('sqla_testing', "requirement_cls")
 
229
    _setup_requirements(requirement_cls)
 
230
 
 
231
def _setup_requirements(argument):
 
232
    from sqlalchemy.testing import config
 
233
    from sqlalchemy import testing
 
234
 
 
235
    if config.requirements is not None:
 
236
        return
 
237
 
 
238
    modname, clsname = argument.split(":")
 
239
 
 
240
    # importlib.import_module() only introduced in 2.7, a little
 
241
    # late
 
242
    mod = __import__(modname)
 
243
    for component in modname.split(".")[1:]:
 
244
        mod = getattr(mod, component)
 
245
    req_cls = getattr(mod, clsname)
 
246
    config.requirements = testing.requires = req_cls(config)
 
247
 
 
248
 
 
249
@post
 
250
def _post_setup_options(opt, file_config):
 
251
    from sqlalchemy.testing import config
 
252
    config.options = options
 
253
    config.file_config = file_config
 
254
 
 
255
 
 
256
@post
 
257
def _setup_profiling(options, file_config):
 
258
    from sqlalchemy.testing import profiling
 
259
    profiling._profile_stats = profiling.ProfileStatsFile(
 
260
                file_config.get('sqla_testing', 'profile_file'))
 
261
 
 
262
 
 
263
class NoseSQLAlchemy(Plugin):
 
264
    """
 
265
    Handles the setup and extra properties required for testing SQLAlchemy
 
266
    """
 
267
    enabled = True
 
268
 
 
269
    name = 'sqla_testing'
 
270
    score = 100
 
271
 
 
272
    def options(self, parser, env=os.environ):
 
273
        Plugin.options(self, parser, env)
 
274
        opt = parser.add_option
 
275
        opt("--log-info", action="callback", type="string", callback=_log,
 
276
            help="turn on info logging for <LOG> (multiple OK)")
 
277
        opt("--log-debug", action="callback", type="string", callback=_log,
 
278
            help="turn on debug logging for <LOG> (multiple OK)")
 
279
        opt("--require", action="append", dest="require", default=[],
 
280
            help="require a particular driver or module version (multiple OK)")
 
281
        opt("--db", action="store", dest="db", default="default",
 
282
            help="Use prefab database uri")
 
283
        opt('--dbs', action='callback', callback=_list_dbs,
 
284
            help="List available prefab dbs")
 
285
        opt("--dburi", action="store", dest="dburi",
 
286
            help="Database uri (overrides --db)")
 
287
        opt("--dropfirst", action="store_true", dest="dropfirst",
 
288
            help="Drop all tables in the target database first")
 
289
        opt("--mockpool", action="store_true", dest="mockpool",
 
290
            help="Use mock pool (asserts only one connection used)")
 
291
        opt("--low-connections", action="store_true", dest="low_connections",
 
292
            help="Use a low number of distinct connections - i.e. for Oracle TNS"
 
293
        )
 
294
        opt("--enginestrategy", action="callback", type="string",
 
295
            callback=_engine_strategy,
 
296
            help="Engine strategy (plain or threadlocal, defaults to plain)")
 
297
        opt("--reversetop", action="store_true", dest="reversetop", default=False,
 
298
            help="Use a random-ordering set implementation in the ORM (helps "
 
299
                  "reveal dependency issues)")
 
300
        opt("--requirements", action="callback", type="string",
 
301
            callback=_requirements_opt,
 
302
            help="requirements class for testing, overrides setup.cfg")
 
303
        opt("--with-cdecimal", action="store_true", dest="cdecimal", default=False,
 
304
            help="Monkeypatch the cdecimal library into Python 'decimal' for all tests")
 
305
        opt("--unhashable", action="store_true", dest="unhashable", default=False,
 
306
            help="Disallow SQLAlchemy from performing a hash() on mapped test objects.")
 
307
        opt("--noncomparable", action="store_true", dest="noncomparable", default=False,
 
308
            help="Disallow SQLAlchemy from performing == on mapped test objects.")
 
309
        opt("--truthless", action="store_true", dest="truthless", default=False,
 
310
            help="Disallow SQLAlchemy from truth-evaluating mapped test objects.")
 
311
        opt("--serverside", action="callback", callback=_server_side_cursors,
 
312
            help="Turn on server side cursors for PG")
 
313
        opt("--mysql-engine", action="store", dest="mysql_engine", default=None,
 
314
            help="Use the specified MySQL storage engine for all tables, default is "
 
315
                 "a db-default/InnoDB combo.")
 
316
        opt("--table-option", action="append", dest="tableopts", default=[],
 
317
            help="Add a dialect-specific table option, key=value")
 
318
        opt("--write-profiles", action="store_true", dest="write_profiles", default=False,
 
319
                help="Write/update profiling data.")
 
320
        global file_config
 
321
        file_config = ConfigParser.ConfigParser()
 
322
        file_config.read(['setup.cfg', 'test.cfg'])
 
323
 
 
324
    def configure(self, options, conf):
 
325
        Plugin.configure(self, options, conf)
 
326
        self.options = options
 
327
        for fn in pre_configure:
 
328
            fn(self.options, file_config)
 
329
 
 
330
    def begin(self):
 
331
        # Lazy setup of other options (post coverage)
 
332
        for fn in post_configure:
 
333
            fn(self.options, file_config)
 
334
 
 
335
        # late imports, has to happen after config as well
 
336
        # as nose plugins like coverage
 
337
        global util, fixtures, engines, exclusions, \
 
338
                        assertions, warnings, profiling,\
 
339
                        config
 
340
        from sqlalchemy.testing import fixtures, engines, exclusions, \
 
341
                        assertions, warnings, profiling, config
 
342
        from sqlalchemy import util
 
343
 
 
344
    def describeTest(self, test):
 
345
        return ""
 
346
 
 
347
    def wantFunction(self, fn):
 
348
        if fn.__module__.startswith('sqlalchemy.testing'):
 
349
            return False
 
350
 
 
351
    def wantClass(self, cls):
 
352
        """Return true if you want the main test selector to collect
 
353
        tests from this class, false if you don't, and None if you don't
 
354
        care.
 
355
 
 
356
        :Parameters:
 
357
           cls : class
 
358
             The class being examined by the selector
 
359
 
 
360
        """
 
361
        if not issubclass(cls, fixtures.TestBase):
 
362
            return False
 
363
        elif cls.__name__.startswith('_'):
 
364
            return False
 
365
        else:
 
366
            return True
 
367
 
 
368
    def _do_skips(self, cls):
 
369
        from sqlalchemy.testing import config
 
370
        if hasattr(cls, '__requires__'):
 
371
            def test_suite():
 
372
                return 'ok'
 
373
            test_suite.__name__ = cls.__name__
 
374
            for requirement in cls.__requires__:
 
375
                check = getattr(config.requirements, requirement)
 
376
 
 
377
                if not check.enabled:
 
378
                    raise SkipTest(
 
379
                        check.reason if check.reason
 
380
                        else
 
381
                        (
 
382
                            "'%s' unsupported on DB implementation '%s'" % (
 
383
                                cls.__name__, config.db.name
 
384
                            )
 
385
                        )
 
386
                    )
 
387
 
 
388
        if cls.__unsupported_on__:
 
389
            spec = exclusions.db_spec(*cls.__unsupported_on__)
 
390
            if spec(config.db):
 
391
                raise SkipTest(
 
392
                    "'%s' unsupported on DB implementation '%s'" % (
 
393
                     cls.__name__, config.db.name)
 
394
                    )
 
395
 
 
396
        if getattr(cls, '__only_on__', None):
 
397
            spec = exclusions.db_spec(*util.to_list(cls.__only_on__))
 
398
            if not spec(config.db):
 
399
                raise SkipTest(
 
400
                    "'%s' unsupported on DB implementation '%s'" % (
 
401
                     cls.__name__, config.db.name)
 
402
                    )
 
403
 
 
404
        if getattr(cls, '__skip_if__', False):
 
405
            for c in getattr(cls, '__skip_if__'):
 
406
                if c():
 
407
                    raise SkipTest("'%s' skipped by %s" % (
 
408
                        cls.__name__, c.__name__)
 
409
                    )
 
410
 
 
411
        for db, op, spec in getattr(cls, '__excluded_on__', ()):
 
412
            exclusions.exclude(db, op, spec,
 
413
                    "'%s' unsupported on DB %s version %s" % (
 
414
                    cls.__name__, config.db.name,
 
415
                    exclusions._server_version(config.db)))
 
416
 
 
417
    def beforeTest(self, test):
 
418
        warnings.resetwarnings()
 
419
        profiling._current_test = test.id()
 
420
 
 
421
    def afterTest(self, test):
 
422
        engines.testing_reaper._after_test_ctx()
 
423
        warnings.resetwarnings()
 
424
 
 
425
    def _setup_engine(self, ctx):
 
426
        if getattr(ctx, '__engine_options__', None):
 
427
            global _existing_engine
 
428
            _existing_engine = config.db
 
429
            config.db = engines.testing_engine(options=ctx.__engine_options__)
 
430
 
 
431
    def _restore_engine(self, ctx):
 
432
        global _existing_engine
 
433
        if _existing_engine is not None:
 
434
            config.db = _existing_engine
 
435
            _existing_engine = None
 
436
 
 
437
    def startContext(self, ctx):
 
438
        if not isinstance(ctx, type) \
 
439
            or not issubclass(ctx, fixtures.TestBase):
 
440
            return
 
441
        self._do_skips(ctx)
 
442
        self._setup_engine(ctx)
 
443
 
 
444
    def stopContext(self, ctx):
 
445
        if not isinstance(ctx, type) \
 
446
            or not issubclass(ctx, fixtures.TestBase):
 
447
            return
 
448
        engines.testing_reaper._stop_test_ctx()
 
449
        if not options.low_connections:
 
450
            assertions.global_cleanup_assertions()
 
451
        self._restore_engine(ctx)