| """ |
| support for presenting detailed information in failing assertions. |
| """ |
| import py |
| import os |
| import sys |
| from _pytest.monkeypatch import monkeypatch |
| from _pytest.assertion import util |
| |
| |
| def pytest_addoption(parser): |
| group = parser.getgroup("debugconfig") |
| group.addoption('--assert', |
| action="store", |
| dest="assertmode", |
| choices=("rewrite", "reinterp", "plain",), |
| default="rewrite", |
| metavar="MODE", |
| help="""control assertion debugging tools. 'plain' |
| performs no assertion debugging. 'reinterp' |
| reinterprets assert statements after they failed |
| to provide assertion expression information. |
| 'rewrite' (the default) rewrites assert |
| statements in test modules on import to |
| provide assert expression information. """) |
| group.addoption('--no-assert', |
| action="store_true", |
| default=False, |
| dest="noassert", |
| help="DEPRECATED equivalent to --assert=plain") |
| group.addoption('--nomagic', '--no-magic', |
| action="store_true", |
| default=False, |
| help="DEPRECATED equivalent to --assert=plain") |
| |
| |
| class AssertionState: |
| """State for the assertion plugin.""" |
| |
| def __init__(self, config, mode): |
| self.mode = mode |
| self.trace = config.trace.root.get("assertion") |
| |
| |
| def pytest_configure(config): |
| mode = config.getvalue("assertmode") |
| if config.getvalue("noassert") or config.getvalue("nomagic"): |
| mode = "plain" |
| if mode == "rewrite": |
| try: |
| import ast # noqa |
| except ImportError: |
| mode = "reinterp" |
| else: |
| # Both Jython and CPython 2.6.0 have AST bugs that make the |
| # assertion rewriting hook malfunction. |
| if (sys.platform.startswith('java') or |
| sys.version_info[:3] == (2, 6, 0)): |
| mode = "reinterp" |
| if mode != "plain": |
| _load_modules(mode) |
| m = monkeypatch() |
| config._cleanup.append(m.undo) |
| m.setattr(py.builtin.builtins, 'AssertionError', |
| reinterpret.AssertionError) # noqa |
| hook = None |
| if mode == "rewrite": |
| hook = rewrite.AssertionRewritingHook() # noqa |
| sys.meta_path.insert(0, hook) |
| warn_about_missing_assertion(mode) |
| config._assertstate = AssertionState(config, mode) |
| config._assertstate.hook = hook |
| config._assertstate.trace("configured with mode set to %r" % (mode,)) |
| def undo(): |
| hook = config._assertstate.hook |
| if hook is not None and hook in sys.meta_path: |
| sys.meta_path.remove(hook) |
| config.add_cleanup(undo) |
| |
| |
| def pytest_collection(session): |
| # this hook is only called when test modules are collected |
| # so for example not in the master process of pytest-xdist |
| # (which does not collect test modules) |
| hook = session.config._assertstate.hook |
| if hook is not None: |
| hook.set_session(session) |
| |
| |
| def _running_on_ci(): |
| """Check if we're currently running on a CI system.""" |
| env_vars = ['CI', 'BUILD_NUMBER'] |
| return any(var in os.environ for var in env_vars) |
| |
| |
| def pytest_runtest_setup(item): |
| """Setup the pytest_assertrepr_compare hook |
| |
| The newinterpret and rewrite modules will use util._reprcompare if |
| it exists to use custom reporting via the |
| pytest_assertrepr_compare hook. This sets up this custom |
| comparison for the test. |
| """ |
| def callbinrepr(op, left, right): |
| """Call the pytest_assertrepr_compare hook and prepare the result |
| |
| This uses the first result from the hook and then ensures the |
| following: |
| * Overly verbose explanations are dropped unless -vv was used or |
| running on a CI. |
| * Embedded newlines are escaped to help util.format_explanation() |
| later. |
| * If the rewrite mode is used embedded %-characters are replaced |
| to protect later % formatting. |
| |
| The result can be formatted by util.format_explanation() for |
| pretty printing. |
| """ |
| hook_result = item.ihook.pytest_assertrepr_compare( |
| config=item.config, op=op, left=left, right=right) |
| for new_expl in hook_result: |
| if new_expl: |
| if (sum(len(p) for p in new_expl[1:]) > 80*8 and |
| item.config.option.verbose < 2 and |
| not _running_on_ci()): |
| show_max = 10 |
| truncated_lines = len(new_expl) - show_max |
| new_expl[show_max:] = [py.builtin._totext( |
| 'Detailed information truncated (%d more lines)' |
| ', use "-vv" to show' % truncated_lines)] |
| new_expl = [line.replace("\n", "\\n") for line in new_expl] |
| res = py.builtin._totext("\n~").join(new_expl) |
| if item.config.getvalue("assertmode") == "rewrite": |
| res = res.replace("%", "%%") |
| return res |
| util._reprcompare = callbinrepr |
| |
| |
| def pytest_runtest_teardown(item): |
| util._reprcompare = None |
| |
| |
| def pytest_sessionfinish(session): |
| hook = session.config._assertstate.hook |
| if hook is not None: |
| hook.session = None |
| |
| |
| def _load_modules(mode): |
| """Lazily import assertion related code.""" |
| global rewrite, reinterpret |
| from _pytest.assertion import reinterpret # noqa |
| if mode == "rewrite": |
| from _pytest.assertion import rewrite # noqa |
| |
| |
| def warn_about_missing_assertion(mode): |
| try: |
| assert False |
| except AssertionError: |
| pass |
| else: |
| if mode == "rewrite": |
| specifically = ("assertions which are not in test modules " |
| "will be ignored") |
| else: |
| specifically = "failing tests may report as passing" |
| |
| sys.stderr.write("WARNING: " + specifically + |
| " because assert statements are not executed " |
| "by the underlying Python interpreter " |
| "(are you using python -O?)\n") |
| |
| |
| # Expose this plugin's implementation for the pytest_assertrepr_compare hook |
| pytest_assertrepr_compare = util.assertrepr_compare |