__init__.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """
  2. support for presenting detailed information in failing assertions.
  3. """
  4. import py
  5. import os
  6. import sys
  7. from _pytest.monkeypatch import monkeypatch
  8. from _pytest.assertion import util
  9. def pytest_addoption(parser):
  10. group = parser.getgroup("debugconfig")
  11. group.addoption('--assert',
  12. action="store",
  13. dest="assertmode",
  14. choices=("rewrite", "reinterp", "plain",),
  15. default="rewrite",
  16. metavar="MODE",
  17. help="""control assertion debugging tools. 'plain'
  18. performs no assertion debugging. 'reinterp'
  19. reinterprets assert statements after they failed
  20. to provide assertion expression information.
  21. 'rewrite' (the default) rewrites assert
  22. statements in test modules on import to
  23. provide assert expression information. """)
  24. group.addoption('--no-assert',
  25. action="store_true",
  26. default=False,
  27. dest="noassert",
  28. help="DEPRECATED equivalent to --assert=plain")
  29. group.addoption('--nomagic', '--no-magic',
  30. action="store_true",
  31. default=False,
  32. help="DEPRECATED equivalent to --assert=plain")
  33. class AssertionState:
  34. """State for the assertion plugin."""
  35. def __init__(self, config, mode):
  36. self.mode = mode
  37. self.trace = config.trace.root.get("assertion")
  38. def pytest_configure(config):
  39. mode = config.getvalue("assertmode")
  40. if config.getvalue("noassert") or config.getvalue("nomagic"):
  41. mode = "plain"
  42. if mode == "rewrite":
  43. try:
  44. import ast # noqa
  45. except ImportError:
  46. mode = "reinterp"
  47. else:
  48. # Both Jython and CPython 2.6.0 have AST bugs that make the
  49. # assertion rewriting hook malfunction.
  50. if (sys.platform.startswith('java') or
  51. sys.version_info[:3] == (2, 6, 0)):
  52. mode = "reinterp"
  53. if mode != "plain":
  54. _load_modules(mode)
  55. m = monkeypatch()
  56. config._cleanup.append(m.undo)
  57. m.setattr(py.builtin.builtins, 'AssertionError',
  58. reinterpret.AssertionError) # noqa
  59. hook = None
  60. if mode == "rewrite":
  61. hook = rewrite.AssertionRewritingHook() # noqa
  62. sys.meta_path.insert(0, hook)
  63. warn_about_missing_assertion(mode)
  64. config._assertstate = AssertionState(config, mode)
  65. config._assertstate.hook = hook
  66. config._assertstate.trace("configured with mode set to %r" % (mode,))
  67. def undo():
  68. hook = config._assertstate.hook
  69. if hook is not None and hook in sys.meta_path:
  70. sys.meta_path.remove(hook)
  71. config.add_cleanup(undo)
  72. def pytest_collection(session):
  73. # this hook is only called when test modules are collected
  74. # so for example not in the master process of pytest-xdist
  75. # (which does not collect test modules)
  76. hook = session.config._assertstate.hook
  77. if hook is not None:
  78. hook.set_session(session)
  79. def _running_on_ci():
  80. """Check if we're currently running on a CI system."""
  81. env_vars = ['CI', 'BUILD_NUMBER']
  82. return any(var in os.environ for var in env_vars)
  83. def pytest_runtest_setup(item):
  84. """Setup the pytest_assertrepr_compare hook
  85. The newinterpret and rewrite modules will use util._reprcompare if
  86. it exists to use custom reporting via the
  87. pytest_assertrepr_compare hook. This sets up this custom
  88. comparison for the test.
  89. """
  90. def callbinrepr(op, left, right):
  91. """Call the pytest_assertrepr_compare hook and prepare the result
  92. This uses the first result from the hook and then ensures the
  93. following:
  94. * Overly verbose explanations are dropped unless -vv was used or
  95. running on a CI.
  96. * Embedded newlines are escaped to help util.format_explanation()
  97. later.
  98. * If the rewrite mode is used embedded %-characters are replaced
  99. to protect later % formatting.
  100. The result can be formatted by util.format_explanation() for
  101. pretty printing.
  102. """
  103. hook_result = item.ihook.pytest_assertrepr_compare(
  104. config=item.config, op=op, left=left, right=right)
  105. for new_expl in hook_result:
  106. if new_expl:
  107. if (sum(len(p) for p in new_expl[1:]) > 80*8 and
  108. item.config.option.verbose < 2 and
  109. not _running_on_ci()):
  110. show_max = 10
  111. truncated_lines = len(new_expl) - show_max
  112. new_expl[show_max:] = [py.builtin._totext(
  113. 'Detailed information truncated (%d more lines)'
  114. ', use "-vv" to show' % truncated_lines)]
  115. new_expl = [line.replace("\n", "\\n") for line in new_expl]
  116. res = py.builtin._totext("\n~").join(new_expl)
  117. if item.config.getvalue("assertmode") == "rewrite":
  118. res = res.replace("%", "%%")
  119. return res
  120. util._reprcompare = callbinrepr
  121. def pytest_runtest_teardown(item):
  122. util._reprcompare = None
  123. def pytest_sessionfinish(session):
  124. hook = session.config._assertstate.hook
  125. if hook is not None:
  126. hook.session = None
  127. def _load_modules(mode):
  128. """Lazily import assertion related code."""
  129. global rewrite, reinterpret
  130. from _pytest.assertion import reinterpret # noqa
  131. if mode == "rewrite":
  132. from _pytest.assertion import rewrite # noqa
  133. def warn_about_missing_assertion(mode):
  134. try:
  135. assert False
  136. except AssertionError:
  137. pass
  138. else:
  139. if mode == "rewrite":
  140. specifically = ("assertions which are not in test modules "
  141. "will be ignored")
  142. else:
  143. specifically = "failing tests may report as passing"
  144. sys.stderr.write("WARNING: " + specifically +
  145. " because assert statements are not executed "
  146. "by the underlying Python interpreter "
  147. "(are you using python -O?)\n")
  148. # Expose this plugin's implementation for the pytest_assertrepr_compare hook
  149. pytest_assertrepr_compare = util.assertrepr_compare