diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2c6ff20208e..f936074d61e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -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. @@ -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 @@ -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 @@ -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): diff --git a/testing/test_pytester.py b/testing/test_pytester.py index b76d413b7b1..02d52479267 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -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") @@ -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={})