Skip to content

Use Config.invocation_params for consistent worker initialization #448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 11, 2019
Merged
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
9 changes: 9 additions & 0 deletions changelog/448.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Initialization between workers and master nodes is now more consistent, which fixes a number of
long-standing issues related to startup with the ``-c`` option.

Issues:

* `#6 <https://github.com/pytest-dev/pytest-xdist/issues/6>`__: Poor interaction between ``-n#`` and ``-c X.cfg``
* `#445 <https://github.com/pytest-dev/pytest-xdist/issues/445>`__: pytest-xdist is not reporting the same nodeid as pytest does

This however only works with **pytest 5.1 or later**, as it required changes in pytest itself.
20 changes: 14 additions & 6 deletions src/xdist/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import pytest
from execnet.gateway_base import dumps, DumpError

from _pytest.config import _prepareconfig, Config


class WorkerInteractor(object):
def __init__(self, config, channel):
Expand Down Expand Up @@ -211,18 +213,18 @@ def getinfodict():


def remote_initconfig(option_dict, args):
from _pytest.config import Config

option_dict["plugins"].append("no:terminal")
config = Config.fromdictargs(option_dict, args)
return Config.fromdictargs(option_dict, args)


def setup_config(config, basetemp):
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.option.maxprocesses = None
config.args = args
return config
config.option.basetemp = basetemp


if __name__ == "__channelexec__":
Expand All @@ -239,7 +241,13 @@ def remote_initconfig(option_dict, args):
os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])

config = remote_initconfig(option_dict, args)
if hasattr(Config, "InvocationParams"):
config = _prepareconfig(args, None)
else:
config = remote_initconfig(option_dict, args)
config.args = args

setup_config(config, option_dict.get("basetemp"))
config._parser.prog = os.path.basename(workerinput["mainargv"][0])
config.workerinput = workerinput
config.workeroutput = {}
Expand Down
10 changes: 8 additions & 2 deletions src/xdist/workermanage.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ def make_reltoroot(roots, args):
for arg in args:
parts = arg.split(splitcode)
fspath = py.path.local(parts[0])
if not fspath.exists():
continue
for root in roots:
x = fspath.relto(root)
if x or fspath == root:
Expand Down Expand Up @@ -236,10 +238,14 @@ def shutting_down(self):
def setup(self):
self.log("setting up worker session")
spec = self.gateway.spec
args = self.config.args
if hasattr(self.config, "invocation_params"):
args = [str(x) for x in self.config.invocation_params.args or ()]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this give unwanted arguments to the worker?

I'm investigating why pytest-cov's test suite is broken on 0.30 and invocation_params.args has lots of stuff while config.args is just the test file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus
Can you comment, please?
(I'm currently looking into getting pytest-cov's CI to pass again so this would help there)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @ionelmc and @blueyed, I must have missed this.

Why do you say it would give unwanted arguments to the worker? Using invocation_params it will initialize exactly like the master process (well almost), which solved a problem with initialization of the config file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus I guess I'm just confused about what's wrong with the latest pytest/xdist/cov combination (it's broken in some situations). Any explanation about how the all works would help me debug it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure myself, but the latest implementation just forwards all the parameters given in the command line to the workers, making them initialize the same way as the master node.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus the way I understand this the problem is that option_dict is empty in the new "invocation_params mode", thus plugins like pytest-cov don't have the necessary options to initialize. Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thus plugins like pytest-cov don't have the necessary options to initialize.

Unless pytest-cov is trying to get the options from pytest-xdist structures instead of config.getoption, it should just work. The workers should now be initializing the same way as the master node/without xdist, so all the options should be there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus the problem is that none of the plugin options are there when stuff like --dist=load --tx=popen//chdir=1 is used. When using -n 1 everything is fine.

option_dict = {}
else:
args = self.config.args
option_dict = vars(self.config.option)
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus I think now I correctly identified the cause of the problem. In the new code the args stripping matters cause the old option_dict is no more, while previously this stripping had no effect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what "stripping" you mean? Can you please clarify?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicoddemus see #491 - make_reltoroot removes anything that ain't a valid path (like the arguments I need).

option_dict = vars(self.config.option)
if spec.popen:
name = "popen-%s" % self.gateway.id
if hasattr(self.config, "_tmpdirhandler"):
Expand Down
24 changes: 24 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,30 @@ def test_hello(myarg):
assert result.ret


def test_config_initialization(testdir, pytestconfig):
"""Ensure workers and master are initialized consistently. Integration test for #445"""
if not hasattr(pytestconfig, "invocation_params"):
pytest.skip(
"requires pytest >=5.1 (config has no attribute 'invocation_params')"
)
testdir.makepyfile(
**{
"dir_a/test_foo.py": """
def test_1(): pass
"""
}
)
testdir.makefile(
".ini",
myconfig="""
[pytest]
testpaths=dir_a
""",
)
result = testdir.runpytest("-n2", "-c", "myconfig.ini", "-v")
result.stdout.fnmatch_lines(["dir_a/test_foo.py::test_1*"])


@pytest.mark.parametrize("when", ["setup", "call", "teardown"])
def test_crashing_item(testdir, when):
"""Ensure crashing item is correctly reported during all testing stages"""
Expand Down