Skip to content
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

pytester: allow passing in stdin to run/popen #5059

Merged
merged 5 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions changelog/5059.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Standard input (stdin) can be given to pytester's ``Testdir.run()`` and ``Testdir.popen()``.
1 change: 1 addition & 0 deletions changelog/5059.trivial.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytester's ``Testdir.popen()`` uses ``stdout`` and ``stderr`` via keyword arguments with defaults now (``subprocess.PIPE``).
45 changes: 39 additions & 6 deletions src/_pytest/pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
u"/var/lib/sss/mc/passwd"
]

CLOSE_STDIN = object
blueyed marked this conversation as resolved.
Show resolved Hide resolved


def pytest_addoption(parser):
parser.addoption(
Expand Down Expand Up @@ -1032,7 +1034,14 @@ def collect_by_name(self, modcol, name):
if colitem.name == name:
return colitem

def popen(self, cmdargs, stdout, stderr, **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
Expand All @@ -1050,10 +1059,18 @@ def popen(self, cmdargs, stdout, stderr, **kw):
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

Expand All @@ -1065,15 +1082,24 @@ def run(self, *cmdargs, **kwargs):
: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`.

"""
__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):
blueyed marked this conversation as resolved.
Show resolved Hide resolved
popen_kwargs["stdin"] = subprocess.PIPE

cmdargs = [
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
]
Expand All @@ -1086,8 +1112,15 @@ def run(self, *cmdargs, **kwargs):
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()
blueyed marked this conversation as resolved.
Show resolved Hide resolved

def handle_timeout():
__tracebackhide__ = True
Expand Down
77 changes: 77 additions & 0 deletions testing/test_pytester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import print_function

import os
import subprocess
import sys
import time

Expand Down Expand Up @@ -482,3 +483,79 @@ 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,
blueyed marked this conversation as resolved.
Show resolved Hide resolved
)

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


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