Distributed across workers¶
pytest 7.0.0 - 7.0.1 - The controller and its workers, which see completely different runs.
These 2 releases produced an identical flow, so they share one page. Identical as far as this scenario can measure - a scenario exercising more hooks may tell them apart.
Run under pytest-xdist, a session is several processes, and they do not see
the same thing at all. The process you launched - the controller - never
collects a test and never runs one. It starts workers, hands out work, and
receives results. Each worker collects the entire suite for itself, then runs
whatever it was given.
That is why this page has a diagram per process rather than one. The controller reaches around 25 hooks, a worker around 41, and the overlap between them is small.
The scheduler changes what you see, so the command is printed with every
diagram below. This capture uses --dist each, which gives every worker the
whole suite: the workers are then identical, and the page draws them once.
Under a splitting scheduler - loadfile, loadscope, load, worksteal -
workers differ by the tests they happened to draw, and this page would show one
section per distinct worker flow. The controller differs too: those four modes
all produce one controller flow, and each produces another.
This scenario also reaches the last two hooks pytest declares but never calls
itself: pytest_report_to_serializable in the workers, where a result is packed
up to be sent, and pytest_report_from_serializable in the controller, where it
is unpacked. And it brings twelve hooks of xdist's own, which pytest knows
nothing about - they are described here because hookspecs are read from the
running plugin manager rather than from pytest's source.
How this was produced¶
- Source:
scenarios/xdist/- the project this diagram was traced from - Invocation:
pytest -p hook_atlas_pytest . -q -n 2 --dist each - Traced against: pytest 7.0.1, pluggy 1.6.0, Python 3.9.25
- Covers: 2 releases - pytest 7.0.0 - 7.0.1, which produced an identical flow
- Observed: 534 hook calls, 26 distinct hooks
The project is test_alpha.py, test_beta.py, test_delta.py, test_gamma.py, test_outcomes.py. Browse it in the repository rather than inline here - scenarios grow directory trees, and GitHub renders them better than a collapsed block can.
The controller¶
Command: pytest -p hook_atlas_pytest . -q -n 2 --dist each
Phases left to right, steps top to bottom. Colour says which phase a hook belongs to; how dark a step is says how often it was called. Click a column heading to jump to that phase in full detail, or any hook to open its pytest documentation. Why it looks like this
Startup and configuration (controller)¶
Everything before collection begins: initial conftests are loaded, plugins register, and the session is configured.
Collection (controller)¶
Finding tests. Note that pytest_make_collect_report recurses - directories contain directories contain files.
The run-test protocol (controller)¶
pytest_runtestloop runs this once per collected test. Note the rhythm: setup, call and teardown are each followed by their own makereport and logreport pair - three reports per test, not one.
A dashed box stands in for a long stretch that only reorders the few hooks it names - under xdist, results arriving from workers in whatever order they happened to finish. Every hook is still listed below.
Session finish (controller)¶
Summary reporting and teardown of the session.
Every hook observed (controller)¶
| Hook | Calls | Semantics | Implemented by, in call order |
|---|---|---|---|
pytest_addhooks |
1 | historic | xdist.plugin (xdist) |
pytest_addoption |
2 | historic | xdist.plugin (xdist)xdist.looponfail (xdist.looponfail) |
pytest_cmdline_main |
1 | firstresult | xdist.plugin (xdist) [tryfirst]_pytest.setupplan [tryfirst]_pytest.setuponly [tryfirst]_pytest.mark [tryfirst]xdist.looponfail (xdist.looponfail)_pytest.python_pytest.helpconfig_pytest.main |
pytest_cmdline_preparse |
1 | - | |
pytest_collection |
1 | firstresult | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]_pytest.warnings [hookwrapper, tryfirst]_pytest.config.Config (pytestconfig) [hookwrapper]xdist.dsession.DSession_pytest.terminal.TerminalReporter_pytest.assertion_pytest.main |
pytest_configure |
1 | historic | _pytest.faulthandler_pytest.warnings_pytest.junitxml_pytest.tmpdir_pytest.legacypath_pytest.skipping_pytest.debugging_pytest.terminal_pytest.python_pytest.mark_pytest.config.PytestPluginManager (<anonymous>)_pytest.pastebin [trylast]_pytest.logging [trylast]xdist.plugin (xdist) [trylast] |
pytest_configure_node |
2 | - | |
pytest_load_initial_conftests |
1 | _pytest.warnings [hookwrapper]_pytest.capture [hookwrapper]_pytest.python_path [tryfirst]_pytest.legacypath [tryfirst]_pytest.config.Config (pytestconfig) [trylast]1 |
|
pytest_plugin_registered |
93 | historic | _pytest.legacypath_pytest.terminal.TerminalReporter_pytest.fixtures.FixtureManager (funcmanage) |
pytest_report_from_serializable |
112 | firstresult | _pytest.reports |
pytest_report_teststatus |
114 | firstresult | _pytest.skipping_pytest.runner_pytest.terminal [trylast] |
pytest_runtest_logfinish |
38 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter |
|
pytest_runtest_logreport |
112 | _pytest.main.Session [tryfirst]_pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter |
|
pytest_runtest_logstart |
38 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter |
|
pytest_runtestloop |
1 | firstresult | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper]xdist.dsession.DSession_pytest.main |
pytest_sessionfinish |
1 | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]_pytest.terminal.TerminalReporter [hookwrapper]_pytest.warnings [hookwrapper]xdist.dsession.DSession_pytest.assertion_pytest.runner |
|
pytest_sessionstart |
1 | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]_pytest.fixtures_pytest.runner_pytest.terminal.TerminalReporter [trylast]xdist.dsession.DSession [trylast] |
|
pytest_terminal_summary |
1 | _pytest.terminal.TerminalReporter [hookwrapper]_pytest.warnings [hookwrapper]xdist.dsession.DSession_pytest.pastebin_pytest.runner |
|
pytest_testnodedown |
2 | xdist.dsession.TerminalDistReporter |
|
pytest_testnodeready |
2 | xdist.dsession.TerminalDistReporter |
|
pytest_unconfigure |
1 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter_pytest.faulthandler_pytest.doctest_pytest.junitxml_pytest.pastebin_pytest.mark_pytest.python_path [trylast]2 |
|
pytest_xdist_getremotemodule |
2 | firstresult | xdist.workermanage.WorkerController.RemoteHook (<anonymous>) [trylast] |
pytest_xdist_make_scheduler |
1 | firstresult | xdist.dsession.DSession [trylast] |
pytest_xdist_newgateway |
2 | xdist.dsession.TerminalDistReporter |
|
pytest_xdist_node_collection_finished |
2 | - | |
pytest_xdist_setupnodes |
1 | xdist.dsession.TerminalDistReporter |
2 of these changed implementer somewhere in pytest 7.0.0 - 7.0.1 without changing the flow - pytest moves implementations between its own plugins. The table shows the newest; what changed along the way is below.
Every worker¶
Both workers produced this flow. gw0 is drawn.
Command: none. xdist starts a worker over execnet and hands it a configuration, so it has no command line of its own.
Phases left to right, steps top to bottom. Colour says which phase a hook belongs to; how dark a step is says how often it was called. Click a column heading to jump to that phase in full detail, or any hook to open its pytest documentation. Why it looks like this
Startup and configuration (gw0)¶
Everything before collection begins: initial conftests are loaded, plugins register, and the session is configured.
Collection (gw0)¶
Finding tests. Note that pytest_make_collect_report recurses - directories contain directories contain files.
A dashed box stands in for a long stretch that only reorders the few hooks it names - under xdist, results arriving from workers in whatever order they happened to finish. Every hook is still listed below.
The run-test protocol (gw0)¶
pytest_runtestloop runs this once per collected test. Note the rhythm: setup, call and teardown are each followed by their own makereport and logreport pair - three reports per test, not one.
The path 15 of 19 took.
The others differed:
- 1 test also called
pytest_assertrepr_compare,pytest_exception_interactand skippedpytest_fixture_post_finalizer,pytest_fixture_setup - 1 test skipped
pytest_fixture_post_finalizer,pytest_fixture_setup,pytest_pyfunc_call,pytest_runtest_call - 1 test skipped
pytest_fixture_setup
Session finish (gw0)¶
Summary reporting and teardown of the session.
Every hook observed (gw0)¶
| Hook | Calls | Semantics | Implemented by, in call order |
|---|---|---|---|
pytest_addhooks |
1 | historic | xdist.plugin (xdist) |
pytest_addoption |
2 | historic | xdist.plugin (xdist)xdist.looponfail (xdist.looponfail) |
pytest_assertrepr_compare |
1 | _pytest.assertion |
|
pytest_cmdline_main |
1 | firstresult | xdist.plugin (xdist) [tryfirst]_pytest.setupplan [tryfirst]_pytest.setuponly [tryfirst]_pytest.mark [tryfirst]xdist.looponfail (xdist.looponfail)_pytest.python_pytest.helpconfig_pytest.main |
pytest_cmdline_preparse |
1 | - | |
pytest_collect_file |
8 | _pytest.doctest_pytest.python |
|
pytest_collection |
1 | firstresult | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]_pytest.warnings [hookwrapper, tryfirst]_pytest.config.Config (pytestconfig) [hookwrapper]_pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>)_pytest.assertion_pytest.main |
pytest_collection_finish |
1 | _pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>) |
|
pytest_collection_modifyitems |
1 | _pytest.fixtures.FixtureManager (funcmanage)__channelexec__.WorkerInteractor (<anonymous>)_pytest.main_pytest.mark |
|
pytest_collectreport |
6 | _pytest.main.Session.pytest_runtest_logreport (session) [tryfirst]_pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>) |
|
pytest_collectstart |
6 | _pytest.main.Session [tryfirst] |
|
pytest_configure |
1 | historic | _pytest.faulthandler_pytest.warnings_pytest.junitxml_pytest.tmpdir_pytest.legacypath_pytest.skipping_pytest.debugging_pytest.terminal_pytest.python_pytest.mark_pytest.config.PytestPluginManager (<anonymous>)_pytest.pastebin [trylast]_pytest.logging [trylast]xdist.plugin (xdist) [trylast] |
pytest_exception_interact |
1 | _pytest.faulthandler [tryfirst] |
|
pytest_fixture_post_finalizer |
24 | _pytest.setuponly |
|
pytest_fixture_setup |
17 | firstresult | _pytest.setuponly [hookwrapper]_pytest.setupplan [tryfirst]_pytest.fixtures |
pytest_generate_tests |
11 | _pytest.fixtures.FixtureManager (funcmanage)_pytest.python |
|
pytest_ignore_collect |
8 | firstresult | _pytest.main |
pytest_itemcollected |
19 | - | |
pytest_load_initial_conftests |
1 | _pytest.warnings [hookwrapper]_pytest.capture [hookwrapper]_pytest.python_path [tryfirst]_pytest.legacypath [tryfirst]_pytest.config.Config (pytestconfig) [trylast]1 |
|
pytest_make_collect_report |
6 | firstresult | _pytest.capture.CaptureManager [hookwrapper]_pytest.runner |
pytest_make_parametrize_id |
12 | firstresult | - |
pytest_plugin_registered |
90 | historic | _pytest.legacypath_pytest.terminal.TerminalReporter_pytest.fixtures.FixtureManager (funcmanage) |
pytest_pycollect_makeitem |
26 | firstresult | _pytest.unittest_pytest.python [trylast] |
pytest_pycollect_makemodule |
5 | firstresult | _pytest.python |
pytest_pyfunc_call |
18 | firstresult | _pytest.python [trylast] |
pytest_report_collectionfinish |
1 | - | |
pytest_report_teststatus |
57 | firstresult | _pytest.skipping_pytest.runner_pytest.terminal [trylast] |
pytest_report_to_serializable |
56 | firstresult | _pytest.reports |
pytest_runtest_call |
18 | _pytest.threadexception [hookwrapper, tryfirst]_pytest.unraisableexception [hookwrapper, tryfirst]_pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper]_pytest.capture.CaptureManager [hookwrapper]_pytest.skipping [hookwrapper]_pytest.runner |
|
pytest_runtest_logfinish |
19 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>) |
|
pytest_runtest_logreport |
56 | _pytest.main.Session [tryfirst]_pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>) |
|
pytest_runtest_logstart |
19 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter__channelexec__.WorkerInteractor (<anonymous>) |
|
pytest_runtest_makereport |
56 | firstresult | _pytest.skipping [hookwrapper]_pytest.unittest [tryfirst]_pytest.runner |
pytest_runtest_protocol |
19 | firstresult | _pytest.warnings [hookwrapper, tryfirst]_pytest.assertion [hookwrapper, tryfirst]_pytest.unittest [hookwrapper]_pytest.faulthandler [hookwrapper, trylast]_pytest.runner |
pytest_runtest_setup |
19 | _pytest.unraisableexception [hookwrapper, tryfirst]_pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper]_pytest.capture.CaptureManager [hookwrapper]_pytest.threadexception [hookwrapper, trylast]_pytest.skipping [tryfirst]_pytest.runner_pytest.nose [trylast] |
|
pytest_runtest_teardown |
19 | _pytest.threadexception [hookwrapper, tryfirst]_pytest.unraisableexception [hookwrapper, tryfirst]_pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper]_pytest.capture.CaptureManager [hookwrapper]_pytest.runner |
|
pytest_runtestloop |
1 | firstresult | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper]__channelexec__.WorkerInteractor (<anonymous>)_pytest.main |
pytest_sessionfinish |
1 | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]_pytest.terminal.TerminalReporter [hookwrapper]__channelexec__.WorkerInteractor (<anonymous>) [hookwrapper]_pytest.warnings [hookwrapper]_pytest.assertion_pytest.runner |
|
pytest_sessionstart |
1 | _pytest.logging.LoggingPlugin (logging-plugin) [hookwrapper, tryfirst]__channelexec__.WorkerInteractor (<anonymous>)_pytest.fixtures_pytest.runner_pytest.terminal.TerminalReporter [trylast] |
|
pytest_terminal_summary |
1 | _pytest.terminal.TerminalReporter [hookwrapper]_pytest.warnings [hookwrapper]_pytest.pastebin_pytest.runner |
|
pytest_unconfigure |
1 | _pytest.logging.LoggingPlugin (logging-plugin)_pytest.terminal.TerminalReporter_pytest.faulthandler_pytest.doctest_pytest.junitxml_pytest.pastebin_pytest.mark_pytest.python_path [trylast]2 |
2 of these changed implementer somewhere in pytest 7.0.0 - 7.0.1 without changing the flow - pytest moves implementations between its own plugins. The table shows the newest; what changed along the way is below.