v0.5.0 #28
IKrysanov
announced in
Announcements
v0.5.0
#28
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
0.5.0 - 2026-06-16
Headline: three complementary ways to re-run only the failed tests instead
of the whole suite, plus multiple test targets and a parser-owned report
location.
Added
PytestOperator(rerun_failed=N)-- in-process re-run of only the failedtests. Runs the full suite once, then re-runs the still-failing tests (via
node_id_to_pytest_args) up toNmore times within the sameexecute(), stopping early once none fail. Needs no.pytest_cache, noXCom and no
try_number, so it is robust on any executor anddeterministic. The XCom summary keeps the first run's counts and adds
rerun_rounds,recovered_node_idsandstill_failing_node_ids.Ignored in
dry_run. Default0(unchanged behaviour).PytestOperator(test_retry_strategy="failed_only")-- Airflow-retrydriven re-run of only the previous attempt's failures, in a single task. The
failed node-ids are carried between attempts in an Airflow Variable keyed
by
(dag_id, task_id, run_id, map_index)(not the task's own XCom, whichAirflow clears on retry; a Variable survives and works on Airflow 2.x/3.x).
map_indexis part of the key so the dynamically-mapped instances of onetask (
.expand(...)) never clobber each other's failed set. The Variablelifecycle is crash-safe: consumed on read (deleted before any test runs)
and (re)written only when a further retry will read it -- never on the
success/final attempt, and never when
fail_on_test_failure=False(the taskthen succeeds, so no retry will ever read it) -- so a killed worker cannot
orphan it. Narrowing is driven by the stored set, not
try_number(a reusedrun_idmay narrow earlier). Best-effort: falls back to the full suite ifthe backend or the context ids are unavailable;
pytest_argsare nevermutated; ignored in
dry_run. Default"all"(unchanged behaviour). Ifthe final-attempt status can't be determined from the task context, the
operator logs a warning to the task log (it then writes the set forward,
which could otherwise leave a Variable behind on what was really the last
attempt).
LastFailedStore(Protocol),VariableLastFailedStoreandlast_failed_var_keyback thefailed_onlystore. Inject a custom backendwith
PytestOperator(..., store=...)-- any object withread(key)/write(key, ids)/delete(key)satisfies the structuralprotocol (validated at init). The Variable class is resolved through the
compat shim, so importing the package stays Airflow-free.
node_id_to_pytest_args(node_ids, *, class_prefix="Test")-- converts thedotted
failed_node_ids(from XCom) back into pytest CLI selectors, for thetwo-task
run_all -> run_failedDAG pattern (documented in the README).Idempotent; leaves malformed/slash-form input untouched.
PytestOperator(test_path=...)andPytestRunner.runnow accept a single string or a sequence of strings,all passed to pytest as positional selectors. With no explicit
cwd, theworking directory is derived as the closest shared parent of the targets.
report_dir, e.g.JUnitResultParser(report_dir="/opt/airflow/artifacts")(alsoJSONResultParser). The location travels with the parser, so it applies toany runner. When unset, the runner writes to a temp dir it cleans up.
TestExecutionErrornow carries thecaptured
stdout/stderras attributes (not just in the worker log).cancellation decisions, so the report location and lifecycle are visible.
Changed
SubprocessPytestRunnerno longer takes areport_dirargument -- set the location on the parser instead(
JUnitResultParser(report_dir=X)). The runner owns only the temp-dirfallback and its
cleanuppolicy.expression that rendered to
"") are dropped with a warning. A run with nousable target fails fast; an empty arg list is fine.
TypeError(rerun_failednot an int,storenot aLastFailedStore),a valid type with a wrong value raises
ValueError(unknowntest_retry_strategy, negativererun_failed).Fixed
report_dirnow resolve correctlyunder the runner's derived cwd (previously a relative target could
double-join,
"tests"->"tests/tests", and a relative report path wentmissing). Targets and report paths are now absolutised.
anchoring on their path portion (
tests/test_x.py::test_a->tests/).Previously any
::target fell back to the worker's inherited cwd, so afailed_onlyretry -- whose targets are all node-ids -- ran from the wrongdirectory and relative
addopts(e.g. Allure's--alluredir) broke onevery retry. The path portion is absolutised in lock-step, so pytest never
double-joins.
cleanup()is idempotent: the operator cleans up twice on a kill (fromexecute()andon_kill); it no longer logs the decision twice._resolve_cwdfalls back gracefully (with a warning) when targets share nocommon anchor (e.g. different Windows drives) instead of raising.
(
pytest.skip()in teardown) keeps its reason; the pluralerrorssummarykey is counted (not only the singular
error); and parametrized node idswhose value contains
::(e.g.test_param[a::b]) are split correctly.OSError/PermissionError(child changed gid, or a cancel/timeout race) — it falls back to killing the
direct child. The auto-created temp report dir is also removed if the parser's
report_requestcallback raises, andreport_dirownership now resolvessymlinks so a symlinked path isn't mistaken for outside the runner's temp dir.
SubprocessPytestRunnervalidates itstimeout(must be positive) andgrace_period(must be non-negative) at construction instead of failingobscurely later.
This discussion was created from the release v0.5.0.
Beta Was this translation helpful? Give feedback.
All reactions