From 9ad00714ba2520b694cb826a2757ad860e29d383 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 09:55:56 +0200 Subject: [PATCH 1/5] pytester: allow passing in stdin to run/popen --- changelog/5059.feature.rst | 1 + src/_pytest/pytester.py | 38 +++++++++++++++++++++---- testing/test_pytester.py | 58 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 changelog/5059.feature.rst diff --git a/changelog/5059.feature.rst b/changelog/5059.feature.rst new file mode 100644 index 000000000..4d5d14061 --- /dev/null +++ b/changelog/5059.feature.rst @@ -0,0 +1 @@ +Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d474df4b9..45c88bb3f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -36,6 +36,8 @@ IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] +CLOSE_STDIN = object + def pytest_addoption(parser): parser.addoption( @@ -1032,7 +1034,7 @@ class Testdir(object): if colitem.name == name: return colitem - def popen(self, cmdargs, stdout, stderr, **kw): + def popen(self, cmdargs, stdout, stderr, stdin=CLOSE_STDIN, **kw): """Invoke subprocess.Popen. This calls subprocess.Popen making sure the current working directory @@ -1050,10 +1052,18 @@ class Testdir(object): env["USERPROFILE"] = env["HOME"] kw["env"] = env - popen = subprocess.Popen( - cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, **kw - ) - popen.stdin.close() + if stdin is CLOSE_STDIN: + kw["stdin"] = subprocess.PIPE + elif isinstance(stdin, bytes): + kw["stdin"] = subprocess.PIPE + else: + kw["stdin"] = stdin + + popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + if stdin is CLOSE_STDIN: + popen.stdin.close() + elif isinstance(stdin, bytes): + popen.stdin.write(stdin) return popen @@ -1065,6 +1075,10 @@ class Testdir(object): :param args: the sequence of arguments to pass to `subprocess.Popen()` :param timeout: the period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired` + :param stdin: optional standard input. Bytes are being send, closing + the pipe, otherwise it is passed through to ``popen``. + Defaults to ``CLOSE_STDIN``, which translates to using a pipe + (``subprocess.PIPE``) that gets closed. Returns a :py:class:`RunResult`. @@ -1072,8 +1086,13 @@ class Testdir(object): __tracebackhide__ = True timeout = kwargs.pop("timeout", None) + stdin = kwargs.pop("stdin", CLOSE_STDIN) raise_on_kwargs(kwargs) + popen_kwargs = {"stdin": stdin} + if isinstance(stdin, bytes): + popen_kwargs["stdin"] = subprocess.PIPE + cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1086,8 +1105,15 @@ class Testdir(object): try: now = time.time() popen = self.popen( - cmdargs, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32") + cmdargs, + stdout=f1, + stderr=f2, + close_fds=(sys.platform != "win32"), + **popen_kwargs ) + if isinstance(stdin, bytes): + popen.stdin.write(stdin) + popen.stdin.close() def handle_timeout(): __tracebackhide__ = True diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 2e4877463..86e160ecb 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -4,6 +4,7 @@ from __future__ import division from __future__ import print_function import os +import subprocess import sys import time @@ -482,3 +483,60 @@ def test_pytester_addopts(request, monkeypatch): testdir.finalize() assert os.environ["PYTEST_ADDOPTS"] == "--orig-unused" + + +def test_run_stdin(testdir): + with pytest.raises(testdir.TimeoutExpired): + testdir.run( + sys.executable, + "-c", + "import sys; print(sys.stdin.read())", + stdin=subprocess.PIPE, + timeout=0.1, + ) + + with pytest.raises(testdir.TimeoutExpired): + result = testdir.run( + sys.executable, + "-c", + "import sys, time; time.sleep(1); print(sys.stdin.read())", + stdin=b"input\n2ndline", + timeout=0.1, + ) + + result = testdir.run( + sys.executable, + "-c", + "import sys; print(sys.stdin.read())", + stdin=b"input\n2ndline", + ) + assert result.stdout.lines == ["input", "2ndline"] + assert result.stderr.str() == "" + assert result.ret == 0 + + +def test_popen_stdin_pipe(testdir): + proc = testdir.popen( + [sys.executable, "-c", "import sys; print(sys.stdin.read())"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + ) + stdin = b"input\n2ndline" + stdout, stderr = proc.communicate(input=stdin) + assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] + assert stderr == b"" + assert proc.returncode == 0 + + +def test_popen_stdin_bytes(testdir): + proc = testdir.popen( + [sys.executable, "-c", "import sys; print(sys.stdin.read())"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=b"input\n2ndline", + ) + stdout, stderr = proc.communicate() + assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] + assert stderr == b"" + assert proc.returncode == 0 From 4fca86e2afe57ee2ad3dae9ffe4c4debdb565a59 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 12:09:31 +0200 Subject: [PATCH 2/5] testdir.popen: use kwargs with defaults for stdout/stderr --- changelog/5059.trivial.rst | 1 + src/_pytest/pytester.py | 9 ++++++++- testing/test_pytester.py | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelog/5059.trivial.rst diff --git a/changelog/5059.trivial.rst b/changelog/5059.trivial.rst new file mode 100644 index 000000000..bd8035669 --- /dev/null +++ b/changelog/5059.trivial.rst @@ -0,0 +1 @@ +pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``). diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 45c88bb3f..4a89f5515 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1034,7 +1034,14 @@ class Testdir(object): if colitem.name == name: return colitem - def popen(self, cmdargs, stdout, stderr, stdin=CLOSE_STDIN, **kw): + def popen( + self, + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw + ): """Invoke subprocess.Popen. This calls subprocess.Popen making sure the current working directory diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 86e160ecb..f3b5f70e2 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -540,3 +540,22 @@ def test_popen_stdin_bytes(testdir): assert stdout.decode("utf8").splitlines() == ["input", "2ndline"] assert stderr == b"" assert proc.returncode == 0 + + +def test_popen_default_stdin_stderr_and_stdin_None(testdir): + # stdout, stderr default to pipes, + # stdin can be None to not close the pipe, avoiding + # "ValueError: flush of closed file" with `communicate()`. + p1 = testdir.makepyfile( + """ + import sys + print(sys.stdin.read()) # empty + print('stdout') + sys.stderr.write('stderr') + """ + ) + proc = testdir.popen([sys.executable, str(p1)], stdin=None) + stdout, stderr = proc.communicate(b"ignored") + assert stdout.splitlines() == [b"", b"stdout"] + assert stderr.splitlines() == [b"stderr"] + assert proc.returncode == 0 From c36a90531a413f9415e950c17fe5d36a0ca052ed Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:01:15 +0200 Subject: [PATCH 3/5] Move CLOSE_STDIN to class --- src/_pytest/pytester.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 4a89f5515..21f8ed26b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -36,8 +36,6 @@ IGNORE_PAM = [ # filenames added when obtaining details about the current user u"/var/lib/sss/mc/passwd" ] -CLOSE_STDIN = object - def pytest_addoption(parser): parser.addoption( @@ -475,6 +473,8 @@ class Testdir(object): """ + CLOSE_STDIN = object + class TimeoutExpired(Exception): pass @@ -1059,7 +1059,7 @@ class Testdir(object): env["USERPROFILE"] = env["HOME"] kw["env"] = env - if stdin is CLOSE_STDIN: + if stdin is Testdir.CLOSE_STDIN: kw["stdin"] = subprocess.PIPE elif isinstance(stdin, bytes): kw["stdin"] = subprocess.PIPE @@ -1067,7 +1067,7 @@ class Testdir(object): kw["stdin"] = stdin popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - if stdin is CLOSE_STDIN: + if stdin is Testdir.CLOSE_STDIN: popen.stdin.close() elif isinstance(stdin, bytes): popen.stdin.write(stdin) @@ -1093,7 +1093,7 @@ class Testdir(object): __tracebackhide__ = True timeout = kwargs.pop("timeout", None) - stdin = kwargs.pop("stdin", CLOSE_STDIN) + stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) raise_on_kwargs(kwargs) popen_kwargs = {"stdin": stdin} From ec46864922d2aab4fb266ae8317d553ec9ef3d9b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:02:38 +0200 Subject: [PATCH 4/5] run: pass through stdin, just close then --- src/_pytest/pytester.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 21f8ed26b..6dc9031df 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1096,10 +1096,6 @@ class Testdir(object): stdin = kwargs.pop("stdin", Testdir.CLOSE_STDIN) raise_on_kwargs(kwargs) - popen_kwargs = {"stdin": stdin} - if isinstance(stdin, bytes): - popen_kwargs["stdin"] = subprocess.PIPE - cmdargs = [ str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs ] @@ -1113,13 +1109,12 @@ class Testdir(object): now = time.time() popen = self.popen( cmdargs, + stdin=stdin, stdout=f1, stderr=f2, close_fds=(sys.platform != "win32"), - **popen_kwargs ) if isinstance(stdin, bytes): - popen.stdin.write(stdin) popen.stdin.close() def handle_timeout(): From b84f826fc8a4f305309d699b3bc2f1dbd3d9672d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 10 Apr 2019 00:03:49 +0200 Subject: [PATCH 5/5] test_run_stdin: add sleep --- testing/test_pytester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index f3b5f70e2..b76d413b7 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -490,7 +490,7 @@ def test_run_stdin(testdir): testdir.run( sys.executable, "-c", - "import sys; print(sys.stdin.read())", + "import sys, time; time.sleep(1); print(sys.stdin.read())", stdin=subprocess.PIPE, timeout=0.1, )