Skip to content

Commit 153a413

Browse files
DMedina559pre-commit-ci[bot]aklajnert
authored
fix: handle file stdout/stderr (#186)
* fix: handle file stdout/stderr * fix: handle stdin=subprocess.PIPE Allows tests to write to the standard input of a mocked process * test: fix failing test - ignore `PytestDeprecationWarning` in `pytest.ini`. - suppress `docutils` errors in `tests/test_examples.py` by setting `report_level` to 5. * Test/fix windows test (#2) * test: convert to lowercase in whoami test * test: use a more cross platform command or else you'd get FileNotFoundError errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix/mac test (#3) * temp: test on push * fix: increase timeout for mac test * fix: ignore warning in universal newlines test * fix: add another filterwarning tag * remove: temp on push test * Add changelog, minor refactor in tests. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: aklajnert <github@aklajnert.pl>
1 parent be30d9a commit 153a413

File tree

8 files changed

+112
-18
lines changed

8 files changed

+112
-18
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,6 @@ target/
7878
# pyenv
7979
.python-version
8080

81+
# Visual Studio Code
82+
.vs/
83+

README.rst

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,18 @@ processes with real ``subprocess``, or use
129129
.. code-block:: python
130130
131131
def test_real_process(fp):
132+
command = ["python", "-c", "pass"]
132133
with pytest.raises(fp.exceptions.ProcessNotRegisteredError):
133-
# this will fail, as "ls" command is not registered
134-
subprocess.call("ls")
134+
# this will fail, as the command is not registered
135+
subprocess.call(command)
135136
136-
fp.pass_command("ls")
137+
fp.pass_command(command)
137138
# now it should be fine
138-
assert subprocess.call("ls") == 0
139+
assert subprocess.call(command) == 0
139140
140141
# allow all commands to be called by real subprocess
141142
fp.allow_unregistered(True)
142-
assert subprocess.call(["ls", "-l"]) == 0
143+
assert subprocess.call(command) == 0
143144
144145
145146
Differing results
@@ -272,12 +273,12 @@ if the subprocess command will be called with a string argument.
272273
273274
def test_non_exact_matching(fp):
274275
# define a command that will take any number of arguments
275-
fp.register(["ls", fp.any()])
276-
assert subprocess.check_call("ls -lah") == 0
276+
fp.register(["python", fp.any()])
277+
assert subprocess.check_call(["python", "-c", "pass"]) == 0
277278
278279
# `fake_subprocess.any()` is OK even with no arguments
279-
fp.register(["ls", fp.any()])
280-
assert subprocess.check_call("ls") == 0
280+
fp.register(["python", fp.any()])
281+
assert subprocess.check_call(["python"]) == 0
281282
282283
# but it can force a minimum amount of arguments
283284
fp.register(["cp", fp.any(min=2)])
@@ -310,8 +311,8 @@ the same name, regardless of the location. This is accomplished with
310311
311312
def test_any_matching_program(fp):
312313
# define a command that can come from anywhere
313-
fp.register([fp.program("ls")])
314-
assert subprocess.check_call("/bin/ls") == 0
314+
fp.register([fp.program("python")])
315+
assert subprocess.check_call(sys.executable) == 0
315316
316317
317318
Check if process was called
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
message: Support file handles in stdout and stderr.
2+
pr_ids:
3+
- '186'
4+
timestamp: 1756146621
5+
type: bug

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[pytest]
22
junit_family=legacy
3+
asyncio_default_fixture_loop_scope = function
34
filterwarnings =
45
error
56
ignore::pytest.PytestUnraisableExceptionWarning
7+
ignore::pytest.PytestDeprecationWarning

pytest_subprocess/fake_popen.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class FakePopen:
3939

4040
stdout: Optional[BUFFER] = None
4141
stderr: Optional[BUFFER] = None
42+
stdin: Optional[BUFFER] = None
4243
returncode: Optional[int] = None
4344
text_mode: bool = False
4445
pid: int = 0
@@ -168,6 +169,11 @@ def configure(self, **kwargs: Optional[Dict]) -> None:
168169
"""Setup the FakePopen instance based on a real Popen arguments."""
169170
self.__kwargs = self.safe_copy(kwargs)
170171
self.__universal_newlines = kwargs.get("universal_newlines", None)
172+
173+
stdin = kwargs.get("stdin")
174+
if stdin == subprocess.PIPE:
175+
self.stdin = self._get_empty_buffer(False)
176+
171177
text = kwargs.get("text", None)
172178
encoding = kwargs.get("encoding", None)
173179
errors = kwargs.get("errors", None)
@@ -288,7 +294,7 @@ def _write_to_buffer(self, data: OPTIONAL_TEXT_OR_ITERABLE, buffer: IO) -> None:
288294
)
289295
if isinstance(data, (list, tuple)):
290296
buffer.writelines([data_type(line + "\n") for line in data])
291-
else:
297+
elif data is not None:
292298
buffer.write(data_type(data))
293299

294300
def _convert(self, input: Union[str, bytes]) -> Union[str, bytes]:

tests/example_script.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
print("Stderr line 1", file=sys.stderr)
1010

1111
if "wait" in sys.argv:
12-
time.sleep(0.5)
12+
time.sleep(1)
1313

1414
if "non-zero" in sys.argv:
1515
sys.exit(1)

tests/test_examples.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ def get_code_blocks(file_path):
1515
with file_path.open() as file_handle:
1616
content = file_handle.read()
1717

18-
code_blocks = publish_doctree(content).findall(condition=is_code_block)
18+
code_blocks = publish_doctree(
19+
content, settings_overrides={"report_level": 5}
20+
).findall(condition=is_code_block)
1921
return [block.astext() for block in code_blocks]
2022

2123

tests/test_subprocess.py

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import time
99
from pathlib import Path
10+
from tempfile import NamedTemporaryFile
1011

1112
import pytest
1213

@@ -347,6 +348,7 @@ def test_run(fp, fake):
347348
assert process.stderr is None
348349

349350

351+
@pytest.mark.filterwarnings("ignore:unclosed file:ResourceWarning")
350352
@pytest.mark.parametrize("fake", [False, True])
351353
def test_universal_newlines(fp, fake):
352354
fp.allow_unregistered(not fake)
@@ -363,6 +365,7 @@ def test_universal_newlines(fp, fake):
363365
assert process.stdout.read() == "Stdout line 1\nStdout line 2\n"
364366

365367

368+
@pytest.mark.filterwarnings("ignore:unclosed file:ResourceWarning")
366369
@pytest.mark.parametrize("fake", [False, True])
367370
def test_text(fp, fake):
368371
fp.allow_unregistered(not fake)
@@ -922,12 +925,14 @@ def test_encoding(fp, fake, argument):
922925
if fake:
923926
fp.register(["whoami"], stdout=username)
924927

925-
output = subprocess.check_output(
926-
["whoami"], **{argument: values.get(argument)}
927-
).strip()
928+
output = (
929+
subprocess.check_output(["whoami"], **{argument: values.get(argument)})
930+
.strip()
931+
.lower()
932+
)
928933

929934
assert isinstance(output, str)
930-
assert output.endswith(username)
935+
assert output.endswith(username.lower())
931936

932937

933938
@pytest.mark.parametrize("command", ["ls -lah", ["ls", "-lah"]])
@@ -1280,3 +1285,73 @@ def spawn_process() -> subprocess.Popen[str]:
12801285
proc.wait()
12811286

12821287
assert proc.stdout.read() == "Stdout line 1\nStdout line 2\n"
1288+
1289+
1290+
def test_stdin_pipe(fp):
1291+
"""
1292+
Test that stdin is a writable buffer when using subprocess.PIPE.
1293+
"""
1294+
fp.register(["my-command"])
1295+
1296+
process = subprocess.Popen(
1297+
["my-command"],
1298+
stdin=subprocess.PIPE,
1299+
)
1300+
1301+
assert process.stdin is not None
1302+
assert process.stdin.writable()
1303+
1304+
# We can write to the buffer.
1305+
process.stdin.write(b"some data")
1306+
process.stdin.flush()
1307+
1308+
# The data can be read back from the buffer for inspection.
1309+
process.stdin.seek(0)
1310+
assert process.stdin.read() == b"some data"
1311+
1312+
# After closing, it should raise a ValueError.
1313+
process.stdin.close()
1314+
with pytest.raises(ValueError):
1315+
process.stdin.write(b"more data")
1316+
1317+
1318+
def test_stdout_stderr_as_file_bug(fp):
1319+
"""
1320+
Test that no TypeError is raised when stdout/stderr is a file
1321+
and the stream is not registered.
1322+
1323+
From GitHub #144
1324+
"""
1325+
# register process with stdout but no stderr
1326+
fp.register(
1327+
["test-no-stderr"],
1328+
stdout="test",
1329+
)
1330+
# register process with stderr but no stdout
1331+
fp.register(
1332+
["test-no-stdout"],
1333+
stderr="test",
1334+
)
1335+
# register process with no streams
1336+
fp.register(
1337+
["test-no-streams"],
1338+
)
1339+
1340+
with NamedTemporaryFile("wb") as temp_file:
1341+
# test with stderr not registered
1342+
process = subprocess.Popen(
1343+
"test-no-stderr", stdout=temp_file.file, stderr=temp_file.file
1344+
)
1345+
process.wait()
1346+
1347+
# test with stdout not registered
1348+
process = subprocess.Popen(
1349+
"test-no-stdout", stdout=temp_file.file, stderr=temp_file.file
1350+
)
1351+
process.wait()
1352+
1353+
# test with no streams registered
1354+
process = subprocess.Popen(
1355+
"test-no-streams", stdout=temp_file.file, stderr=temp_file.file
1356+
)
1357+
process.wait()

0 commit comments

Comments
 (0)