How it works
Worth knowing before you trust it with your suite. Several of these decisions look wrong until you know what they are avoiding.
It does not replace pytest’s reporter
An earlier version unregistered pytest’s TerminalReporter and substituted a
subclass of it. This one leaves it in place — so any plugin that looks it up
still finds it — and quietens it through documented options: verbose = -2,
no_header, an emptied reportchars, colour off, plus a wrapper around
pytest_report_teststatus that drops the progress characters while preserving
pytest’s own categorization.
Two of those are deliberate avoidances rather than obvious choices.
no_summary is not used, although it looks like the switch for the job. It
gates the whole pytest_terminal_summary hook, which is where third-party
plugins write, so setting it swallowed pytest-cov’s coverage table entirely.
Silencing pytest must not silence everyone else. Reported upstream as
pytest#14724.
tbstyle is not set at configure time. Setting it to "no" looks like the
clean way to suppress tracebacks, and it is: it also impoverishes longrepr at
construction time — the same failure renders to 145 characters under short
and 21 under no — which destroys the frame data this plugin exists to
summarize. Suppression happens at session finish instead, once every longrepr
has been built. Reported upstream as
pytest#14720.
It collects from public hooks
pytest_runtest_logreport for phase results, pytest_collectreport for
collection failures, pytest_warning_recorded for warnings, and
pytest_sessionfinish to render.
Rendering happens in sessionfinish rather than terminal_summary because
pytest does not call the latter for INTERNAL_ERROR — which is exactly when a
consumer most needs to be told what happened.
Grouping is call-site aware
The key is exception type, phase, crash location, and cause chain.
The message is deliberately excluded. Keying on it fragmented a parametrized test into one group per input, which defeats grouping precisely where suites are most repetitive. Differing messages are kept as variants inside the group and shown.
Using the crash location rather than the test line is what makes this
correct: a bug in merge.py:117 groups every caller, wherever their tests live.
The cause chain is part of the key, so one wrapper over two different underlying failures stays two bugs.
Warnings are grouped separately, by category and normalized message, with numbers and shapes normalized but names left alone — a size mismatch is the same warning at any size, while a different attribute is different information.
Tracebacks keep the decisive frame
Every local frame survives, because that is the code you can change. External frames are pruned to the boundary where you entered the dependency and the frame that actually broke, with elisions marked:
frames: tests/test_merge.py:12 -> molsysmt/merge.py:41 -> numpy/core/shape.py:88 (ext) -> ... -> numpy/core/_methods.py:52 (ext)
Dropping external frames entirely is cheaper and wrong: when a failure originates inside NumPy or a serializer, the external frame is the answer.
Nothing is deferred to a second run
Grouping is a presentation decision. Every occurrence keeps its node ID, phase, and location, and the complete report is written to the pytest cache while the run is still going.
Detail is only ever held back when that file exists to hold it — with
-p no:cacheprovider nothing is withheld at all, because there would be nowhere
to recover it from. A consumer can never be left with information reachable only
by running the suite again.
Paths resolve from where you invoked pytest
Not from rootpath. Naming a test outside the project sets pytest’s rootdir to
the common ancestor, and paths relative to that resolve from nowhere: a rerun
command came back file or directory not found, and mypkg/__init__.py
rendered as mypkg/mypkg/__init__.py.
Everything printed — crash locations, the frame chain, rerun commands, and the
node IDs in occurrence lists — is relative to invocation_params.dir. A test far
outside the tree is printed absolute rather than as a chain of ../.., so the
contract is that a path resolves, not that it is relative.
Distributed runs
The plugin is instantiated in every xdist worker as well as the controller. Each worker collects the whole suite but finishes only its share, so left alone all twelve announce their own progress at their own pace.
Only the controller emits, and it takes the collected total from
pytest_xdist_node_collection_finished, declared optional so the plugin still
loads where xdist is absent.
Reports arrive in whatever order workers finish, so occurrences and groups are given a total order before rendering. A distributed run therefore produces byte-identical output to a serial one.
It degrades rather than fails
The whole render is wrapped. Any receptor exception produces RECEPTOR_ERROR,
the underlying traceback, the raw pytest evidence, and pytest’s original exit
status.
The worst case of enabling this plugin is standard pytest plus one line of
noise. That is what makes it safe to put in addopts.