suite.plugin

OttoPlugin — internal pytest plugin registered when otto invokes pytest.main().

Provides the pytest_runtest_makereport hook that attaches the per-phase test report to each item (as item.rep_setup, item.rep_call, item.rep_teardown). This makes pass/fail status available to fixtures (including OttoSuite._test_lifecycle) during the teardown phase.

When sut_test_dirs is supplied, the pytest_ignore_collect hook restricts collection to only those directories and their descendants, ensuring that only tests defined in OTTO_SUT_DIRS repos are run.

Additional hooks:

pytest_runtest_protocol

Implements stability testing (--iterations / --duration). Repeats each test item within a single setup/teardown cycle, stopping when the iteration or time limit is reached.

pytest_runtest_call

Implements @pytest.mark.retry(n) — retries the test body up to n times on failure, stopping on the first success.

pytest_runtest_logreport

In stability mode, accumulates per-test pass/fail counts into the StabilityCollector attached to the plugin instance.

otto.suite.plugin.otto_cov_key : StashKey[bool] = <_pytest.stash.StashKey object>

Stash key indicating that --cov was passed to otto test. Fixtures can read this to decide whether to preserve .gcda files on remote hosts for post-run collection.

class otto.suite.plugin.StabilityCollector

Bases: object

Accumulates per-test pass/fail counts across multiple stability runs.

record(nodeid, passed)
Return type:

None

class otto.suite.plugin.OttoPlugin(sut_test_dirs=None, stability_collector=None, cov=False, iterations=0, duration=0, monitor=False, monitor_interval=5.0, monitor_output=None, monitor_hosts=None)

Bases: object

Internal pytest plugin used by otto test to instrument test runs.

Parameters

sut_test_dirs :

Resolved test directories from all configured OTTO_SUT_DIRS repos (i.e. the union of Repo.tests for every repo). When provided, collection is restricted to these directories. Pass an empty list or omit to disable filtering.

stability_collector :

When running in stability mode, pass a StabilityCollector instance here to accumulate pass/fail counts across repeated runs.

pytest_configure(config)

Register the shared async timeout fixture and enforce auto asyncio mode.

OttoSuites always run with asyncio_mode=auto so that async fixtures and test methods work without explicit @pytest.mark.asyncio markers. This is distinct from otto’s own unit tests which use asyncio_mode=strict (set in pyproject.toml).

Return type:

None

pytest_sessionstart(session)

Quiet down pytest’s terminal reporter output.

Two adjustments, both because otto streams its own Rich log output and pytest’s terse terminal chatter just collides with it. Done here rather than in pytest_configure because the terminalreporter isn’t registered yet at configure time.

showfspath = False: in non-verbose mode pytest writes the test file path with no trailing newline (write_fspath_result), expecting per-test progress letters to follow. otto suppresses those letters (see pytest_report_teststatus()), so the bare path would collide with the first log line. otto’s _otto_log_test_start fixture already logs each test start, making the header redundant.

report_collect: the “collected N items” line has no granular suppression flag — only quiet mode (verbose < 0) hides it, which would strip other output too. The pytest_collection hook writes a bare, un-terminated collecting ... prefix that report_collect normally rewrites in place into collected N items\n; simply no-oping it would leave that prefix dangling. Instead override it to erase the line on the final call and park the cursor at column 0 for the next writer. Collection counts are tracked separately and stay intact.

Return type:

None

pytest_ignore_collect(collection_path, config)

Ignore any path not under a configured SUT test directory.

Returns True (ignore) for paths outside all SUT test dirs. Returns None (collect normally) for paths inside a SUT test dir or for ancestor directories that need to be traversed to reach one. When no SUT test dirs are configured, all paths are collected normally.

Return type:

bool | None

pytest_runtest_protocol(item, nextitem)

Repeat each test item when stability mode is active.

When --iterations or --duration (or both) are specified, each collected test is executed multiple times within a single pytest session. Class-scoped fixtures (setup_class / teardown_class) remain cached by pytest for the lifetime of the class and fire only once. Method-scoped fixtures fire on every iteration.

Unlike calling runtestprotocol in a loop (which tears down all fixtures including class-scoped ones after each call), this hook runs setup once, repeats the call phase N times, then runs teardown once. This keeps class-scoped resources (SSH connections, deployed artifacts, etc.) alive across iterations.

Returns True to signal that this hook handled the item, or None to fall through to default behaviour.

Return type:

bool | None

pytest_runtest_call(item)

Implement @pytest.mark.retry(n) — retry the test body up to n times.

Stops on the first success. Re-raises the last exception if all attempts fail. Each failed attempt is logged at WARNING level.

Return type:

None

pytest_report_teststatus(report, config)

Suppress pytest’s per-test progress characters.

otto’s RichHandler streams log output to the console in real time, so pytest’s dot/F/E column adds no information and races with log records when capture is disabled. Returning an empty short-letter keeps the category and verbose word intact (so failure summaries and the final pass/fail counts still render) while stopping the terminal reporter from writing anything per test.

Return type:

tuple[str, str, str] | None

pytest_runtest_logreport(report)

In stability mode, accumulate per-test pass/fail counts.

Return type:

None

pytest_runtest_makereport(item, call)
Return type:

Generator[None, None, None]