diff --git a/changelog/767.bugfix.rst b/changelog/767.bugfix.rst new file mode 100644 index 00000000..ab50fa98 --- /dev/null +++ b/changelog/767.bugfix.rst @@ -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). diff --git a/src/xdist/looponfail.py b/src/xdist/looponfail.py index 7fcebe51..0f9c5b2f 100644 --- a/src/xdist/looponfail.py +++ b/src/xdist/looponfail.py @@ -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() @@ -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 @@ -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() diff --git a/testing/test_looponfail.py b/testing/test_looponfail.py index 844736f6..3242d3e6 100644 --- a/testing/test_looponfail.py +++ b/testing/test_looponfail.py @@ -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: