Giter Site home page Giter Site logo

pytest-subprocess's People

Contributors

aklajnert avatar cjp256 avatar henryiii avatar marcgibbons avatar mgorny avatar mrmino avatar pre-commit-ci[bot] avatar rasmuspeders1 avatar romanek-adam avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

pytest-subprocess's Issues

Type error because List is invariant

Version info:

mypy                    0.812
pytest-subprocess       1.1.0

Error:

main.py:34: error: Argument 1 to "register_subprocess" of "FakeProcess" has incompatible type "List[str]"; expected "Union[List[Union[str, Any]], Tuple[Union[str, Any], ...], str, Command]"
main.py:34: note: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance
main.py:34: note: Consider using "Sequence" instead, which is covariant
main.py:34: error: Argument "stdout" to "register_subprocess" of "FakeProcess" has incompatible type "List[str]"; expected "Union[str, bytes, List[Union[str, bytes]], Tuple[Union[str, bytes], ...], None]"

https://mypy-play.net/?mypy=latest&python=3.9&gist=89232479f9811be76ee54460a5f1a895

Fixable if you annotate the arguments as List[Union[str, bytes]].

Does register_subprocess really require the ability to mutate the input list? If it only needs read-only access, then using Sequence would be helpful to avoid the unnecessary upcast.

Exceptions are not propagated to callee functions

Hello,
this is an extension of #11.

Say you have a module, with a function:

file: mod1.py

def f():
    try:
        x = subprocess.run('asdfasf')
        return True
    except subprocess.CalledProcessError:
        return False

and you want to test the except part; i wrote:

def test_f(fake_process):
    def callback_exception(process):
        raise subprocess.CalledProcessError(returncode=0, cmd=["bad"])
    with fake_process.context() as nested_process:
        nested_process.register_subprocess(['yadayada', 'lallalla'], callback=callback_exception)
        y = mod1.f()
        assert not y

but turns out subprocess.run gets executed just fine in mod1.f but just with a returncode=None, which is not what i'd expect

it would be great if, instead of using a callback function, we could have an extra argument to register_subprocess to actually make it raise an exception, even if you're testing code not in the current module.

Thanks!

asyncio.create_subprocess_exec fails if *args used.

Python: 3.8.12
pytest-subprocess: 1.3.1

The function signature for for asyncio.create_subprocess_exec takes an executable and *args as arguments.

If I register a fake_process to mock a call with just an executable, the test will run fine.
If I register a fake_process that needs to handle arguments, the test fails with a TypeError that says for example:

async_shell() takes 2 positional arguments but 5 were given

I've attached an MWE that demonstrates this. Please change .txt to .py. Github would not let me upload .py files.

main.py should run execute when called with python3
text.py will produce two passes and a failure. The failing test will be test_exec_args.

main.txt
test.txt

Support for non-PIPE outputs

I have some code with uses subprocess with a file handle as the stdout argument. The subprocess module will write the output of the command to the file handle, and my code does not directly call communicate. When I register a command with fake_process in the tests with some stdout content specified, the file doesn't seem to have the expected content written to it. Have I overlooked something, or is that not supported by this library?

Example:

# tests/test_sp.py
import subprocess


def test_sp(fake_process):
    fake_process.register_subprocess(["echo", "hello"], stdout="hello\n")

    filename = "file.txt"
    with open(filename, "w") as handle:
        subprocess.run(["echo", "hello"], stdout=handle)

    assert open(filename).read() == "hello\n"

Result:

╰─❯ poetry run pytest
============================= test session starts ==============================
platform darwin -- Python 3.9.7, pytest-5.4.3, py-1.10.0, pluggy-0.13.1
rootdir: [...]/fake_sp_test
plugins: subprocess-1.3.0
collected 1 item                                                               

tests/test_sp.py F                                                       [100%]

=================================== FAILURES ===================================
___________________________________ test_sp ____________________________________

fake_process = <pytest_subprocess.core.FakeProcess object at 0x10ee90190>

    def test_sp(fake_process):
        fake_process.register_subprocess(["echo", "hello"], stdout="hello\n")
    
        filename = "file.txt"
        with open(filename, "w") as handle:
            subprocess.run(["echo", "hello"], stdout=handle)
    
>       assert open(filename).read() == "hello\n"
E       AssertionError: assert '' == 'hello\n'
E         - hello

tests/test_sp.py:11: AssertionError
=========================== short test summary info ============================
FAILED tests/test_sp.py::test_sp - AssertionError: assert '' == 'hello\n'
============================== 1 failed in 0.06s ===============================

TypeError is raised if no stderr is registered, but a file is passed as the stderr argument

Description

An TypeError is raised at fake_popen.py:271 when calling with a file passed to argument stderr if no stderr was given during registration

>           buffer.write(data_type(data))
E           TypeError: encoding without a string argument

pyvenv\Lib\site-packages\pytest_subprocess\fake_popen.py:271: TypeError

Example

Below is an example demonstrating the error.

def test_fp_bug(fp):
    with NamedTemporaryFile("wb", delete=False) as f:
        fp.register(
            ["git", 'rev-parse', 'HEAD'],
            stdout=["47c77698dd8e0e35af00da56806fe98fcb9a1056"],
            )
        p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=f.file)
        p.wait()
        fp.allow_unregistered(True)
        p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=f.file)
        p.wait()

This raises the type error. I can correct this:

--- fake_popen.py   2024-02-10 06:09:01.897909800 -0500
+++ fake_popen.py       2024-02-10 06:08:49.610943500 -0500
@@ -273,7 +267,7 @@
         )
         if isinstance(data, (list, tuple)):
             buffer.writelines([data_type(line + "\n") for line in data])
-        elif data is not None:
+        else:
             buffer.write(data_type(data))

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

pytest_subprocess.asyncio_subprocess has no attribute 'DEVNULL'

fake_process appears to be incompatible with code that uses asyncio.subprocess.DEVNULL.

Example test:

def test_devnull(fake_process):
    async def impl():
        process = await asyncio.create_subprocess_exec(
            "cat",
            stdin=oasyncio.subprocess.DEVNULL,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

    fake_process.register_subprocess("cat")
    asyncio.get_event_loop().run_until_complete(impl())

Output:

E       AttributeError: module 'pytest_subprocess.asyncio_subprocess' has no attribute 'DEVNULL'

From looking at asyncio_subprocess.py, my guess would be that DEVNULL needs to be imported here (and probably STDOUT too) .

check env of subprocess

I have some code along the lines of

subprocess.run(['thing'], env={'KEY': some_carefully_constructed_value()})

I can fake_process.register_subprocess(['thing'])
but there doesn't appear to be a way to check the env that was passed to the process

I also had

subprocess.run(f'KEY={some_carefully_constructed_value()} thing', shell=True)

but I couldn't find a way to grab that either

1.4.1: pytest id failing and warnings

I'm packaging your module as an rpm package so I'm using the typical PEP517 based build, install and test cycle used on building packages from non-root account.

  • python3 -sBm build -w --no-isolation
  • because I'm calling build with --no-isolation I'm using during all processes only locally installed modules
  • install .whl file in </install/prefix>
  • run pytest with PYTHONPATH pointing to sitearch and sitelib inside </install/prefix>

Support __fspath__ protocol?

It would be nice to support anything that supports PathLike. Most of Python supports it except the subprocess.run calls, so I'd understand if you didn't want to, but it's extremely handy (and, as I said, supported pretty much everywhere else - including the latest main branch of nox). It's easy to do - os.fspath passes through strings and bytes already and calls __fspath__ on anything else. (Correction: 3.8+ does support it)

(Currently, it just silently accepts it and doesn't match, which was confusing me for some time until I realized I'd forgotten to convert to a string on one test register call).

stderr returning None

FYI thanks for creating this package. clean and pytest way of testing subprocess commands.

I have followed the simple guide in the readme and created a stderr arg for fake_process.register_subprocess. However, process.stderr always returns None.
Is there something I am missing in generating the stderr? I can get stdout just fine it seems.

async process objects' stdout and stderr are not StreamReaders when using PIPE

According to the asyncio.subprocess.PIPE docs:

If PIPE is passed to stdin argument, the Process.stdin attribute will point to a StreamWriter instance.

If PIPE is passed to stdout or stderr arguments, the Process.stdout and Process.stderr attributes will point to StreamReader instances.

It looks like the object that fake_subprocess returns from create_process_exec (and probably create_process_shell) don't mimic this correctly. Here's an example test:

import asyncio
import os

def test_stdio_and_stderr(fake_process):
    async def impl():
        process = await asyncio.create_subprocess_exec(
            "ls", "/",
            stdin=open(os.devnull, "r"), # work around #64
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        # wait for the process
        async def _read_stream(
            stream: asyncio.StreamReader, logtype: str,
        ):
            while True:
                line = await stream.readline()
                if not line:
                    break
                # do something with line

        await asyncio.wait(
            [
                asyncio.create_task(
                    _read_stream(process.stdout, "stdout")
                ),
                asyncio.create_task(
                    _read_stream(process.stderr, "stderr")
                ),
                asyncio.create_task(process.wait()),
            ]
        )

    fake_process.register_subprocess(["ls", "/"], stdout=[b"one", b"two"])
    asyncio.get_event_loop().run_until_complete(impl())

    # Force pytest to display the asyncio exception messages
    assert False

The expected output is for there to be a failed assertion -- but nothing else. Here's the actual output:

------------------------------------------- Captured log call --------------------------------------------
ERROR    asyncio:base_events.py:1707 Task exception was never retrieved
future: <Task finished name='Task-2' coro=<test_stdio_and_stderr.<locals>.impl.<locals>._read_stream() done, defined at /elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py:15> exception=TypeError("object bytes can't be used in 'await' expression")>
Traceback (most recent call last):
  File "/elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py", line 19, in _read_stream
    line = await stream.readline()
TypeError: object bytes can't be used in 'await' expression
ERROR    asyncio:base_events.py:1707 Task exception was never retrieved
future: <Task finished name='Task-3' coro=<test_stdio_and_stderr.<locals>.impl.<locals>._read_stream() done, defined at /elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py:15> exception=TypeError("object bytes can't be used in 'await' expression")>
Traceback (most recent call last):
  File "/elm0/jranieri/src/gtirb-server2/server/test/test_stdio.py", line 19, in _read_stream
    line = await stream.readline()
TypeError: object bytes can't be used in 'await' expression
======================================== short test summary info =========================================
FAILED test_stdio.py::test_stdio_and_stderr - assert False

Raising exception in callback works with Popen but not with check_output

If I add this test to tests/test_subprocess.py:

def test_raise_exception_check_output(fake_process):
    def callback_function(process):
        process.returncode = 1
        raise PermissionError("exception raised by subprocess")

    fake_process.register_subprocess(["test"], callback=callback_function)

    with pytest.raises(PermissionError, match="exception raised by subprocess"):
        process = subprocess.check_output(["test"])

    assert process.returncode == 1

The test fails.

This should behave the same as the code in test_raise_exception does, shouldn't it?

Cannot raise exception from subprocess

I'm trying to test a part of code, in which a call to subprocess.run() raises a PermissionError. How can this be done with the pytest-subprocess fixture?

Thanks for the great piece of code and your support in advance ;)

Running tests with pytest-asyncio 0.23.5.post1 makes test_asyncio.py tests fail due to DeprecationWarning

We intend to update pytest-asyncio to the latest release (0.23.5.post1) in Fedora. Testing packages that depend on it, showed pytest-subprocess tests failing, because warnings are turned into errors as per:

filterwarnings =
error
ignore::pytest.PytestUnraisableExceptionWarning

resulting in:

==================================== ERRORS ====================================
__________________ ERROR at setup of test_basic_usage[shell] ___________________
fixturedef = <FixtureDef argname='event_loop' scope='function' baseid='tests/test_asyncio.py'>
    @pytest.hookimpl(hookwrapper=True)
    def pytest_fixture_setup(
        fixturedef: FixtureDef,
    ) -> Generator[None, Any, None]:
        """Adjust the event loop policy when an event loop is produced."""
        if fixturedef.argname == "event_loop":
            # The use of a fixture finalizer is preferred over the
            # pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once
            # for each fixture, whereas the hook may be invoked multiple times for
            # any specific fixture.
            # see https://github.com/pytest-dev/pytest/issues/5848
            _add_finalizers(
                fixturedef,
                _close_event_loop,
                _restore_event_loop_policy(asyncio.get_event_loop_policy()),
                _provide_clean_event_loop,
            )
            outcome = yield
            loop: asyncio.AbstractEventLoop = outcome.get_result()
            # Weird behavior was observed when checking for an attribute of FixtureDef.func
            # Instead, we now check for a special attribute of the returned event loop
            fixture_filename = inspect.getsourcefile(fixturedef.func)
            if not getattr(loop, "__original_fixture_loop", False):
                _, fixture_line_number = inspect.getsourcelines(fixturedef.func)
>               warnings.warn(
                    _REDEFINED_EVENT_LOOP_FIXTURE_WARNING
                    % (fixture_filename, fixture_line_number),
                    DeprecationWarning,
                )
E               DeprecationWarning: The event_loop fixture provided by pytest-asyncio has been redefined in
E               /builddir/build/BUILD/pytest-subprocess-1.5.0/tests/test_asyncio.py:14
E               Replacing the event_loop fixture with a custom implementation is deprecated
E               and will lead to errors in the future.
E               If you want to request an asyncio event loop with a scope other than function
E               scope, use the "scope" argument to the asyncio mark when marking the tests.
E               If you want to return different types of event loops, use the event_loop_policy
E               fixture.
/usr/lib/python3.12/site-packages/pytest_asyncio/plugin.py:769: DeprecationWarning
_________________ ERROR at teardown of test_basic_usage[shell] _________________
    def _close_event_loop() -> None:
        policy = asyncio.get_event_loop_policy()
        try:
>           loop = policy.get_event_loop()
/usr/lib/python3.12/site-packages/pytest_asyncio/plugin.py:823: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
self = <asyncio.unix_events._UnixDefaultEventLoopPolicy object at 0x7fd70d9466c0>
    def get_event_loop(self):
        """Get the event loop for the current context.
    
        Returns an instance of EventLoop or raises an exception.
        """
        if (self._local._loop is None and
                not self._local._set_called and
                threading.current_thread() is threading.main_thread()):
            stacklevel = 2
            try:
                f = sys._getframe(1)
            except AttributeError:
                pass
            else:
                # Move up the call stack so that the warning is attached
                # to the line outside asyncio itself.
                while f:
                    module = f.f_globals.get('__name__')
                    if not (module == 'asyncio' or module.startswith('asyncio.')):
                        break
                    f = f.f_back
                    stacklevel += 1
            import warnings
>           warnings.warn('There is no current event loop',
                          DeprecationWarning, stacklevel=stacklevel)
E           DeprecationWarning: There is no current event loop
/usr/lib64/python3.12/asyncio/events.py:697: DeprecationWarning

(Above is a sample. The same happens for all tests from test_asyncio.py)

We've turned off that setting for now.

check_output fails with stderr set to subprocess.STDOUT

This is how I am invoking a process in the code I am testing:

        output = subprocess.check_output(
            '/usr/lib/update-notifier/apt-check',
            stderr=subprocess.STDOUT).decode('utf8')

This is how I am registering the process:

    fake_process.register_subprocess('/usr/lib/update-notifier/apt-check',
                                     stdout=("0;0",))

This is what I get when I try to run my test:

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
client/plugins/os_updates.py:255: in ubuntu_checker
    output = subprocess.check_output(
/usr/lib/python3.8/subprocess.py:411: in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
/usr/lib/python3.8/subprocess.py:489: in run
    with Popen(*popenargs, **kwargs) as process:
../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:262: in dispatch
    result.configure(**kwargs)
../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:126: in configure
    self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pytest_subprocess.core.FakePopen object at 0x7f4608c8cdc0>, input = None
io_base = <_io.BytesIO object at 0x7f46081dc4f0>

    def _prepare_buffer(self, input, io_base=None):
        linesep = self._convert(os.linesep)
    
        if isinstance(input, (list, tuple)):
            input = linesep.join(map(self._convert, input))
    
        if isinstance(input, str) and not self.text_mode:
            input = input.encode()
    
        if isinstance(input, bytes) and self.text_mode:
            input = input.decode()
    
        if input:
            if not input.endswith(linesep):
                input += linesep
            if self.text_mode and self.__universal_newlines:
                input = input.replace("\r\n", "\n")
    
        if io_base is not None:
>           input = io_base.getvalue() + input
E           TypeError: can't concat NoneType to bytes

../../.virtualenvs/PenguinDome/lib/python3.8/site-packages/pytest_subprocess/core.py:149: TypeError

Expose `ProcessDispatcher._get_process` as a public API or return a `Popen` result from `register(...)`

It is impossible to properly unit-test functionalities like sending a signal, when the tested class facades subprocess.Popen without exposing the resulting process object. Rummaging in class internals during unit tests is a bad practice which leads to brittle testcases.

It would be nice if pytest-subprocess had a way of getting the result of faked Popen (or its proxy). That way we could assert some side effect without having to extract the Popen result from the unit under test:

def test_kills_the_process(fake_subprocess):
    process = fake_subprocess.register(["command"])
    run_and_kill("command")
    assert process.received_signals() == (signals.SIGKILL,)

1.5.0 + master 4187ef2c): pytest fails in more than half units

I'm packaging your module as an rpm package so I'm using the typical PEP517 based build, install and test cycle used on building packages from non-root account.

  • python3 -sBm build -w --no-isolation
  • because I'm calling build with --no-isolation I'm using during all processes only locally installed modules
  • install .whl file in </install/prefix> using installer module
  • run pytest with $PYTHONPATH pointing to sitearch and sitelib inside </install/prefix>
  • build is performed in env which is cut off from access to the public network (pytest is executed with -m "not network")

Python 3.9.18 and pytest 8.1.1.

Detailed list of installed modules in build env:
Package                       Version
----------------------------- -----------
alabaster                     0.7.16
anyio                         4.3.0
Babel                         2.14.0
build                         1.1.1
changelogd                    0.1.7
charset-normalizer            3.3.2
click                         8.1.7
docutils                      0.20.1
exceptiongroup                1.1.3
idna                          3.6
imagesize                     1.4.1
importlib_metadata            7.1.0
iniconfig                     2.0.0
installer                     0.7.0
Jinja2                        3.1.3
MarkupSafe                    2.1.3
packaging                     24.0
pluggy                        1.4.0
Pygments                      2.17.2
pyproject_hooks               1.0.0
pytest                        8.1.1
pytest-asyncio                0.23.6
pytest-rerunfailures          12.0
python-dateutil               2.9.0.post0
requests                      2.31.0
ruamel.yaml                   0.18.5
ruamel.yaml.clib              0.2.8
setuptools                    69.1.1
sniffio                       1.3.0
snowballstemmer               2.2.0
Sphinx                        7.2.6
sphinx-autodoc-typehints      2.0.0
sphinxcontrib-applehelp       1.0.8
sphinxcontrib-devhelp         1.0.5
sphinxcontrib-htmlhelp        2.0.5
sphinxcontrib-jsmath          1.0.1
sphinxcontrib-qthelp          1.0.7
sphinxcontrib-serializinghtml 1.1.10
tokenize_rt                   5.2.0
toml                          0.10.2
tomli                         2.0.1
typing_extensions             4.10.0
urllib3                       1.26.18
wheel                         0.43.0
zipp                          3.17.0

Please let me know if you need more details or want me to perform some diagnostics.

Allow any number of occurrences

Currently a finite number of occurrences per command is allowed, e.g. for pass_command. Would be nice if an arbitrary number of occurrences is supported.

Seems like currently this behavior is implemented using a deque. Didn't read the code very thoroughly so I'm not sure how the deque is used, but one approach could be storing an iterator instead which would support an infinite iterator e.g. via itertools.repeat.

doesnt support `encoding='utf-8'`

i have a function that executes:

def f():
    return subprocess.check_output('secret_command', encoding='utf-8').strip()

but when i test it with:

    retvalue = '1029384756'
    with fake_process.context() as nested_process:
        nested_process.register_subprocess('secret_command', stdout=retvalue)
        node_id = f()
        assert node_id == retvalue

it fails with:

>           assert node_id == retvalue
E           AssertionError: assert b'1029384756' == '1029384756'

i believe because stdout is passed to _prepare_buffer() https://github.com/aklajnert/pytest-subprocess/blob/master/pytest_subprocess/core.py#L135 which will encode() the output. there's a chance to avoid it by setting self.text but i couldnt find a way from register_subprocess to set that

pytest_subprocess.utils.Command is not iterable

Hello,

I am having troubles with one of our mocked processes.

Specifically the args attribute.

I'm running into a problem where instead of getting a tuple from .args we get an object of type pytest_subprocess.utils.Command. We need to iterate over args which should be a tuple but your Command class which was introduced in 1.0.0 is not iterable.

When you print p.args (where p is the returned process) it prints what looks to be a tuple but is not. The tuple itself is in p.args.command.

See the below:

============================
contents and type of p.args
============================
('command', 'being', 'run', 'plus', 'passed', 'args')
<class 'pytest_subprocess.utils.Command'>
============================
contents and type of p.args.command
============================
('command', 'being', 'run', 'plus', 'passed', 'args')
<class 'tuple'>

pytest_subprocess.utils.Command can be made iterable and solve this problem if an __iter__ method is added to the Command class, adding the below locally fixes the problem:

def __iter__(self):
      return iter(self.command)

Kind Regards
Gino Cubeddu

1.5.0: sphinx warnings `reference target not found`

First of all currently on use sphinx-build command to build documentation out of source tree sphinx cannot find pytest-subprocess code

+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v7.2.6
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
writing output...
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'fake_process.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
WARNING: autodoc: failed to import class 'utils.Any' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } done
build succeeded, 2 warnings.

This can be fixed by patch like below:

--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,9 +8,10 @@
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath(".."))
+
 import datetime
 from pathlib import Path

This patch fixes what is in the comment and that can of fix is suggested in sphinx example copy.py https://www.sphinx-doc.org/en/master/usage/configuration.html#example-of-configuration-file

Than .. on building my packages I'm using sphinx-build command with -n switch which shows warmings about missing references. These are not critical issues.

+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v7.2.6
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
writing output...
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } <unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Any
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.process_recorder.ProcessRecorder
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: os.PathLike
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Program
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.utils.Command
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Sequence
<unknown>:1: WARNING: py:data reference target not found: typing.Union
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Any
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:class reference target not found: collections.abc.Callable
<unknown>:1: WARNING: py:class reference target not found: pytest_subprocess.process_recorder.ProcessRecorder
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
<unknown>:1: WARNING: py:data reference target not found: typing.Optional
done
build succeeded, 59 warnings.

You can peak on fixes that kind of issues in other projects
RDFLib/rdflib-sqlalchemy#95
RDFLib/rdflib#2036
click-contrib/sphinx-click@abc31069
frostming/unearth#14
jaraco/cssutils#21
latchset/jwcrypto#289
latchset/jwcrypto#289
pypa/distlib@98b9b89f
pywbem/pywbem#2895
sissaschool/elementpath@bf869d9e
sissaschool/xmlschema@42ea98f2
sqlalchemy/sqlalchemy@5e88e6e8

2 tests fail

__________________________________________________________________________________________ ERROR at setup of test_multiple_wait[True] __________________________________________________________________________________________

self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_multiple_wait[True]>

    def pytest_runtest_setup(self, item):
        """
        Pytest hook to modify the test before it's run.
    
        :param item:
            The test item.
        """
        if not self._has_flaky_attributes(item):
            if hasattr(item, 'iter_markers'):
                for marker in item.iter_markers(name='flaky'):
>                   self._make_test_flaky(item, *marker.args, **marker.kwargs)
E                   TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'

/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
_____________________________________________________________________________________ ERROR at setup of test_raise_exception_check_output ______________________________________________________________________________________

self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_raise_exception_check_output>

    def pytest_runtest_setup(self, item):
        """
        Pytest hook to modify the test before it's run.
    
        :param item:
            The test item.
        """
        if not self._has_flaky_attributes(item):
            if hasattr(item, 'iter_markers'):
                for marker in item.iter_markers(name='flaky'):
>                   self._make_test_flaky(item, *marker.args, **marker.kwargs)
E                   TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'

/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
_________________________________________________________________________________________ ERROR at setup of test_multiple_wait[False] __________________________________________________________________________________________

self = <flaky.flaky_pytest_plugin.FlakyPlugin object at 0x8cfd165e0>, item = <Function test_multiple_wait[False]>

    def pytest_runtest_setup(self, item):
        """
        Pytest hook to modify the test before it's run.
    
        :param item:
            The test item.
        """
        if not self._has_flaky_attributes(item):
            if hasattr(item, 'iter_markers'):
                for marker in item.iter_markers(name='flaky'):
>                   self._make_test_flaky(item, *marker.args, **marker.kwargs)
E                   TypeError: _make_test_flaky() got an unexpected keyword argument 'reruns'

/usr/local/lib/python3.9/site-packages/flaky/flaky_pytest_plugin.py:244: TypeError
=========================================================================================================== FAILURES ===========================================================================================================
______________________________________________________________________________________________ test_documentation[docs/index.rst] ______________________________________________________________________________________________

testdir = <Testdir local('/tmp/pytest-of-yuri/pytest-12/test_documentation0')>, rst_file = 'docs/index.rst'

    @pytest.mark.parametrize("rst_file", ("docs/index.rst", "README.rst"))
    def test_documentation(testdir, rst_file):
        imports = "\n".join(
            [
                "import asyncio",
                "import os",
                "import sys",
                "",
                "import pytest",
                "import pytest_subprocess",
                "import subprocess",
            ]
        )
    
        setup_fixture = (
            "\n\n"
            "@pytest.fixture(autouse=True)\n"
            "def setup():\n"
            "    os.chdir(os.path.dirname(__file__))\n\n"
        )
    
        event_loop_fixture = (
            "\n\n"
            "@pytest.fixture(autouse=True)\n"
            "def event_loop(request):\n"
            "    policy = asyncio.get_event_loop_policy()\n"
            '    if sys.platform == "win32":\n'
            "        loop = asyncio.ProactorEventLoop()\n"
            "    else:\n"
            "        loop = policy.get_event_loop()\n"
            "    yield loop\n"
            "    loop.close()\n"
        )
    
        code_blocks = "\n".join(get_code_blocks(ROOT_DIR / rst_file))
        testdir.makepyfile(
            imports + setup_fixture + event_loop_fixture + "\n" + code_blocks
        )
    
>       result = testdir.inline_run()

/usr/ports/devel/py-pytest-subprocess/work-py39/pytest-subprocess-1.5.0/tests/test_examples.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.9/site-packages/_pytest/legacypath.py:173: in inline_run
    return self._pytester.inline_run(
/usr/local/lib/python3.9/site-packages/_pytest/pytester.py:1135: in inline_run
    ret = main([str(x) for x in args], plugins=plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:147: in main
    config = _prepareconfig(args, plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:328: in _prepareconfig
    config = pluginmanager.hook.pytest_cmdline_parse(
/usr/local/lib/python3.9/site-packages/pluggy/_hooks.py:265: in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:80: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/_pytest/helpconfig.py:103: in pytest_cmdline_parse
    config: Config = outcome.get_result()
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1067: in pytest_cmdline_parse
    self.parse(args)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1354: in parse
    self._preparse(args, addopts=addopts)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1237: in _preparse
    self.pluginmanager.load_setuptools_entrypoints("pytest11")
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:288: in load_setuptools_entrypoints
    self.register(plugin, name=ep.name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:488: in register
    ret: Optional[str] = super().register(plugin, name)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:103: in register
    hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:459: in parse_hookimpl_opts
    return _get_legacy_hook_marks(
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:373: in _get_legacy_hook_marks
    warn_explicit_for(cast(FunctionType, method), message)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = <function pytest_load_initial_conftests at 0x8744c2820>
message = PytestDeprecationWarning('The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or at...igure the hooks.\n See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers')

    def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
        """
        Issue the warning :param:`message` for the definition of the given :param:`method`
    
        this helps to log warnigns for functions defined prior to finding an issue with them
        (like hook wrappers being marked in a legacy mechanism)
        """
        lineno = method.__code__.co_firstlineno
        filename = inspect.getfile(method)
        module = method.__module__
        mod_globals = method.__globals__
        try:
            warnings.warn_explicit(
                message,
                type(message),
                filename=filename,
                module=module,
                registry=mod_globals.setdefault("__warningregistry__", {}),
                lineno=lineno,
            )
        except Warning as w:
            # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
>           raise type(w)(f"{w}\n at {filename}:{lineno}") from None
E           pytest.PytestDeprecationWarning: The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or attributes).
E           Please use the pytest.hookimpl(tryfirst=True) decorator instead
E            to configure the hooks.
E            See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers
E            at /usr/local/lib/python3.9/site-packages/pytest_cov/plugin.py:115

/usr/local/lib/python3.9/site-packages/_pytest/warning_types.py:170: PytestDeprecationWarning
----------------------------------------------------------------------------------------------------- Captured stderr call -----------------------------------------------------------------------------------------------------
<string>:30: (ERROR/3) Unknown directive type "toctree".

.. toctree::
   :maxdepth: 2

   usage
   api
   history



<string>:42: (ERROR/3) Unknown interpreted text role "ref".
<string>:43: (ERROR/3) Unknown interpreted text role "ref".
<string>:44: (ERROR/3) Unknown interpreted text role "ref".
________________________________________________________________________________________________ test_documentation[README.rst] ________________________________________________________________________________________________

testdir = <Testdir local('/tmp/pytest-of-yuri/pytest-12/test_documentation1')>, rst_file = 'README.rst'

    @pytest.mark.parametrize("rst_file", ("docs/index.rst", "README.rst"))
    def test_documentation(testdir, rst_file):
        imports = "\n".join(
            [
                "import asyncio",
                "import os",
                "import sys",
                "",
                "import pytest",
                "import pytest_subprocess",
                "import subprocess",
            ]
        )
    
        setup_fixture = (
            "\n\n"
            "@pytest.fixture(autouse=True)\n"
            "def setup():\n"
            "    os.chdir(os.path.dirname(__file__))\n\n"
        )
    
        event_loop_fixture = (
            "\n\n"
            "@pytest.fixture(autouse=True)\n"
            "def event_loop(request):\n"
            "    policy = asyncio.get_event_loop_policy()\n"
            '    if sys.platform == "win32":\n'
            "        loop = asyncio.ProactorEventLoop()\n"
            "    else:\n"
            "        loop = policy.get_event_loop()\n"
            "    yield loop\n"
            "    loop.close()\n"
        )
    
        code_blocks = "\n".join(get_code_blocks(ROOT_DIR / rst_file))
        testdir.makepyfile(
            imports + setup_fixture + event_loop_fixture + "\n" + code_blocks
        )
    
>       result = testdir.inline_run()

/usr/ports/devel/py-pytest-subprocess/work-py39/pytest-subprocess-1.5.0/tests/test_examples.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.9/site-packages/_pytest/legacypath.py:173: in inline_run
    return self._pytester.inline_run(
/usr/local/lib/python3.9/site-packages/_pytest/pytester.py:1135: in inline_run
    ret = main([str(x) for x in args], plugins=plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:147: in main
    config = _prepareconfig(args, plugins)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:328: in _prepareconfig
    config = pluginmanager.hook.pytest_cmdline_parse(
/usr/local/lib/python3.9/site-packages/pluggy/_hooks.py:265: in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:80: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/usr/local/lib/python3.9/site-packages/_pytest/helpconfig.py:103: in pytest_cmdline_parse
    config: Config = outcome.get_result()
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1067: in pytest_cmdline_parse
    self.parse(args)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1354: in parse
    self._preparse(args, addopts=addopts)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:1237: in _preparse
    self.pluginmanager.load_setuptools_entrypoints("pytest11")
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:288: in load_setuptools_entrypoints
    self.register(plugin, name=ep.name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:488: in register
    ret: Optional[str] = super().register(plugin, name)
/usr/local/lib/python3.9/site-packages/pluggy/_manager.py:103: in register
    hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:459: in parse_hookimpl_opts
    return _get_legacy_hook_marks(
/usr/local/lib/python3.9/site-packages/_pytest/config/__init__.py:373: in _get_legacy_hook_marks
    warn_explicit_for(cast(FunctionType, method), message)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

method = <function pytest_load_initial_conftests at 0x8744c2820>
message = PytestDeprecationWarning('The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or at...igure the hooks.\n See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers')

    def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
        """
        Issue the warning :param:`message` for the definition of the given :param:`method`
    
        this helps to log warnigns for functions defined prior to finding an issue with them
        (like hook wrappers being marked in a legacy mechanism)
        """
        lineno = method.__code__.co_firstlineno
        filename = inspect.getfile(method)
        module = method.__module__
        mod_globals = method.__globals__
        try:
            warnings.warn_explicit(
                message,
                type(message),
                filename=filename,
                module=module,
                registry=mod_globals.setdefault("__warningregistry__", {}),
                lineno=lineno,
            )
        except Warning as w:
            # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
>           raise type(w)(f"{w}\n at {filename}:{lineno}") from None
E           pytest.PytestDeprecationWarning: The hookimpl pytest_load_initial_conftests uses old-style configuration options (marks or attributes).
E           Please use the pytest.hookimpl(tryfirst=True) decorator instead
E            to configure the hooks.
E            See https://docs.pytest.org/en/latest/deprecations.html#configuring-hook-specs-impls-using-markers
E            at /usr/local/lib/python3.9/site-packages/pytest_cov/plugin.py:115

/usr/local/lib/python3.9/site-packages/_pytest/warning_types.py:170: PytestDeprecationWarning
=================================================================================================== short test summary info ====================================================================================================
SKIPPED [4] tests/test_asyncio.py:114: condition: sys.platform!="win32"
===================================================================================== 2 failed, 127 passed, 4 skipped, 3 errors in 12.56s ======================================================================================
*** Error code 1

Version: 1.5.0
Python-3.9
FreeBSD 13.2

conflict with mock.patch

I have a script that internally is using subproccess.run to call some external executable (e.g dosomething).
So for example

$ python myscript.py do

It internally results in a subprocess call of dosomething /tmp
Test about this base functionality is like

def test_main(fp):
    fp.register(['dosomething', '/tmp'])
    main(['do'])

Some of the script configurations (sort of hidden) are using env variables. So for example

$ MYSCRIPT_TMP_FOLDER=/mytmp  python myscript.py do

It internally results in a subprocess call of dosomething /mytmp

My attempt to test it is like

def test_main(fp):
    fp.register(['dosomething', '/mytmp'])
    with mock.patch.dict(os.environ, {'MYSCRIPT_TMP_FOLDER': 'mytmp'}):
        main(['do'])

Unfortunately the with mock (or something else) seems to interfere with fp. As result the true subprocess and so the dosomething executable is called during the test.

Expected behavior is that fp to keep to be able to intercept submodule call and not to perform it.

Matching on program name but not path

I've been really enjoying this, but I have had a small pain point: the name must be fully specified. Here's an example:

cmake_path = Path(cmake.CMAKE_BIN_DIR) / "cmake"
fp.register([os.fspath(cmake_path), "--version"], stdout="3.15.0")

I tried the nuclear option, but it didn't seem to work (and really isn't what I would normally want to do anyway):

fp.register([fp.any(), "--version"], stdout="3.15.0")

A really handy shortcut would be something like this:

fp.register([fp.program("make"), "--version"], stdout="3.15.0")

which would only match the final component of the path. Maybe this could even be automatic, such as "thing" matching "/any/thing" or shutil.which("thing") it it's not an absolute path. I'd think the automatic one should require it be on the path - but I'm also interested in a fuzzy one that doesn't require it be on the path.

`communicate()` does not raise exceptions from callbacks

This test works as expected - it fails because of an exception:

def callback_function(process):
    process.returncode = 1
    raise PermissionError("exception raised by subprocess")

def test_raise_exception(fake_process):
    fake_process.register(["test"], callback=callback_function)
    process = subprocess.Popen(["test"])
    process.wait()
       raise PermissionError("exception raised by subprocess") 
       PermissionError: exception raised by subprocess

Replacing process.wait() with process.communicate() does not trigger this side effect. Since communicate is basically wait + read, shouldn't it raise an exception too?

Bug: infinite timeout when reading stdout with callback

When fp.register stubs an asyncio subprocess, and a callback is provided, process.stdout.read() or process.stdout.readline() will loop infinitely. When no callback is provided, the function works as expected.

Raised originally as a PR with a testcase: #117

This issue is covered by an xfailing testcase: test_asyncio_subprocess_using_callback.

Module already imported so cannot be rewritten: subprocess

When I try to load the pytest-subprocess plugin using the -p pytest option the load fails because it is in conflict with the Python subprocess library:

$ env - pytest -W error -p subprocess
Traceback (most recent call last):
  File "/usr/bin/pytest", line 8, in <module>
    sys.exit(console_main())
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 201, in console_main
    code = main()
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 156, in main
    config = _prepareconfig(args, plugins)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 341, in _prepareconfig
    config = pluginmanager.hook.pytest_cmdline_parse(
  File "/usr/lib/python3.9/vendor-packages/pluggy/_hooks.py", line 513, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  File "/usr/lib/python3.9/vendor-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 139, in _multicall
    raise exception.with_traceback(exception.__traceback__)
  File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 122, in _multicall
    teardown.throw(exception)  # type: ignore[union-attr]
  File "/usr/lib/python3.9/vendor-packages/_pytest/helpconfig.py", line 105, in pytest_cmdline_parse
    config = yield
  File "/usr/lib/python3.9/vendor-packages/pluggy/_callers.py", line 103, in _multicall
    res = hook_impl.function(*args)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1140, in pytest_cmdline_parse
    self.parse(args)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1490, in parse
    self._preparse(args, addopts=addopts)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1373, in _preparse
    self.pluginmanager.consider_preparse(args, exclude_only=False)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 787, in consider_preparse
    self.consider_pluginarg(parg)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 810, in consider_pluginarg
    self.import_plugin(arg, consider_entry_points=True)
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 850, in import_plugin
    self.rewrite_hook.mark_rewrite(importspec)
  File "/usr/lib/python3.9/vendor-packages/_pytest/assertion/rewrite.py", line 263, in mark_rewrite
    self._warn_already_imported(name)
  File "/usr/lib/python3.9/vendor-packages/_pytest/assertion/rewrite.py", line 270, in _warn_already_imported
    self.config.issue_config_time_warning(
  File "/usr/lib/python3.9/vendor-packages/_pytest/config/__init__.py", line 1528, in issue_config_time_warning
    warnings.warn(warning, stacklevel=stacklevel)
pytest.PytestAssertRewriteWarning: Module already imported so cannot be rewritten: subprocess
$

Possible solution would be to rename the plugin (in the entry_points.txt file).

Regex matching commands

I want to mock specific calls to subprocess but the command inputs are varying, e.g. cmake -S/path/to/generated_data -B/random/build_path. I would thus like to use regex match these arguments and make sure other commands like cmake --build are not called.

1.5.1 installs stray top-level `tests` package

The new release installs a stray top-level tests package, i.e.:

/usr/lib/python3.12/site-packages/tests/__init__.py
/usr/lib/python3.12/site-packages/tests/conftest.py
/usr/lib/python3.12/site-packages/tests/example_script.py
...

test_multiple_wait[False] fails

__________________________ test_multiple_wait[False] ___________________________

fake_process = <pytest_subprocess.core.FakeProcess object at 0x7ffff6a99a60>
fake = False

    @pytest.mark.parametrize("fake", [False, True])
    def test_multiple_wait(fake_process, fake):
        """
        Wait multiple times for 0.2 seconds with process lasting for 0.5.
        Third wait shall not raise an exception.
        """
        fake_process.allow_unregistered(not fake)
        if fake:
            fake_process.register_subprocess(
                ["python", "example_script.py", "wait"], wait=0.5,
            )

        process = subprocess.Popen(("python", "example_script.py", "wait"),)
        with pytest.raises(subprocess.TimeoutExpired):
            process.wait(timeout=0.2)

        with pytest.raises(subprocess.TimeoutExpired):
            process.wait(timeout=0.2)

>       process.wait(0.2)

/build/source/tests/test_subprocess.py:420:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/nix/store/1b0k48v6131drfl737qr1v53yk37k4d6-python3-3.9.2/lib/python3.9/subprocess.py:1189: in wait
    return self._wait(timeout=timeout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Popen: returncode: None args: ['python', 'example_script.py', 'wait']>
timeout = 0.2

    def _wait(self, timeout):
        """Internal implementation of wait() on POSIX."""
        if self.returncode is not None:
            return self.returncode

        if timeout is not None:
            endtime = _time() + timeout
            # Enter a busy loop if we have a timeout.  This busy loop was
            # cribbed from Lib/threading.py in Thread.wait() at r71065.
            delay = 0.0005 # 500 us -> initial delay of 1 ms
            while True:
                if self._waitpid_lock.acquire(False):
                    try:
                        if self.returncode is not None:
                            break  # Another thread waited.
                        (pid, sts) = self._try_wait(os.WNOHANG)
                        assert pid == self.pid or pid == 0
                        if pid == self.pid:
                            self._handle_exitstatus(sts)
                            break
                    finally:
                        self._waitpid_lock.release()
                remaining = self._remaining_time(endtime)
                if remaining <= 0:
>                   raise TimeoutExpired(self.args, timeout)
E                   subprocess.TimeoutExpired: Command '('python', 'example_script.py', 'wait')' timed out after 0.2 seconds

/nix/store/1b0k48v6131drfl737qr1v53yk37k4d6-python3-3.9.2/lib/python3.9/subprocess.py:1911: TimeoutExpired

Registering a subprocess with a single-argument list or tuple should be the same as a string

If the process I am invoking in the code I'm testing has no arguments, then I should be able to register it with fake_process using either just the same string that is being passed to check_output, or as a list or tuple, but right now, only the former works and the latter doesn't.

For example, if this is what I am testing:

output = subprocess.check_output("some-command")

Then this:

fake_process.register_subprocess(("some-command",))

and this:

fake_process.register_subprocess("some-command")

should both work, but only the latter works.

Non-exact process matching

Is it possible to specify processes to mock without specifying the entire command?

For example, being able to do

fake_process.register_subprocess("cp")

And have it fake all calls to cp, regardless of arguments? At the moment I'm having to specify every single possible argument that my application can use in order to get a match.

Am I missing something?

Fail to mock: 'method' object is not subscriptable

Not sure where the error comes from, but I get a "'method' object is not subscriptable". The code path it was trying to evaluate is:

        def _spawn_process() -> subprocess.Popen[bytes]:
            return subprocess.Popen(
                self.to_popen(),
                cwd=cwd,
                shell=shell,
                env=actual_env.to_popen() if actual_env is not None else None,
                start_new_session=True,
                stdin=subprocess.DEVNULL,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT if join else subprocess.PIPE,
                executable=executable)

        # Spawn the child process
        try:
            process = _spawn_process()

fake_process is incompatible with anyio

fake_process appears to be incompatible with code that uses anyio with asyncio.

Example test:

def test_anyio(fake_process):
    async def impl():
        await anyio.sleep(1)

    asyncio.get_event_loop().run_until_complete(impl())

Error:

/usr/lib/python3.8/asyncio/base_events.py:616: in run_until_complete
    return future.result()
tmp/async_devnull_test.py:28: in impl
    await anyio.sleep(1)
/usr/local/lib/python3.8/dist-packages/anyio/_core/_eventloop.py:69: in sleep
    return await get_asynclib().sleep(delay)
/usr/local/lib/python3.8/dist-packages/anyio/_core/_eventloop.py:140: in get_asynclib
    return import_module(modulename)
/usr/lib/python3.8/importlib/__init__.py:127: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1014: in _gcd_import
    ???
<frozen importlib._bootstrap>:991: in _find_and_load
    ???
<frozen importlib._bootstrap>:975: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:671: in _load_unlocked
    ???
/usr/local/lib/python3.8/dist-packages/_pytest/assertion/rewrite.py:170: in exec_module
    exec(co, module.__dict__)
/usr/local/lib/python3.8/dist-packages/anyio/_backends/_asyncio.py:897: in <module>
    class Process(abc.Process):
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    @dataclass(eq=False)
    class Process(abc.Process):
>       _process: asyncio.subprocess.Process
E       AttributeError: module 'pytest_subprocess.asyncio_subprocess' has no attribute 'Process'

Notably this only occurs if nothing else has caused anyio._backends._asyncio to be imported yet and can be worked around with a fixture like this (or by otherwise running the equivalent code before the fake_process fixture runs):

@pytest.fixture
def init_anyio():
    async def impl():
        anyio.get_cancelled_exc_class()

    asyncio.get_event_loop().run_until_complete(impl())

FWIW the example test case is not representative of the actual test we ran into it with, but just something that reproduces the problem. In the real test we're testing a FastAPI-based server to see if it invokes processes when it should, and FastAPI is based on anyio.

sdist is mising tests

The sdist package at PyPI is missing tests. Please add missing tests to sdist to make downstream testing easier. Thank you.

The `Popen` patching is a little fragile

If you use from multiprocess import Popen the library is not able to swap the implementation, I don't know if it's an intended behavior, maybe should be explicitly indicated in the documentation.

Minimal reproducible example: imagine you have the module benchmarking.py with the following function:

from subprocess import Popen as imported_Popen

def meow(cmdline):
    return imported_Popen(cmdline, stdout=subprocess.PIPE)

and then you have a separate file with your test

from benchmarking import meow

def test_echo_null_byte(fp):
    fp.register(["echo", "-ne", "\x00"], stdout=bytes.fromhex("00"))

    process = meow(["echo", "-ne", "\x00"])
    out, _ = process.communicate()

    assert process.returncode == 0
    assert out == b"\x00"

then the test it's gonna fail because (probably) at the moment the test is run it's too late to swap the implementation.

1.4.1: build documentation issues

  • Looks like conf..py still uses outdated sphinxcontrib.napoleon module.
    Here is the patch whcih fixes that
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -38,7 +38,7 @@
 # ones.

 extensions = [
-    "sphinxcontrib.napoleon",
+    "sphinx.ext.napoleon",
     "sphinx.ext.autodoc",
     "sphinx_autodoc_typehints",
 ]
  • On tryinf to buidl documentation lookjs like sphinx is not able to find pytest_subprocess module
+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v5.1.1
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'core.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
WARNING: autodoc: failed to import class 'utils.Any' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } done
build succeeded, 2 warnings.

Here is the patch which fixes that

--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,9 +8,10 @@
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath(".."))
+
 import datetime
 from pathlib import Path

This patch sdoes wjat is descrived in lines above modifird lines.

  • With both patches sphos still shows 3 warnings
+ /usr/bin/sphinx-build -n -T -b man docs build/sphinx/man
Running Sphinx v5.1.1
WARNING:root:Generated changelog file to history.rst
making output directory... done
building [mo]: targets for 0 po files that are out of date
building [man]: all manpages
updating environment: [new config] 4 added, 0 changed, 0 removed
reading sources... [100%] usage
WARNING: autodoc: failed to import class 'core.FakeProcess' from module 'pytest_subprocess'; the following exception was raised:
No module named 'pytest_subprocess.core'
looking for now-outdated files... none found
pickling environment... done
checking consistency... done
writing... python-pytest-subprocess.3 { usage api history } /home/tkloczko/rpmbuild/BUILD/pytest-subprocess-1.4.1/pytest_subprocess/utils.py:docstring of pytest_subprocess.utils.Any:4: WARNING: py:data reference target not found: typing.Optional
/home/tkloczko/rpmbuild/BUILD/pytest-subprocess-1.4.1/pytest_subprocess/utils.py:docstring of pytest_subprocess.utils.Any:6: WARNING: py:data reference target not found: typing.Optional
done
build succeeded, 3 warnings.

In case sphinx warnings reference target not found you can peak on fixes that kind of issues in other projects
latchset/jwcrypto#289
click-contrib/sphinx-click@abc31069
latchset/jwcrypto#289
RDFLib/rdflib-sqlalchemy#95
sissaschool/elementpath@bf869d9e
jaraco/cssutils#21
pywbem/pywbem#2895
sissaschool/xmlschema@42ea98f2
RDFLib/rdflib#2036
frostming/unearth#14

Please let me know if you want above patches as PRs.

handling binary outputs

Looks like the outputs are assumed to be strings and pytest-subprocess is appending line separators.

    def test_binary_output(fake_process):
        fake_process.register_subprocess(
            ["cat", "/tmp/foo"], stdout=bytes.fromhex("000102")
        )
    
        proc = subprocess.run(["cat", "/tmp/foo"], capture_output=True)
    
>       assert proc.stdout == b"\x00\x01\x02"
E       AssertionError: assert b'\x00\x01\x02\n' == b'\x00\x01\x02'
E         Full diff:
E         - b'\x00\x01\x02'
E         + b'\x00\x01\x02\n'
E         ?               ++

This should check for text mode: https://github.com/aklajnert/pytest-subprocess/blob/master/pytest_subprocess/core.py#L146

allow_unregistered(True) in one test case affects other test cases

Today I discovered that fake_process.allow_unregistered(True) in one test case applies to / affects all following test cases.

Here's a minimal reproducible example:

import subprocess

import pytest
import pytest_subprocess


def test_1(fake_process):
    with pytest.raises(pytest_subprocess.ProcessNotRegisteredError):
        subprocess.run(["ls"])


def test_2(fake_process):
    fake_process.allow_unregistered(True)


def test_3(fake_process):
    with pytest.raises(pytest_subprocess.ProcessNotRegisteredError):
        subprocess.run(["ls"])

Note test_1 and test_3 are exactly the same. Unfortunately when run one after another test_1 passes, while test_3 fails.

Tested on Python 3.8.10, pytest 6.2.4 and pytest_subprocess 1.1.1.

AssertionError if stderr == subprocess.STDOUT and stdout is a file

Description

An AssertionError is raised at fake_popen.py:188when calling with argument stderr=subprocess.STDOUT if stdout is a file (stdout is expected to be None:

>           assert self.stdout is not None
E           AssertionError

pyvenv\Lib\site-packages\pytest_subprocess\fake_popen.py:188: AssertionError

subprocess.Popen handles this correctly; stderr is simply written to the file at stdout.

Example

Below is an example demonstrating the error. I have GIT_TRACE set so that git rev-parse gives a small amount of stdout and stderr:

$ git rev-parse HEAD
06:05:28.502294 exec-cmd.c:244          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin
06:05:28.537291 git.c:463               trace: built-in: git rev-parse HEAD
47c77698dd8e0e35af00da56806fe98fcb9a1056

I define a function that runs the subprocess (so that I can call the same thing, using either the fake or real Popen:

def run_git_revparse():
    import os
    os.environ['GIT_TRACE'] = '1'
    with NamedTemporaryFile("w+b", delete=False) as f:
        path = pathlib.Path(f.name)
        # NOTE: I pass f.file because for some reason:
        #           isinstance(f, io.BufferedWriter) == False
        #       so pytest_subprocess isinstance checks would fail.  I am
        #       hesitant to change those tests, though you could simply check
        #       for a few key methods:
        #           hasattr(f.mode)
        #           isinstance(f.mode, str)
        #           hasattr(f.write)
        #           isinstance(f.write, types.FunctionType)
        p = subprocess.Popen(('git', 'rev-parse', 'HEAD'), stdout=f.file, stderr=subprocess.STDOUT)
        f.close()
        assert p.wait() == 0
        assert path.exists()
        output = path.read_text()

        # print to sys.stdout so I can see with `pytest --capture=no`
        print('-'*50)
        print(output)
        print('-'*50)

        output = output.splitlines()
        assert len(output) == 3

        # account for stderr coming before or after stdout
        if 'exec-cmd.c' in output[1]:
            output[2], output[0], output[1] = output
        # first two lines should be stderr
        assert 'exec-cmd.c' in output[0]
        assert 'git.c' in output[1]
        # last line should be a hex string
        assert len(set(output[2]) - set('0123456789abcdef')) == 0

Then define my test function, which first runs the real git, then the fake "git":

def test_fp(fp):
    print() # for --capture=no
    fp.allow_unregistered(True)
    run_git_revparse()
    fp.register(
        ["git", 'rev-parse', 'HEAD'],
        stdout=["47c77698dd8e0e35af00da56806fe98fcb9a1056"],
        stderr=[
            '05:17:17.982923 exec-cmd.c:244          trace: resolved executable dir: C:/Program Files/Git/mingw64/bin',
            '05:17:18.000760 git.c:463               trace: built-in: git rev-parse HEAD'
            ])
    run_git_revparse()

This raises the assertion error. I can correct this:

--- fake_popen.py   2024-02-10 06:09:01.897909800 -0500
+++ fake_popen.py       2024-02-10 06:08:49.610943500 -0500
@@ -181,22 +181,16 @@
         stdout = kwargs.get("stdout")
         if stdout == subprocess.PIPE:
             self.stdout = self._prepare_buffer(self.__stdout)
-        elif self.__is_io_writer(stdout):
+        elif isinstance(stdout, (io.BufferedWriter, io.TextIOWrapper)):
             self._write_to_buffer(self.__stdout, stdout)
         stderr = kwargs.get("stderr")
         if stderr == subprocess.STDOUT and self.__stderr:
-            if self.__is_io_writer(stdout):
-                self._write_to_buffer(self.__stderr, stdout)
-            else:
-                assert self.stdout is not None
-                self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
+            assert self.stdout is not None
+            self.stdout = self._prepare_buffer(self.__stderr, self.stdout)
         elif stderr == subprocess.PIPE:
             self.stderr = self._prepare_buffer(self.__stderr)
-        elif self.__is_io_writer(stderr):
+        elif isinstance(stderr, (io.BufferedWriter, io.TextIOWrapper)):
             self._write_to_buffer(self.__stderr, stderr)
-
-    def __is_io_writer(self, o):
-        return isinstance(o, (io.BufferedWriter, io.TextIOWrapper, io.BufferedRandom))

     def _prepare_buffer(
         self,

Note:

  • I pulled out the isinstance testing.
  • I added io.BufferedRandom to the group, as that is what I had, and I would expect it should be included...my test still won't work without it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.