pytestplugin.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. try:
  2. # installed by bootstrap.py
  3. import sqla_plugin_base as plugin_base
  4. except ImportError:
  5. # assume we're a package, use traditional import
  6. from . import plugin_base
  7. import pytest
  8. import argparse
  9. import inspect
  10. import collections
  11. import os
  12. try:
  13. import xdist # noqa
  14. has_xdist = True
  15. except ImportError:
  16. has_xdist = False
  17. def pytest_addoption(parser):
  18. group = parser.getgroup("sqlalchemy")
  19. def make_option(name, **kw):
  20. callback_ = kw.pop("callback", None)
  21. if callback_:
  22. class CallableAction(argparse.Action):
  23. def __call__(self, parser, namespace,
  24. values, option_string=None):
  25. callback_(option_string, values, parser)
  26. kw["action"] = CallableAction
  27. group.addoption(name, **kw)
  28. plugin_base.setup_options(make_option)
  29. plugin_base.read_config()
  30. def pytest_configure(config):
  31. if hasattr(config, "slaveinput"):
  32. plugin_base.restore_important_follower_config(config.slaveinput)
  33. plugin_base.configure_follower(
  34. config.slaveinput["follower_ident"]
  35. )
  36. if config.option.write_idents:
  37. with open(config.option.write_idents, "a") as file_:
  38. file_.write(config.slaveinput["follower_ident"] + "\n")
  39. else:
  40. if config.option.write_idents and \
  41. os.path.exists(config.option.write_idents):
  42. os.remove(config.option.write_idents)
  43. plugin_base.pre_begin(config.option)
  44. plugin_base.set_coverage_flag(bool(getattr(config.option,
  45. "cov_source", False)))
  46. plugin_base.set_skip_test(pytest.skip.Exception)
  47. def pytest_sessionstart(session):
  48. plugin_base.post_begin()
  49. def pytest_sessionfinish(session):
  50. plugin_base.final_process_cleanup()
  51. if has_xdist:
  52. import uuid
  53. def pytest_configure_node(node):
  54. # the master for each node fills slaveinput dictionary
  55. # which pytest-xdist will transfer to the subprocess
  56. plugin_base.memoize_important_follower_config(node.slaveinput)
  57. node.slaveinput["follower_ident"] = "test_%s" % uuid.uuid4().hex[0:12]
  58. from sqlalchemy.testing import provision
  59. provision.create_follower_db(node.slaveinput["follower_ident"])
  60. def pytest_testnodedown(node, error):
  61. from sqlalchemy.testing import provision
  62. provision.drop_follower_db(node.slaveinput["follower_ident"])
  63. def pytest_collection_modifyitems(session, config, items):
  64. # look for all those classes that specify __backend__ and
  65. # expand them out into per-database test cases.
  66. # this is much easier to do within pytest_pycollect_makeitem, however
  67. # pytest is iterating through cls.__dict__ as makeitem is
  68. # called which causes a "dictionary changed size" error on py3k.
  69. # I'd submit a pullreq for them to turn it into a list first, but
  70. # it's to suit the rather odd use case here which is that we are adding
  71. # new classes to a module on the fly.
  72. rebuilt_items = collections.defaultdict(list)
  73. items[:] = [
  74. item for item in
  75. items if isinstance(item.parent, pytest.Instance)
  76. and not item.parent.parent.name.startswith("_")]
  77. test_classes = set(item.parent for item in items)
  78. for test_class in test_classes:
  79. for sub_cls in plugin_base.generate_sub_tests(
  80. test_class.cls, test_class.parent.module):
  81. if sub_cls is not test_class.cls:
  82. list_ = rebuilt_items[test_class.cls]
  83. for inst in pytest.Class(
  84. sub_cls.__name__,
  85. parent=test_class.parent.parent).collect():
  86. list_.extend(inst.collect())
  87. newitems = []
  88. for item in items:
  89. if item.parent.cls in rebuilt_items:
  90. newitems.extend(rebuilt_items[item.parent.cls])
  91. rebuilt_items[item.parent.cls][:] = []
  92. else:
  93. newitems.append(item)
  94. # seems like the functions attached to a test class aren't sorted already?
  95. # is that true and why's that? (when using unittest, they're sorted)
  96. items[:] = sorted(newitems, key=lambda item: (
  97. item.parent.parent.parent.name,
  98. item.parent.parent.name,
  99. item.name
  100. ))
  101. def pytest_pycollect_makeitem(collector, name, obj):
  102. if inspect.isclass(obj) and plugin_base.want_class(obj):
  103. return pytest.Class(name, parent=collector)
  104. elif inspect.isfunction(obj) and \
  105. isinstance(collector, pytest.Instance) and \
  106. plugin_base.want_method(collector.cls, obj):
  107. return pytest.Function(name, parent=collector)
  108. else:
  109. return []
  110. _current_class = None
  111. def pytest_runtest_setup(item):
  112. # here we seem to get called only based on what we collected
  113. # in pytest_collection_modifyitems. So to do class-based stuff
  114. # we have to tear that out.
  115. global _current_class
  116. if not isinstance(item, pytest.Function):
  117. return
  118. # ... so we're doing a little dance here to figure it out...
  119. if _current_class is None:
  120. class_setup(item.parent.parent)
  121. _current_class = item.parent.parent
  122. # this is needed for the class-level, to ensure that the
  123. # teardown runs after the class is completed with its own
  124. # class-level teardown...
  125. def finalize():
  126. global _current_class
  127. class_teardown(item.parent.parent)
  128. _current_class = None
  129. item.parent.parent.addfinalizer(finalize)
  130. test_setup(item)
  131. def pytest_runtest_teardown(item):
  132. # ...but this works better as the hook here rather than
  133. # using a finalizer, as the finalizer seems to get in the way
  134. # of the test reporting failures correctly (you get a bunch of
  135. # py.test assertion stuff instead)
  136. test_teardown(item)
  137. def test_setup(item):
  138. plugin_base.before_test(item, item.parent.module.__name__,
  139. item.parent.cls, item.name)
  140. def test_teardown(item):
  141. plugin_base.after_test(item)
  142. def class_setup(item):
  143. plugin_base.start_test_class(item.cls)
  144. def class_teardown(item):
  145. plugin_base.stop_test_class(item.cls)