Skip to content
Closed
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
49 changes: 35 additions & 14 deletions src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,14 @@ def inline_genitems(self, *args):
items = [x.item for x in rec.getcalls("pytest_itemcollected")]
return items, rec

def _get_isolated_env(self):
tmpdir = str(self.tmpdir)
return (
# Do not load user config.
("HOME", tmpdir),
("USERPROFILE", tmpdir),
)

def inline_run(self, *args, **kwargs):
"""Run ``pytest.main()`` in-process, returning a HookRecorder.

Expand All @@ -819,8 +827,8 @@ def inline_run(self, *args, **kwargs):
try:
# Do not load user config (during runs only).
mp_run = MonkeyPatch()
mp_run.setenv("HOME", str(self.tmpdir))
mp_run.setenv("USERPROFILE", str(self.tmpdir))
for k, v in self._get_isolated_env():
mp_run.setenv(k, v)
finalizers.append(mp_run.undo)

# When running pytest inline any plugins active in the main test
Expand Down Expand Up @@ -1050,20 +1058,33 @@ def popen(
):
"""Invoke subprocess.Popen.

This calls subprocess.Popen making sure the current working directory
is in the PYTHONPATH.
This calls subprocess.Popen, making sure the current working directory
is in the PYTHONPATH by default.

You probably want to use :py:meth:`run` instead.
Optional keyword arguments:

:param env: OS environment to be used as is (no PYTHONPATH adjustment,
nor isolation for HOME etc).
:param env_update: OS environment values to update the current
environment with.
PYTHONPATH gets adjusted if not passed in explicitly.

You probably want to use :py:meth:`run` instead.
"""
env = os.environ.copy()
env["PYTHONPATH"] = os.pathsep.join(
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
)
# Do not load user config.
env["HOME"] = str(self.tmpdir)
env["USERPROFILE"] = env["HOME"]
kw["env"] = env
if "env" in kw:
env = kw.pop("env")
if "env_update" in kw:
raise ValueError("env and env_update are mutually exclusive")
else:
env = os.environ.copy()
env.update(self._get_isolated_env())

env_update = kw.pop("env_update", {})
if "PYTHONPATH" not in env_update:
env["PYTHONPATH"] = os.pathsep.join(
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
)
env.update(env_update)

if stdin is Testdir.CLOSE_STDIN:
kw["stdin"] = subprocess.PIPE
Expand All @@ -1072,7 +1093,7 @@ def popen(
else:
kw["stdin"] = stdin

popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, env=env, **kw)
if stdin is Testdir.CLOSE_STDIN:
popen.stdin.close()
elif isinstance(stdin, bytes):
Expand Down
38 changes: 38 additions & 0 deletions testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
from _pytest.pytester import SysModulesSnapshot
from _pytest.pytester import SysPathsSnapshot

try:
import mock
except ImportError:
import unittest.mock as mock


def test_make_hook_recorder(testdir):
item = testdir.getitem("def test_func(): pass")
Expand Down Expand Up @@ -559,3 +564,36 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir):
assert stdout.splitlines() == [b"", b"stdout"]
assert stderr.splitlines() == [b"stderr"]
assert proc.returncode == 0


def test_popen_env(testdir, monkeypatch):
monkeypatch.delenv("PYTHONPATH", raising=False)
popen_args = (["cmd"], None, None)

with mock.patch("subprocess.Popen") as m:
testdir.popen(*popen_args)
env = m.call_args[1]["env"]
assert set(env.keys()) == set(
list(os.environ.keys()) + ["PYTHONPATH", "USERPROFILE", "HOME"]
)
assert env["PYTHONPATH"] == os.getcwd()

# Updates PYTHONPATH by default.
monkeypatch.setenv("PYTHONPATH", "custom")
testdir.popen(*popen_args)
env = m.call_args[1]["env"]
assert env["PYTHONPATH"] == os.pathsep.join((os.getcwd(), "custom"))

# Uses explicit PYTHONPATH via env_update.
testdir.popen(*popen_args, env_update={"PYTHONPATH": "mypp", "CUSTOM_ENV": "1"})
env = m.call_args[1]["env"]
assert env["PYTHONPATH"] == "mypp"
assert env["CUSTOM_ENV"] == "1"

# Uses explicit env only.
testdir.popen(*popen_args, env={"CUSTOM_ENV": "1"})
env = m.call_args[1]["env"]
assert env == {"CUSTOM_ENV": "1"}

with pytest.raises(ValueError, match="env and env_update are mutually exclusive"):
testdir.popen(*popen_args, env={}, env_update={})