Repository: pytest-dev/pytest-xdist

Issue:
xdist adds current directory to sys.path
With 1.26.1.
```python
import sys

def test_path():
    assert "" not in sys.path
```
```
$ pytest test_path.py
<OK>
$ pytest test_path.py -n 1
E     AssertionError....
```
Obviously this can cause problems/change behaviour depending on where you are running the tests from.

Possibly related to #376?

Comments:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 21418d0..19f2f10 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 -   repo: https://github.com/psf/black
-    rev: 21.5b2
+    rev: 21.6b0
     hooks:
     -   id: black
         args: [--safe, --quiet, --target-version, py35]
@@ -17,7 +17,7 @@ repos:
     hooks:
     -   id: flake8
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v2.19.0
+    rev: v2.19.4
     hooks:
     -   id: pyupgrade
         args: [--py3-plus]
diff --git a/changelog/421.bugfix.rst b/changelog/421.bugfix.rst
new file mode 100644
index 0000000..121c69c
--- /dev/null
+++ b/changelog/421.bugfix.rst
@@ -0,0 +1,1 @@
+Copy the parent process sys.path into local workers, to work around execnet's python -c adding the current directory to sys.path.
diff --git a/changelog/646.feature.rst b/changelog/646.feature.rst
new file mode 100644
index 0000000..210c73e
--- /dev/null
+++ b/changelog/646.feature.rst
@@ -0,0 +1,3 @@
+Add ``--numprocesses=logical`` flag, which automatically uses the number of logical CPUs available, instead of physical CPUs with ``auto``.
+
+This is very useful for test suites which are not CPU-bound.
diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py
index 1eba32b..12b3a0e 100644
--- a/src/xdist/plugin.py
+++ b/src/xdist/plugin.py
@@ -1,17 +1,21 @@
 import os
 import uuid
+import sys
 
 import py
 import pytest
 
+_sys_path = list(sys.path)  # freeze a copy of sys.path at interpreter startup
 
-def pytest_xdist_auto_num_workers():
+
+def pytest_xdist_auto_num_workers(config):
     try:
         import psutil
     except ImportError:
         pass
     else:
-        count = psutil.cpu_count(logical=False) or psutil.cpu_count()
+        use_logical = config.option.numprocesses == "logical"
+        count = psutil.cpu_count(logical=use_logical) or psutil.cpu_count()
         if count:
             return count
     try:
@@ -36,8 +40,8 @@ def pytest_xdist_auto_num_workers():
 
 
 def parse_numprocesses(s):
-    if s == "auto":
-        return "auto"
+    if s in ("auto", "logical"):
+        return s
     elif s is not None:
         return int(s)
 
@@ -51,9 +55,10 @@ def pytest_addoption(parser):
         metavar="numprocesses",
         action="store",
         type=parse_numprocesses,
-        help="shortcut for '--dist=load --tx=NUM*popen', "
-        "you can use 'auto' here for auto detection CPUs number on "
-        "host system and it will be 0 when used with --pdb",
+        help="Shortcut for '--dist=load --tx=NUM*popen'. With 'auto', attempt "
+        "to detect physical CPU count. With 'logical', detect logical CPU "
+        "count. If physical CPU count cannot be found, falls back to logical "
+        "count. This will be 0 when used with --pdb.",
     )
     group.addoption(
         "--maxprocesses",
@@ -190,7 +195,7 @@ def pytest_configure(config):
 @pytest.mark.tryfirst
 def pytest_cmdline_main(config):
     usepdb = config.getoption("usepdb", False)  # a core option
-    if config.option.numprocesses == "auto":
+    if config.option.numprocesses in ("auto", "logical"):
         if usepdb:
             config.option.numprocesses = 0
             config.option.dist = "no"
diff --git a/src/xdist/remote.py b/src/xdist/remote.py
index 7f95b5c..d79f0b3 100644
--- a/src/xdist/remote.py
+++ b/src/xdist/remote.py
@@ -219,12 +219,14 @@ if __name__ == "__channelexec__":
     channel = channel  # noqa
     workerinput, args, option_dict, change_sys_path = channel.receive()
 
-    if change_sys_path:
+    if change_sys_path is None:
         importpath = os.getcwd()
         sys.path.insert(0, importpath)
         os.environ["PYTHONPATH"] = (
             importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
         )
+    else:
+        sys.path = change_sys_path
 
     os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"]
     os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"]
diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py
index 8fed077..2c4f1a6 100644
--- a/src/xdist/workermanage.py
+++ b/src/xdist/workermanage.py
@@ -9,6 +9,7 @@ import pytest
 import execnet
 
 import xdist.remote
+from xdist.plugin import _sys_path
 
 
 def parse_spec_config(config):
@@ -261,7 +262,8 @@ class WorkerController:
         remote_module = self.config.hook.pytest_xdist_getremotemodule()
         self.channel = self.gateway.remote_exec(remote_module)
         # change sys.path only for remote workers
-        change_sys_path = not self.gateway.spec.popen
+        # restore sys.path from a frozen copy for local workers
+        change_sys_path = _sys_path if self.gateway.spec.popen else None
         self.channel.send((self.workerinput, args, option_dict, change_sys_path))
 
         if self.putevent:


Test patch:
diff --git a/testing/test_plugin.py b/testing/test_plugin.py
index c1aac65..5870676 100644
--- a/testing/test_plugin.py
+++ b/testing/test_plugin.py
@@ -69,6 +69,12 @@ def test_auto_detect_cpus(testdir, monkeypatch):
     assert config.getoption("numprocesses") == 0
     assert config.getoption("dist") == "no"
 
+    config = testdir.parseconfigure("-nlogical", "--pdb")
+    check_options(config)
+    assert config.getoption("usepdb")
+    assert config.getoption("numprocesses") == 0
+    assert config.getoption("dist") == "no"
+
     monkeypatch.delattr(os, "sched_getaffinity", raising=False)
     monkeypatch.setenv("TRAVIS", "true")
     config = testdir.parseconfigure("-nauto")
@@ -81,12 +87,16 @@ def test_auto_detect_cpus_psutil(testdir, monkeypatch):
 
     psutil = pytest.importorskip("psutil")
 
-    monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 42)
+    monkeypatch.setattr(psutil, "cpu_count", lambda logical=True: 84 if logical else 42)
 
     config = testdir.parseconfigure("-nauto")
     check_options(config)
     assert config.getoption("numprocesses") == 42
 
+    config = testdir.parseconfigure("-nlogical")
+    check_options(config)
+    assert config.getoption("numprocesses") == 84
+
 
 def test_hook_auto_num_workers(testdir, monkeypatch):
     from xdist.plugin import pytest_cmdline_main as check_options
@@ -101,6 +111,10 @@ def test_hook_auto_num_workers(testdir, monkeypatch):
     check_options(config)
     assert config.getoption("numprocesses") == 42
 
+    config = testdir.parseconfigure("-nlogical")
+    check_options(config)
+    assert config.getoption("numprocesses") == 42
+
 
 def test_boxed_with_collect_only(testdir):
     from xdist.plugin import pytest_cmdline_main as check_options
diff --git a/testing/test_remote.py b/testing/test_remote.py
index 2f6e222..31a6a2a 100644
--- a/testing/test_remote.py
+++ b/testing/test_remote.py
@@ -292,3 +292,17 @@ def test_remote_usage_prog(testdir, request):
     result = testdir.runpytest_subprocess("-n1")
     assert result.ret == 1
     result.stdout.fnmatch_lines(["*usage: *", "*error: my_usage_error"])
+
+
+def test_remote_sys_path(testdir):
+    """Work around sys.path differences due to execnet using `python -c`."""
+    testdir.makepyfile(
+        """
+        import sys
+
+        def test_sys_path():
+            assert "" not in sys.path
+        """
+    )
+    result = testdir.runpytest("-n1")
+    assert result.ret == 0