plugin_base.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. # plugin/plugin_base.py
  2. # Copyright (C) 2005-2017 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """Testing extensions.
  8. this module is designed to work as a testing-framework-agnostic library,
  9. so that we can continue to support nose and also begin adding new
  10. functionality via py.test.
  11. """
  12. from __future__ import absolute_import
  13. import sys
  14. import re
  15. py3k = sys.version_info >= (3, 0)
  16. if py3k:
  17. import configparser
  18. else:
  19. import ConfigParser as configparser
  20. # late imports
  21. fixtures = None
  22. engines = None
  23. exclusions = None
  24. warnings = None
  25. profiling = None
  26. assertions = None
  27. requirements = None
  28. config = None
  29. testing = None
  30. util = None
  31. file_config = None
  32. logging = None
  33. include_tags = set()
  34. exclude_tags = set()
  35. options = None
  36. def setup_options(make_option):
  37. make_option("--log-info", action="callback", type="string", callback=_log,
  38. help="turn on info logging for <LOG> (multiple OK)")
  39. make_option("--log-debug", action="callback",
  40. type="string", callback=_log,
  41. help="turn on debug logging for <LOG> (multiple OK)")
  42. make_option("--db", action="append", type="string", dest="db",
  43. help="Use prefab database uri. Multiple OK, "
  44. "first one is run by default.")
  45. make_option('--dbs', action='callback', callback=_list_dbs,
  46. help="List available prefab dbs")
  47. make_option("--dburi", action="append", type="string", dest="dburi",
  48. help="Database uri. Multiple OK, "
  49. "first one is run by default.")
  50. make_option("--dropfirst", action="store_true", dest="dropfirst",
  51. help="Drop all tables in the target database first")
  52. make_option("--backend-only", action="store_true", dest="backend_only",
  53. help="Run only tests marked with __backend__")
  54. make_option("--low-connections", action="store_true",
  55. dest="low_connections",
  56. help="Use a low number of distinct connections - "
  57. "i.e. for Oracle TNS")
  58. make_option("--write-idents", type="string", dest="write_idents",
  59. help="write out generated follower idents to <file>, "
  60. "when -n<num> is used")
  61. make_option("--reversetop", action="store_true",
  62. dest="reversetop", default=False,
  63. help="Use a random-ordering set implementation in the ORM "
  64. "(helps reveal dependency issues)")
  65. make_option("--requirements", action="callback", type="string",
  66. callback=_requirements_opt,
  67. help="requirements class for testing, overrides setup.cfg")
  68. make_option("--with-cdecimal", action="store_true",
  69. dest="cdecimal", default=False,
  70. help="Monkeypatch the cdecimal library into Python 'decimal' "
  71. "for all tests")
  72. make_option("--include-tag", action="callback", callback=_include_tag,
  73. type="string",
  74. help="Include tests with tag <tag>")
  75. make_option("--exclude-tag", action="callback", callback=_exclude_tag,
  76. type="string",
  77. help="Exclude tests with tag <tag>")
  78. make_option("--write-profiles", action="store_true",
  79. dest="write_profiles", default=False,
  80. help="Write/update failing profiling data.")
  81. make_option("--force-write-profiles", action="store_true",
  82. dest="force_write_profiles", default=False,
  83. help="Unconditionally write/update profiling data.")
  84. def configure_follower(follower_ident):
  85. """Configure required state for a follower.
  86. This invokes in the parent process and typically includes
  87. database creation.
  88. """
  89. from sqlalchemy.testing import provision
  90. provision.FOLLOWER_IDENT = follower_ident
  91. def memoize_important_follower_config(dict_):
  92. """Store important configuration we will need to send to a follower.
  93. This invokes in the parent process after normal config is set up.
  94. This is necessary as py.test seems to not be using forking, so we
  95. start with nothing in memory, *but* it isn't running our argparse
  96. callables, so we have to just copy all of that over.
  97. """
  98. dict_['memoized_config'] = {
  99. 'include_tags': include_tags,
  100. 'exclude_tags': exclude_tags
  101. }
  102. def restore_important_follower_config(dict_):
  103. """Restore important configuration needed by a follower.
  104. This invokes in the follower process.
  105. """
  106. global include_tags, exclude_tags
  107. include_tags.update(dict_['memoized_config']['include_tags'])
  108. exclude_tags.update(dict_['memoized_config']['exclude_tags'])
  109. def read_config():
  110. global file_config
  111. file_config = configparser.ConfigParser()
  112. file_config.read(['setup.cfg', 'test.cfg'])
  113. def pre_begin(opt):
  114. """things to set up early, before coverage might be setup."""
  115. global options
  116. options = opt
  117. for fn in pre_configure:
  118. fn(options, file_config)
  119. def set_coverage_flag(value):
  120. options.has_coverage = value
  121. _skip_test_exception = None
  122. def set_skip_test(exc):
  123. global _skip_test_exception
  124. _skip_test_exception = exc
  125. def post_begin():
  126. """things to set up later, once we know coverage is running."""
  127. # Lazy setup of other options (post coverage)
  128. for fn in post_configure:
  129. fn(options, file_config)
  130. # late imports, has to happen after config as well
  131. # as nose plugins like coverage
  132. global util, fixtures, engines, exclusions, \
  133. assertions, warnings, profiling,\
  134. config, testing
  135. from sqlalchemy import testing # noqa
  136. from sqlalchemy.testing import fixtures, engines, exclusions # noqa
  137. from sqlalchemy.testing import assertions, warnings, profiling # noqa
  138. from sqlalchemy.testing import config # noqa
  139. from sqlalchemy import util # noqa
  140. warnings.setup_filters()
  141. def _log(opt_str, value, parser):
  142. global logging
  143. if not logging:
  144. import logging
  145. logging.basicConfig()
  146. if opt_str.endswith('-info'):
  147. logging.getLogger(value).setLevel(logging.INFO)
  148. elif opt_str.endswith('-debug'):
  149. logging.getLogger(value).setLevel(logging.DEBUG)
  150. def _list_dbs(*args):
  151. print("Available --db options (use --dburi to override)")
  152. for macro in sorted(file_config.options('db')):
  153. print("%20s\t%s" % (macro, file_config.get('db', macro)))
  154. sys.exit(0)
  155. def _requirements_opt(opt_str, value, parser):
  156. _setup_requirements(value)
  157. def _exclude_tag(opt_str, value, parser):
  158. exclude_tags.add(value.replace('-', '_'))
  159. def _include_tag(opt_str, value, parser):
  160. include_tags.add(value.replace('-', '_'))
  161. pre_configure = []
  162. post_configure = []
  163. def pre(fn):
  164. pre_configure.append(fn)
  165. return fn
  166. def post(fn):
  167. post_configure.append(fn)
  168. return fn
  169. @pre
  170. def _setup_options(opt, file_config):
  171. global options
  172. options = opt
  173. @pre
  174. def _monkeypatch_cdecimal(options, file_config):
  175. if options.cdecimal:
  176. import cdecimal
  177. sys.modules['decimal'] = cdecimal
  178. @post
  179. def _init_skiptest(options, file_config):
  180. from sqlalchemy.testing import config
  181. config._skip_test_exception = _skip_test_exception
  182. @post
  183. def _engine_uri(options, file_config):
  184. from sqlalchemy.testing import config
  185. from sqlalchemy import testing
  186. from sqlalchemy.testing import provision
  187. if options.dburi:
  188. db_urls = list(options.dburi)
  189. else:
  190. db_urls = []
  191. if options.db:
  192. for db_token in options.db:
  193. for db in re.split(r'[,\s]+', db_token):
  194. if db not in file_config.options('db'):
  195. raise RuntimeError(
  196. "Unknown URI specifier '%s'. "
  197. "Specify --dbs for known uris."
  198. % db)
  199. else:
  200. db_urls.append(file_config.get('db', db))
  201. if not db_urls:
  202. db_urls.append(file_config.get('db', 'default'))
  203. config._current = None
  204. for db_url in db_urls:
  205. cfg = provision.setup_config(
  206. db_url, options, file_config, provision.FOLLOWER_IDENT)
  207. if not config._current:
  208. cfg.set_as_current(cfg, testing)
  209. @post
  210. def _requirements(options, file_config):
  211. requirement_cls = file_config.get('sqla_testing', "requirement_cls")
  212. _setup_requirements(requirement_cls)
  213. def _setup_requirements(argument):
  214. from sqlalchemy.testing import config
  215. from sqlalchemy import testing
  216. if config.requirements is not None:
  217. return
  218. modname, clsname = argument.split(":")
  219. # importlib.import_module() only introduced in 2.7, a little
  220. # late
  221. mod = __import__(modname)
  222. for component in modname.split(".")[1:]:
  223. mod = getattr(mod, component)
  224. req_cls = getattr(mod, clsname)
  225. config.requirements = testing.requires = req_cls()
  226. @post
  227. def _prep_testing_database(options, file_config):
  228. from sqlalchemy.testing import config, util
  229. from sqlalchemy.testing.exclusions import against
  230. from sqlalchemy import schema, inspect
  231. if options.dropfirst:
  232. for cfg in config.Config.all_configs():
  233. e = cfg.db
  234. inspector = inspect(e)
  235. try:
  236. view_names = inspector.get_view_names()
  237. except NotImplementedError:
  238. pass
  239. else:
  240. for vname in view_names:
  241. e.execute(schema._DropView(
  242. schema.Table(vname, schema.MetaData())
  243. ))
  244. if config.requirements.schemas.enabled_for_config(cfg):
  245. try:
  246. view_names = inspector.get_view_names(
  247. schema="test_schema")
  248. except NotImplementedError:
  249. pass
  250. else:
  251. for vname in view_names:
  252. e.execute(schema._DropView(
  253. schema.Table(vname, schema.MetaData(),
  254. schema="test_schema")
  255. ))
  256. util.drop_all_tables(e, inspector)
  257. if config.requirements.schemas.enabled_for_config(cfg):
  258. util.drop_all_tables(e, inspector, schema=cfg.test_schema)
  259. if against(cfg, "postgresql"):
  260. from sqlalchemy.dialects import postgresql
  261. for enum in inspector.get_enums("*"):
  262. e.execute(postgresql.DropEnumType(
  263. postgresql.ENUM(
  264. name=enum['name'],
  265. schema=enum['schema'])))
  266. @post
  267. def _reverse_topological(options, file_config):
  268. if options.reversetop:
  269. from sqlalchemy.orm.util import randomize_unitofwork
  270. randomize_unitofwork()
  271. @post
  272. def _post_setup_options(opt, file_config):
  273. from sqlalchemy.testing import config
  274. config.options = options
  275. config.file_config = file_config
  276. @post
  277. def _setup_profiling(options, file_config):
  278. from sqlalchemy.testing import profiling
  279. profiling._profile_stats = profiling.ProfileStatsFile(
  280. file_config.get('sqla_testing', 'profile_file'))
  281. def want_class(cls):
  282. if not issubclass(cls, fixtures.TestBase):
  283. return False
  284. elif cls.__name__.startswith('_'):
  285. return False
  286. elif config.options.backend_only and not getattr(cls, '__backend__',
  287. False):
  288. return False
  289. else:
  290. return True
  291. def want_method(cls, fn):
  292. if not fn.__name__.startswith("test_"):
  293. return False
  294. elif fn.__module__ is None:
  295. return False
  296. elif include_tags:
  297. return (
  298. hasattr(cls, '__tags__') and
  299. exclusions.tags(cls.__tags__).include_test(
  300. include_tags, exclude_tags)
  301. ) or (
  302. hasattr(fn, '_sa_exclusion_extend') and
  303. fn._sa_exclusion_extend.include_test(
  304. include_tags, exclude_tags)
  305. )
  306. elif exclude_tags and hasattr(cls, '__tags__'):
  307. return exclusions.tags(cls.__tags__).include_test(
  308. include_tags, exclude_tags)
  309. elif exclude_tags and hasattr(fn, '_sa_exclusion_extend'):
  310. return fn._sa_exclusion_extend.include_test(include_tags, exclude_tags)
  311. else:
  312. return True
  313. def generate_sub_tests(cls, module):
  314. if getattr(cls, '__backend__', False):
  315. for cfg in _possible_configs_for_cls(cls):
  316. name = "%s_%s_%s" % (cls.__name__, cfg.db.name, cfg.db.driver)
  317. subcls = type(
  318. name,
  319. (cls, ),
  320. {
  321. "__only_on__": ("%s+%s" % (cfg.db.name, cfg.db.driver)),
  322. }
  323. )
  324. setattr(module, name, subcls)
  325. yield subcls
  326. else:
  327. yield cls
  328. def start_test_class(cls):
  329. _do_skips(cls)
  330. _setup_engine(cls)
  331. def stop_test_class(cls):
  332. #from sqlalchemy import inspect
  333. #assert not inspect(testing.db).get_table_names()
  334. engines.testing_reaper._stop_test_ctx()
  335. try:
  336. if not options.low_connections:
  337. assertions.global_cleanup_assertions()
  338. finally:
  339. _restore_engine()
  340. def _restore_engine():
  341. config._current.reset(testing)
  342. def final_process_cleanup():
  343. engines.testing_reaper._stop_test_ctx_aggressive()
  344. assertions.global_cleanup_assertions()
  345. _restore_engine()
  346. def _setup_engine(cls):
  347. if getattr(cls, '__engine_options__', None):
  348. eng = engines.testing_engine(options=cls.__engine_options__)
  349. config._current.push_engine(eng, testing)
  350. def before_test(test, test_module_name, test_class, test_name):
  351. # like a nose id, e.g.:
  352. # "test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause"
  353. name = test_class.__name__
  354. suffix = "_%s_%s" % (config.db.name, config.db.driver)
  355. if name.endswith(suffix):
  356. name = name[0:-(len(suffix))]
  357. id_ = "%s.%s.%s" % (test_module_name, name, test_name)
  358. profiling._current_test = id_
  359. def after_test(test):
  360. engines.testing_reaper._after_test_ctx()
  361. def _possible_configs_for_cls(cls, reasons=None):
  362. all_configs = set(config.Config.all_configs())
  363. if cls.__unsupported_on__:
  364. spec = exclusions.db_spec(*cls.__unsupported_on__)
  365. for config_obj in list(all_configs):
  366. if spec(config_obj):
  367. all_configs.remove(config_obj)
  368. if getattr(cls, '__only_on__', None):
  369. spec = exclusions.db_spec(*util.to_list(cls.__only_on__))
  370. for config_obj in list(all_configs):
  371. if not spec(config_obj):
  372. all_configs.remove(config_obj)
  373. if hasattr(cls, '__requires__'):
  374. requirements = config.requirements
  375. for config_obj in list(all_configs):
  376. for requirement in cls.__requires__:
  377. check = getattr(requirements, requirement)
  378. skip_reasons = check.matching_config_reasons(config_obj)
  379. if skip_reasons:
  380. all_configs.remove(config_obj)
  381. if reasons is not None:
  382. reasons.extend(skip_reasons)
  383. break
  384. if hasattr(cls, '__prefer_requires__'):
  385. non_preferred = set()
  386. requirements = config.requirements
  387. for config_obj in list(all_configs):
  388. for requirement in cls.__prefer_requires__:
  389. check = getattr(requirements, requirement)
  390. if not check.enabled_for_config(config_obj):
  391. non_preferred.add(config_obj)
  392. if all_configs.difference(non_preferred):
  393. all_configs.difference_update(non_preferred)
  394. return all_configs
  395. def _do_skips(cls):
  396. reasons = []
  397. all_configs = _possible_configs_for_cls(cls, reasons)
  398. if getattr(cls, '__skip_if__', False):
  399. for c in getattr(cls, '__skip_if__'):
  400. if c():
  401. config.skip_test("'%s' skipped by %s" % (
  402. cls.__name__, c.__name__)
  403. )
  404. if not all_configs:
  405. if getattr(cls, '__backend__', False):
  406. msg = "'%s' unsupported for implementation '%s'" % (
  407. cls.__name__, cls.__only_on__)
  408. else:
  409. msg = "'%s' unsupported on any DB implementation %s%s" % (
  410. cls.__name__,
  411. ", ".join(
  412. "'%s(%s)+%s'" % (
  413. config_obj.db.name,
  414. ".".join(
  415. str(dig) for dig in
  416. config_obj.db.dialect.server_version_info),
  417. config_obj.db.driver
  418. )
  419. for config_obj in config.Config.all_configs()
  420. ),
  421. ", ".join(reasons)
  422. )
  423. config.skip_test(msg)
  424. elif hasattr(cls, '__prefer_backends__'):
  425. non_preferred = set()
  426. spec = exclusions.db_spec(*util.to_list(cls.__prefer_backends__))
  427. for config_obj in all_configs:
  428. if not spec(config_obj):
  429. non_preferred.add(config_obj)
  430. if all_configs.difference(non_preferred):
  431. all_configs.difference_update(non_preferred)
  432. if config._current not in all_configs:
  433. _setup_config(all_configs.pop(), cls)
  434. def _setup_config(config_obj, ctx):
  435. config._current.push(config_obj, testing)