Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/767.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Command-line options such as ``--tb=short`` are no longer dropped when
``--looponfail`` is combined with distributed runs (``-n``). Previously the
worker session was rebuilt only from the parsed options and positional
arguments, so the original invocation flags were lost and the nested workers
fell back to their defaults (for example, the long traceback style).
18 changes: 18 additions & 0 deletions src/xdist/looponfail.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def setup(self) -> None:
init_worker_session,
args=self.config.args,
option_dict=vars(self.config.option),
invocation_args=list(self.config.invocation_params.args),
)
remote_outchannel: execnet.Channel = channel.receive()

Expand Down Expand Up @@ -161,6 +162,7 @@ def init_worker_session(
channel: "execnet.Channel", # noqa: UP037
args: list[str],
option_dict: dict[str, "Any"], # noqa: UP037
invocation_args: list[str],
) -> None:
import os
import sys
Expand All @@ -183,6 +185,22 @@ def init_worker_session(

config = Config.fromdictargs(option_dict, list(args))
config.args = args
# fromdictargs() rebuilds the config from the parsed options plus the
# positional arguments only, so config.invocation_params.args loses the
# original command-line flags (e.g. --tb=short). Restore the full
# invocation arguments so that any nested distributed (-n) run propagates
# those options to its workers. See #767.
#
# InvocationParams is constructed directly (rather than via
# dataclasses.replace) because it is a frozen attrs class on pytest 7.x
# and a dataclass only on newer pytest; calling its type with the same
# fields works across both.
invocation_params = config.invocation_params
config.invocation_params = type(invocation_params)(
args=tuple(invocation_args),
plugins=invocation_params.plugins,
dir=invocation_params.dir,
)
from xdist.looponfail import WorkerFailSession

WorkerFailSession(config, channel).main()
Expand Down
44 changes: 44 additions & 0 deletions testing/test_looponfail.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,50 @@ def runsession_dups() -> tuple[list[str], list[str], bool]:
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1

def test_looponfail_passes_command_line_options_to_workers(
self,
pytester: pytest.Pytester,
capsys: pytest.CaptureFixture[str],
) -> None:
# Regression test for #767: command-line options such as --tb=short
# were dropped when looponfail ran the failing tests through
# distributed (-n) workers. The reconstructed worker config lost the
# original invocation arguments, so the nested workers fell back to
# the default (long) traceback style.
pytester.makepyfile(
foo=textwrap.dedent(
"""
def a():
b()

def b():
c()

def c():
assert False
"""
)
)
modcol = pytester.getmodulecol(
textwrap.dedent(
"""
from foo import a

def test_foo():
a()
"""
),
configargs=["-n2", "--tb=short"],
)
control = RemoteControl(modcol.config)
control.loop_once()
out, _err = capsys.readouterr()
# --tb=short renders frames as compact "path:lineno: in func" lines.
# The default (long) style instead expands the source ("def test_foo():"
# with a ">" marker), so its absence confirms the option propagated.
assert ": in test_foo" in out
assert "def test_foo():" not in out


class TestFunctional:
def test_fail_to_ok(self, pytester: pytest.Pytester) -> None:
Expand Down
Loading