pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
12.1k stars 2.68k forks source link

Presence of a plugin causes doctests a shared namespace to fail traceback with assertion #8059

Open jaraco opened 3 years ago

jaraco commented 3 years ago

In jaraco/jaraco.test#3, I've described an issue that appears to implicate some of the most edgy features of pytest and Python, in particular:

Oh, joy.

In summary, the issue seems to be that:

The presence of an empty module as a plugin in a namespace shared by the package under test causes assert exceptions in doctests to be re-written, causing the output from those doctests to fail where they wouldn't otherwise, even if the plugin is disabled with -p no:....

As you can see from the tox execution in the downstream report, I'm using pytest 6.1.2 on Python 3.9.0 with jaraco.test 4.0.1 (the implicated plugin) on macOS 11.0.1 (latest stable everything as of this writing).

I don't yet have a minimal example, though you can check out jaraco/jaraco.itertools@e9f23a4 and run tox -- -k assert to see the error.

I expect (though haven't verified) that any of the following remedies might be possible:

None of these workarounds would address the root cause, but instead avoid the confounding factors leading to the failed expectation.

I recognize this issue is probably too obscure to expect it to be fixed upstream, but I could really use some advice on how to proceed with an investigation. In particular, can you explain or point me to the implementation where doctests get special treatment for assert-rewrite (or vice-versa), especially any code that might be relevant for selectively affecting the modules-under-test or plugins?

jaraco commented 3 years ago

In the referenced commit, I've confirmed and committed the second workaround, strengthening my suspicion that assert rewrite is implicated.

jaraco commented 3 years ago

I took a quick look at _pytest.assertion.rewrite, I'm reminded that rewrites are done through a special importer, so it's probably not a surprise that if a namespace (jaraco) is initialized during plugin setup might affect the timing of rewrites for that package (and maybe modules/packages in that namespace).

I notice #2371 and #2419 may be related, though neither of those reference doctests.

Probably the most helpful question to be answered: how is it that in the simple, general case, doctests that trigger assertions don't stumble on rewritten assertions?

nicoddemus commented 3 years ago

Hi @jaraco,

Sorry for the delay.

The assertion rewriter is triggered automatically in test modules (normal test_*.py files) and all python files of a plugin. The rationale for the latter is that plugins often use assert themselves as part of their functionality, so we want to rewrite those.

The code that marks plugin files for rewriting is found here:

https://github.com/pytest-dev/pytest/blob/775ba63c67f85d8dd50326ab9e3e3710483480e0/src/_pytest/config/__init__.py#L1115-L1133

This iterates over all the files in a plugin package and marks their asserts for later rewriting on import.

As an example, pytest-qt has the entry-point pytest11 = pytestqt.plugin, and the asserts of all files inside the package are rewritten, even if they are not below the entry point file (for example pytestqt.wait_signal).

So your assessment is correct: this mechanism is rewriting all asserts inside the jaraco.* namespace, because the pytest11 entry point is jaraco.test.pytest.enabler, so all files found in the jaraco distribution get rewritten.

I suspect your case is not isolated, the rewriter is probably being triggered in other packages which include a pytest plugin, although it should only cause problems if someone expects assertion errors in a specific format (like doctests), as the rewriter only changes the assertion message/traceback.

Probably the most helpful question to be answered: how is it that in the simple, general case, doctests that trigger assertions don't stumble on rewritten assertions?

In the general case doctests are not part of a plugin, but normal user code, so they don't get rewritten.

As for workarounds, your two first suggestions are on-point, as you confirmed by using raise AssertionError. Another would be to disable assertion rewriting in that module explicitly.

About fixing this in pytest, I'm not sure how we could detect that part of a plugin is normal code, which shouldn't be changed by the assertion rewriter, and other part is "testing" code, which should have their assertions rewritten.