Why the diagrams look like this¶
Order, not just containment¶
An earlier version of this site drew only containment - an arrow meant
"called inside", never "then". pytest_runtest_protocol fanned out into eight
unordered boxes, and the thing people actually get wrong stayed invisible:
setup, call and teardown are each followed by their own makereport and
logreport pair. Three reports per test, not one.
Arrows now mean what happened next, in the order the trace recorded. Nesting is drawn as a box inside a box.
Why some stretches are folded away¶
A run has stretches where nothing new happens for a long time. The xdist
controller's run loop is over a hundred steps of logstart, logreport,
report_from_serializable and logfinish as results come back from workers -
in whatever order the workers happened to finish, which is why they do not
collapse into a repeat count the way identical consecutive steps do.
Drawn in full that is seven thousand pixels saying "reports came back". So one cycle is drawn and the rest becomes a dashed box naming the hooks it stands for. Folding happens when drawing only: it never enters the fingerprint that decides which releases share a flow, and the hook table below each diagram still lists everything that ran.
Why the implementers are in that order¶
pluggy does not call a hook's implementations in registration order. Wrappers
run outermost, then anything marked tryfirst, then ordinary implementations,
then trylast. The hook table lists them in the order they actually run and
annotates the ones carrying a marker, so the sequence explains itself:
_pytest.logging.LoggingPlugin [wrapper]
_pytest.capture.CaptureManager [wrapper]
_pytest.skipping [tryfirst]
conftest.py
_pytest.runner
_pytest.unraisableexception [trylast]
_pytest.threadexception [trylast]
For a firstresult hook this decides which implementation wins: the first one
to return something other than None ends the call.
Colour means phase; darkness means frequency¶
Two channels, kept separate. Hue tells you which part of the run a hook belongs to. How dark a step is tells you how often it was called - on a logarithmic scale, because counts run from 1 to about 80 in a single session and a linear ramp would flatten everything below ten into the palest shade.
Nothing else uses colour. In particular, differences between pytest versions deliberately do not get one: a hook appearing or disappearing is something you can find with ctrl-F, and a hook moving is surfaced by grouping instead, which is where it belongs.
The palette was measured, not chosen¶
The four phase colours were picked by simulating dichromatic vision and maximising the worst-case separation across normal, protanopia, deuteranopia and tritanopia.
This was not a formality. The obvious palette - the Google-ish blue, green, amber and purple that most tools reach for - put two of its four hues at a colour difference of 3.9 under deuteranopia. That is effectively indistinguishable, and deuteranopia affects roughly 1 in 12 men. Those readers would have seen the first and last phases as one colour. The palette in use scores 48.5 on the same measure.
Every colour carrying text or a border is also pushed toward or away from the page background until it clears the WCAG AA contrast ratio, separately for the light and dark themes. Raw hues failed badly here: one scored 1.6:1 as a title on white, another 1.3:1 on the dark background.
There is deliberately no green and no alarm-red. The phases are categories, not statuses, and a traffic-light reading would imply an ordering - "collection passed, the run-test phase is a warning" - that does not exist.
Finally, colour is redundant throughout. Every column also carries a title, a border and a fixed position, and every hook's semantics are written out in words beneath its name rather than encoded in a shape. A reader who cannot distinguish the hues loses an accelerator, never information.
Overview first, detail below¶
The overview is depth-limited so the whole session fits on one screen. At full depth the collection column alone runs to 29 nested steps and the page is about two and a half screens tall.
That collection dwarfs the other columns is not a flaw in the layout. Most of pytest's machinery is in startup and collection; running a test is largely just calling a Python function, and the phases after it are mostly reporting. The picture should say that, so it does.
Hooks that are not drawn faithfully¶
Two hooks are excluded from the fingerprint that decides when versions are grouped, though they are still drawn:
pytest_plugin_registered fires once per registered plugin, so its count
tracks how many internal plugins a pytest release happens to ship. Two releases
can differ by that alone, which is not a change worth a separate page.
pytest_warning_recorded is deferred. pytest wraps five hooks in
warnings.catch_warnings(record=True) and replays the whole batch from a
finally block once the wrapped phase ends. So its position in the diagram
marks a phase boundary rather than the moment a warning was raised, and its
count depends on whatever happened to warn.