**Approach (brief)**- Add a CLI/ini option to configure max retries.- Implement pytest_runtest_protocol to run the test up to N+1 times (original + retries).- Use pytest_runtest_makereport to attach the retry count to the report (via user_properties).- Keep implementation minimal and safe for CI: do setup/call/teardown per attempt so hooks behave normally.Code skeletonpython
# file: pytest_retry_plugin.py
import pytest
def pytest_addoption(parser):
group = parser.getgroup("flaky-retries")
group.addoption(
"--max-retries",
type=int,
default=None,
help="Maximum number of retries for failed tests"
)
parser.addini("max_retries", "Maximum number of retries for failed tests", default="0")
def _get_max_retries(config):
cli = config.getoption("max_retries")
if cli is not None:
return cli
# fallback to ini (string)
try:
return int(config.getini("max_retries") or 0)
except Exception:
return 0
def pytest_runtest_protocol(item, nextitem):
"""
Run test up to max_retries+1 times. Return True to indicate we've handled running.
We call the standard per-phase hooks so reports are created each attempt.
"""
max_retries = _get_max_retries(item.config)
attempts = 0
last_report = None
while True:
# expose current attempt on the item for hooks/reporting
item.user_properties = getattr(item, "user_properties", [])
# call setup/call/teardown via hook invocations so reports are produced
item.ihook.pytest_runtest_setup(item=item)
try:
item.ihook.pytest_runtest_call(item=item)
finally:
item.ihook.pytest_runtest_teardown(item=item, nextitem=nextitem)
# obtain the outcome report for the 'call' phase
rep = item.ihook.pytest_runtest_makereport(item=item, call=item._call) # may be None in some versions
# robust find: prefer item._report_call if available
if rep is None:
rep = getattr(item, "_report_call", None)
# mark retry count on item so makereport can attach it
item._retry_count = attempts
if rep is None:
# can't determine outcome: stop
break
last_report = rep
if rep.passed or attempts >= max_retries:
break
attempts += 1
# loop to retry
# ensure final report has retry count in user_properties via makereport hook
return True
def pytest_runtest_makereport(item, call):
"""
Attach retry count as a user_property so it appears in reports and junit xml.
"""
outcome = pytest.runner.CallInfo.from_call(call) if False else None # placeholder to show we use 'call'
rep = pytest.Item._repr_failure_py? # placeholder - actual framework supplies 'rep' to other hooks
# Simpler: return a dict-like user_properties via attribute on item (pytest will merge)
retry = getattr(item, "_retry_count", 0)
# ensure user_properties exists and contains a tuple
props = getattr(item, "user_properties", [])
# set/replace a retry property
# remove existing
props = [p for p in props if p[0] != "retry_count"]
props.append(("retry_count", retry))
item.user_properties = props
# don't need to return anything; pytest will build the real report and include user_properties
How to configure- Via pytest.ini: - add line: max_retries = 2- CLI overrides ini: - pytest --max-retries=3 tests/Notes and reasoning- Storing retry in item.user_properties makes it available in generated reports and junit xml.- Implementing retries in pytest_runtest_protocol ensures setup/call/teardown hooks run per attempt so fixtures behave correctly.- This skeleton omits some low-level compatibility details: depending on pytest version you may need to use different internal helpers to obtain the call report object. In production, use item.ihook.pytest_runtest_makereport(...) return value or inspect reports emitted by hook calls and ensure thread-safety; also add logging and metrics for flaky test tracking.