pax_global_header00006660000000000000000000000064147362311470014522gustar00rootroot0000000000000052 comment=566a3d2a649bcf7a7e647808947c86b3cad6e0a6 pytest-subprocess-1.5.3/000077500000000000000000000000001473623114700152465ustar00rootroot00000000000000pytest-subprocess-1.5.3/.coveragerc000066400000000000000000000010241473623114700173640ustar00rootroot00000000000000[run] source = pytest_subprocess [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError raise PluginInternalError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: ignore_errors = True pytest-subprocess-1.5.3/.editorconfig000066400000000000000000000010311473623114700177160ustar00rootroot00000000000000# http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{py, rst, ini}] indent_style = space indent_size = 4 [*.py] max_line_length = 89 known_first_party = docker_manager multi_line_output = 3 default_section = THIRDPARTY [*.{html, css, scss, json, yml, ts}] indent_style = space indent_size = 2 [*.md] trim_trailing_whitespace = false [*.rst] max_line_length = 78 [Makefile] indent_style = tab [nginx.conf] indent_style = space indent_size = 2 pytest-subprocess-1.5.3/.github/000077500000000000000000000000001473623114700166065ustar00rootroot00000000000000pytest-subprocess-1.5.3/.github/workflows/000077500000000000000000000000001473623114700206435ustar00rootroot00000000000000pytest-subprocess-1.5.3/.github/workflows/ci.yml000066400000000000000000000030021473623114700217540ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: pytest-subprocess-ci on: push: branches: - master pull_request: branches: - master jobs: test: runs-on: ${{ matrix.platform }} strategy: max-parallel: 4 matrix: platform: [ ubuntu-latest, macos-latest, windows-latest ] python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.8'] steps: - uses: actions/checkout@v3 - name: Setup Nox uses: wntrblm/nox@2024.10.09 with: python-versions: ${{ matrix.python-version }} - name: Run tests run: | nox -s tests-${{ matrix.python-version }} env: PLATFORM: ${{ matrix.platform }} mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Nox uses: wntrblm/nox@2024.10.09 - name: Run mypy run: | nox -s mypy flake8: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Nox uses: wntrblm/nox@2024.10.09 - name: Run flake8 run: | nox -s flake8 check-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Nox uses: wntrblm/nox@2024.10.09 - name: Build docs run: | nox -s docs pytest-subprocess-1.5.3/.gitignore000066400000000000000000000016421473623114700172410ustar00rootroot00000000000000.idea # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg venv* # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ .pytest_cache .mypy_cache typed-src # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask instance folder instance/ # Sphinx documentation docs/_build/ # MkDocs documentation /site/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version pytest-subprocess-1.5.3/.pre-commit-config.yaml000066400000000000000000000006641473623114700215350ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black - repo: https://github.com/wimglenn/reorder-python-imports-black rev: v3.12.0 hooks: - id: reorder-python-imports - repo: https://github.com/asottile/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [black==22.10.0] args: [--line-length=74, docs/index.rst, README.rst] pytest-subprocess-1.5.3/.readthedocs.yml000066400000000000000000000003051473623114700203320ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py build: os: ubuntu-22.04 tools: python: "3.9" python: install: - method: pip path: . extra_requirements: - docs pytest-subprocess-1.5.3/HISTORY.rst000066400000000000000000000212431473623114700171430ustar00rootroot00000000000000History ======= 1.5.3 (2025-01-04) ------------------ Features ~~~~~~~~ * `#171 `_, `#178 `_: Allow to access keyword arguments passed to Popen. Bug fixes ~~~~~~~~~ * `#180 `_: Fixed an incorrect wait timeout calculation. * `#170 `_: Wrapped ProcessDispatcher.dispatch into FakePopenWrapper as it was causing TypeError when Popen is used as a type. * `#169 `_: Get rid of using thread in AsyncFakePopen as it causes thread.join() to hang indefinitely. 1.5.2 (2024-07-24) ------------------ Bug fixes ~~~~~~~~~ * `#162 `_: Include tests (and docs) and sdist correctly, and stop installing them to site-packages. Other changes ~~~~~~~~~~~~~ * `#163 `_: Add support for Python 3.12. 1.5.1 (2024-07-23) ------------------ Other changes ~~~~~~~~~~~~~ * `#160 `_: Changed pytest entrypoint to avoid error while loading plugin with `-p` argument. * `#128 `_: Add `tests` directory to sdist. 1.5.0 (2023-01-28) ------------------ Features ~~~~~~~~ * `#109 `_: Match also `os.PathLike`. * `#105 `_: Add program matcher. Other changes ~~~~~~~~~~~~~ * `#110 `_: Produce TypeError on Win Py<3.8 for Path args. 1.4.2 (2022-10-02) ------------------ Features ~~~~~~~~ * `#87 `_: Add support for Python 3.11. * `#80 `_, `#86 `_: The `register()` method returns an auxiliary object that will contain all matching `FakePopen` instances. Bug fixes ~~~~~~~~~ * `#93 `_: Raise callback exceptions on `communicate()` calls. Other changes ~~~~~~~~~~~~~ * `#97 `_: Fixed warnings in tests, treat warnings as errors. * `#91 `_: Use `sys.executable` instead just `"python"` in tests while invoking python subprocess. * `#90 `_: Fix documentation build, add CI check for it. 1.4.1 (2022-02-09) ------------------ Other changes ~~~~~~~~~~~~~ * `#74 `_: Add ``fp`` alias for the fixture, and ``register`` for the ``regisiter_subprocess``. 1.4.0 (2022-01-23) ------------------ Features ~~~~~~~~ * `#71 `_: Add support for stdin with asyncio. Bug fixes ~~~~~~~~~ * `#68 `_: Make `stdout` and `stderr` an `asyncio.StreamReader` instance when using asyncio functions. * `#63 `_, `#67 `_: Add missing items to `asyncio.subprocess`. Other changes ~~~~~~~~~~~~~ * `#69 `_: Extracted code into separate files to improve navigation. 1.3.2 (2021-11-07) ------------------ Bug fixes ~~~~~~~~~ * `#61 `_: Fixed behavior of ``asyncio.create_subproess_exec()``. 1.3.1 (2021-11-01) ------------------ Bug fixes ~~~~~~~~~ * `#58 `_: Correctly handle file stream output. 1.3.0 (2021-10-24) ------------------ Features ~~~~~~~~ * `#55 `_: Add support for ``terminate()``, ``kill()``, ``send_signal()``. 1.2.0 (2021-10-09) ------------------ Features ~~~~~~~~ * `#49 `_, `#52 `_: Add support for ``asyncio``. Other changes ~~~~~~~~~~~~~ * `#50 `_: Change docs theme. 1.1.2 (2021-07-17) ------------------ Bug fixes ~~~~~~~~~ * `#47 `_: Prevent `allow_unregistered()` and `keep_last_process()` from affecting other tests. 1.1.1 (2021-06-18) ------------------ Bug fixes ~~~~~~~~~ * `#43 `_: Wait for callback thread to finish when calling ``communicate()``. Other changes ~~~~~~~~~~~~~ * `#42 `_: Fix type annotations for `register_subprocess()`. 1.1.0 (2021-04-18) ------------------ Bug fixes ~~~~~~~~~ * `#37 `_: Preserve original command in `proc.args` to prevent leaking the internal `Command` type. Other changes ~~~~~~~~~~~~~ * `#38 `_: Switched CI from Azure Pipelines to GitHub Actions. * `#35 `_: Drop support for python 3.4 and 3.5. Move type annotations from `.pyi` files into sources. 1.0.1 (2021-03-20) ------------------ Bug fixes ~~~~~~~~~ * `#34 `_: Prevent appending newlines to outputs unless defined as list/tuple. Other changes ~~~~~~~~~~~~~ * `#32 `_: Make the ``Command`` class iterable. 1.0.0 (2020-08-22) ------------------ Features ~~~~~~~~ * `#29 `_: Remember subprocess calls to check if expected commands were executed. * `#28 `_: Allow to match a command with variable arguments (non-exact matching). 0.1.5 (2020-06-19) ------------------ Bug fixes ~~~~~~~~~ * `#26 `_: `encoding` and `errors` arguments will properly trigger `text` mode. 0.1.4 (2020-04-28) ------------------ Bug fixes ~~~~~~~~~ * `#22 `_: The `returncode` will not be ignored when `callback` is used. * `#21 `_: The exception raised from callback will take precedence over those from subprocess. * `#20 `_: Registering process will be now consistent regardless of the command type. * `#19 `_: Fixed crash for stderr redirect with an empty stream definition. 0.1.3 (2020-03-04) ------------------ Features ~~~~~~~~ * `#13 `_: Allow passing keyword arguments into callbacks. Bug fixes ~~~~~~~~~ * `#12 `_: Properly raise exceptions from callback functions. Documentation changes ~~~~~~~~~~~~~~~~~~~~~ * `#15 `_: Add documentation chapter about the callback functions. 0.1.2 (2020-01-17) ------------------ Features ~~~~~~~~ * `#3 `_: Add basic support for process input. Bug fixes ~~~~~~~~~ * `#5 `_: Make ``wait()`` method to raise ``TimeoutError`` after the desired time will elapse. Documentation changes ~~~~~~~~~~~~~~~~~~~~~ * `#7 `_, `#8 `_, `#9 `_: Create Sphinx documentation. Other changes ~~~~~~~~~~~~~ * `#10 `_: Switch from ``tox`` to ``nox`` for running tests and tasks. * `#4 `_: Add classifier for Python 3.9. Update CI config to test also on that interpreter version. 0.1.1 (2019-11-24) ------------------ Other changes ~~~~~~~~~~~~~ * `#1 `_, `#2 `_: Enable support for Python 3.4, add CI tests for that version. 0.1.0 (2019-11-23) ------------------ Initial release pytest-subprocess-1.5.3/LICENSE000066400000000000000000000020741473623114700162560ustar00rootroot00000000000000 The MIT License (MIT) Copyright (c) 2019 Andrzej Klajnert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-subprocess-1.5.3/MANIFEST.in000066400000000000000000000002771473623114700170120ustar00rootroot00000000000000include LICENSE include README.rst include HISTORY.rst include pytest.ini recursive-include docs * recursive-include tests *.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] pytest-subprocess-1.5.3/README.rst000066400000000000000000000377661473623114700167600ustar00rootroot00000000000000 pytest-subprocess ================= .. image:: https://img.shields.io/pypi/v/pytest-subprocess.svg :target: https://pypi.org/project/pytest-subprocess :alt: PyPI version .. image:: https://img.shields.io/pypi/pyversions/pytest-subprocess.svg :target: https://pypi.org/project/pytest-subprocess :alt: Python versions .. image:: https://readthedocs.org/projects/pytest-subprocess/badge/?version=latest :target: https://pytest-subprocess.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status Pytest plugin to fake subprocess. .. contents:: :local: .. include-start Usage ===== The plugin adds the ``fake_process`` fixture (and ``fp`` as an alias). It can be used it to register subprocess results so you won't need to rely on the real processes. The plugin hooks on the ``subprocess.Popen()``, which is the base for other subprocess functions. That makes the ``subprocess.run()``, ``subprocess.call()``, ``subprocess.check_call()`` and ``subprocess.check_output()`` methods also functional. Installation ------------ You can install ``pytest-subprocess`` via `pip`_ from `PyPI`_:: $ pip install pytest-subprocess Basic usage ----------- The most important method is ``fp.register()`` (or ``register_subprocess`` if you prefer to be more verbose), which allows defining the fake processes behavior. .. code-block:: python def test_echo_null_byte(fp): fp.register(["echo", "-ne", "\x00"], stdout=bytes.fromhex("00")) process = subprocess.Popen( ["echo", "-ne", "\x00"], stdout=subprocess.PIPE, ) out, _ = process.communicate() assert process.returncode == 0 assert out == b"\x00" Optionally, the ``stdout`` and ``stderr`` parameters can be a list (or tuple) of lines to be joined together with a trailing ``os.linesep`` on each line. .. code-block:: python def test_git(fp): fp.register(["git", "branch"], stdout=["* fake_branch", " master"]) process = subprocess.Popen( ["git", "branch"], stdout=subprocess.PIPE, universal_newlines=True, ) out, _ = process.communicate() assert process.returncode == 0 assert out == "* fake_branch\n master\n" Passing input ------------- By default, if you use ``input`` argument to the ``Popen.communicate()`` method, it won't crash, but also won't do anything useful. By passing a function as ``stdin_callable`` argument for the ``fp.register()`` method you can specify the behavior based on the input. The function shall accept one argument, which will be the input data. If the function will return a dictionary with ``stdout`` or ``stderr`` keys, its value will be appended to according stream. .. code-block:: python def test_pass_input(fp): def stdin_function(input): return { "stdout": "This input was added: {data}".format( data=input.decode() ) } fp.register( ["command"], stdout=[b"Just stdout"], stdin_callable=stdin_function, ) process = subprocess.Popen( ["command"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) out, _ = process.communicate(input=b"sample input\n") assert out.splitlines() == [ b"Just stdout", b"This input was added: sample input", ] Unregistered commands --------------------- By default, when the ``fp`` fixture is being used, any attempt to run subprocess that has not been registered will raise the ``ProcessNotRegisteredError`` exception. To allow it, use ``fp.allow_unregistered(True)``, which will execute all unregistered processes with real ``subprocess``, or use ``fp.pass_command("command")`` to allow just a single command. .. code-block:: python def test_real_process(fp): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # this will fail, as "ls" command is not registered subprocess.call("ls") fp.pass_command("ls") # now it should be fine assert subprocess.call("ls") == 0 # allow all commands to be called by real subprocess fp.allow_unregistered(True) assert subprocess.call(["ls", "-l"]) == 0 Differing results ----------------- Each ``register()`` or ``pass_command()`` method call will register only one command execution. You can call those methods multiple times, to change the faked output on each subprocess run. When you call subprocess more will be raised. To prevent that, call ``fp.keep_last_process(True)``, which will keep the last registered process forever. .. code-block:: python def test_different_output(fp): # register process with output changing each execution fp.register("test", stdout="first execution") # the second execution will return non-zero exit code fp.register("test", stdout="second execution", returncode=1) assert subprocess.check_output("test") == b"first execution" second_process = subprocess.run("test", stdout=subprocess.PIPE) assert second_process.stdout == b"second execution" assert second_process.returncode == 1 # 3rd time shall raise an exception with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call("test") # now, register two processes once again, # but the last one will be kept forever fp.register("test", stdout="first execution") fp.register("test", stdout="second execution") fp.keep_last_process(True) # now the processes can be called forever assert subprocess.check_output("test") == b"first execution" assert subprocess.check_output("test") == b"second execution" assert subprocess.check_output("test") == b"second execution" assert subprocess.check_output("test") == b"second execution" Using callbacks --------------- You can pass a function as ``callback`` argument to the ``register()`` method which will be executed instead of the real subprocess. The callback function can raise exceptions which will be interpreted in tests as an exception raised by the subprocess. The fixture will pass ``FakePopen`` class instance into the callback function, that can be used to change the return code or modify output streams. .. code-block:: python def callback_function(process): process.returncode = 1 raise PermissionError("exception raised by subprocess") def test_raise_exception(fp): fp.register(["test"], callback=callback_function) with pytest.raises( PermissionError, match="exception raised by subprocess" ): process = subprocess.Popen(["test"]) process.wait() assert process.returncode == 1 It is possible to pass additional keyword arguments into ``callback`` by using the ``callback_kwargs`` argument: .. code-block:: python def callback_function_with_kwargs(process, return_code): process.returncode = return_code def test_callback_with_arguments(fp): return_code = 127 fp.register( ["test"], callback=callback_function_with_kwargs, callback_kwargs={"return_code": return_code}, ) process = subprocess.Popen(["test"]) process.wait() assert process.returncode == return_code As a context manager -------------------- The ``fp`` fixture provides ``context()`` method that allows us to use it as a context manager. It can be used to limit the scope when a certain command is allowed, e.g. to make sure that the code doesn't want to execute it somewhere else. .. code-block:: python def test_context_manager(fp): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # command not registered, so will raise an exception subprocess.check_call("test") with fp.context() as nested_process: nested_process.register("test", occurrences=3) # now, we can call the command 3 times without error assert subprocess.check_call("test") == 0 assert subprocess.check_call("test") == 0 # the command was called 2 times, so one occurrence left, but since the # context manager has been left, it is not registered anymore with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call("test") Non-exact command matching -------------------------- If you need to catch a command with some non-predictable elements, like a path to a randomly-generated file name, you can use ``fake_subprocess.any()`` for that purpose. The number of arguments that should be matched can be controlled by ``min`` and ``max`` arguments. To use ``fake_subprocess.any()`` you need to define the command as a ``tuple`` or ``list``. The matching will work even if the subprocess command will be called with a string argument. .. code-block:: python def test_non_exact_matching(fp): # define a command that will take any number of arguments fp.register(["ls", fp.any()]) assert subprocess.check_call("ls -lah") == 0 # `fake_subprocess.any()` is OK even with no arguments fp.register(["ls", fp.any()]) assert subprocess.check_call("ls") == 0 # but it can force a minimum amount of arguments fp.register(["cp", fp.any(min=2)]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # only one argument is used, so registered command won't match subprocess.check_call("cp /source/dir") # but two arguments will be fine assert subprocess.check_call("cp /source/dir /tmp/random-dir") == 0 # the `max` argument can be used to limit maximum amount of arguments fp.register(["cd", fp.any(max=1)]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # cd with two arguments won't match with max=1 subprocess.check_call("cd ~/ /tmp") # but any single argument is fine assert subprocess.check_call("cd ~/") == 0 # `min` and `max` can be used together fp.register(["my_app", fp.any(min=1, max=2)]) assert subprocess.check_call(["my_app", "--help"]) == 0 You can also specify just the command name, and have it match any command with the same name, regardless of the location. This is accomplished with ``fake_subprocess.program("name")``. .. code-block:: python def test_any_matching_program(fp): # define a command that can come from anywhere fp.register([fp.program("ls")]) assert subprocess.check_call("/bin/ls") == 0 Check if process was called --------------------------- You may want to simply check if a certain command was called, you can do this by accessing ``fp.calls``, where all commands are stored as-called. You can also use a utility function ``fp.call_count()`` to see how many a command has been called. The latter supports ``fp.any()``. .. code-block:: python def test_check_if_called(fp): fp.keep_last_process(True) # any command can be called fp.register([fp.any()]) subprocess.check_call(["cp", "/tmp/source", "/source"]) subprocess.check_call(["cp", "/source", "/destination"]) subprocess.check_call(["cp", "/source", "/other/destination"]) # you can check if command is in ``fp.calls`` assert ["cp", "/tmp/source", "/source"] in fp.calls assert ["cp", "/source", "/destination"] in fp.calls assert ["cp", "/source", "/other/destination"] in fp.calls # or check how many it was called, possibly with wildcard arguments assert fp.call_count(["cp", "/source", "/destination"]) == 1 # with ``call_count()`` you don't need to use the same type as # the subprocess was called assert fp.call_count("cp /tmp/source /source") == 1 # can be used with ``fp.any()`` to match more calls assert fp.call_count(["cp", fp.any()]) == 3 Check Popen arguments --------------------- You can use the recorded calls functionality to introspect the keyword arguments that were passed to `Popen`. .. code-block:: python def test_process_recorder_kwargs(fp): fp.keep_last_process(True) recorder = fp.register(["test_script", fp.any()]) subprocess.run( ("test_script", "arg1"), env={"foo": "bar"}, cwd="/home/user" ) subprocess.Popen( ["test_script", "arg2"], env={"foo": "bar1"}, executable="test_script", shell=True, ) assert recorder.calls[0].args == ("test_script", "arg1") assert recorder.calls[0].kwargs == { "cwd": "/home/user", "env": {"foo": "bar"}, } assert recorder.calls[1].args == ["test_script", "arg2"] assert recorder.calls[1].kwargs == { "env": {"foo": "bar1"}, "executable": "test_script", "shell": True, } Handling signals ---------------- You can use standard ``kill()``, ``terminate()`` or ``send_signal()`` methods in ``Popen`` instances. There is an additional ``received_signals()`` method to get a tuple of all signals received by the process. It is also possible to set up an optional callback function for signals. .. code-block:: python import signal def test_signal_callback(fp): """Test that signal callbacks work.""" def callback(process, sig): if sig == signal.SIGTERM: process.returncode = -1 # the `register()` method returns a ProgressRecorder object, where # all future matching `Popen()` instances will be appended process_recorder = fp.register("test", signal_callback=callback) process = subprocess.Popen("test") process.send_signal(signal.SIGTERM) process.wait() assert process.returncode == -1 assert process.received_signals() == (signal.SIGTERM,) # the instance appended to `register()` output is the `Popen` instance # created later assert process_recorder.first_call is process Asyncio support --------------- The plugin now supports asyncio and works for ``asyncio.create_subprocess_shell`` and ``asyncio.create_subprocess_exec``: .. code-block:: python @pytest.mark.asyncio async def test_basic_usage( fp, ): fp.register( ["some-command-that-is-definitely-unavailable"], returncode=500 ) process = await asyncio.create_subprocess_shell( "some-command-that-is-definitely-unavailable" ) returncode = await process.wait() assert process.returncode == returncode assert process.returncode == 500 .. _`pip`: https://pypi.org/project/pip/ .. _`PyPI`: https://pypi.org/project .. include-end Documentation ------------- For full documentation, including API reference, please see https://pytest-subprocess.readthedocs.io/en/latest/. Contributing ------------ Contributions are very welcome. Tests can be run with `tox`_, please ensure the coverage at least stays the same before you submit a pull request. License ------- Distributed under the terms of the `MIT`_ license, "pytest-subprocess" is free and open source software Issues ------ If you encounter any problems, please `file an issue`_ along with a detailed description. ---- This `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`_'s `cookiecutter-pytest-plugin`_ template. .. _`Cookiecutter`: https://github.com/audreyr/cookiecutter .. _`@hackebrot`: https://github.com/hackebrot .. _`MIT`: http://opensource.org/licenses/MIT .. _`BSD-3`: http://opensource.org/licenses/BSD-3-Clause .. _`GNU GPL v3.0`: http://www.gnu.org/licenses/gpl-3.0.txt .. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0 .. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin .. _`file an issue`: https://github.com/aklajnert/pytest-subprocess/issues .. _`pytest`: https://github.com/pytest-dev/pytest .. _`tox`: https://tox.readthedocs.io/en/latest/ pytest-subprocess-1.5.3/changelog.d/000077500000000000000000000000001473623114700174175ustar00rootroot00000000000000pytest-subprocess-1.5.3/changelog.d/config.yaml000066400000000000000000000010301473623114700215420ustar00rootroot00000000000000context: prs_url: https://github.com/aklajnert/pytest-subprocess/pull message_types: - name: feature title: Features - name: bug title: Bug fixes - name: doc title: Documentation changes - name: deprecation title: Deprecations - name: other title: Other changes entry_fields: - name: pr_ids verbose_name: Pull request ID type: str required: false multiple: true - name: message verbose_name: Changelog message type: str required: true output_file: ../HISTORY.rst partial_release_name: unreleased user_data: [] pytest-subprocess-1.5.3/changelog.d/releases/000077500000000000000000000000001473623114700212225ustar00rootroot00000000000000pytest-subprocess-1.5.3/changelog.d/releases/.gitkeep000066400000000000000000000000001473623114700226410ustar00rootroot00000000000000pytest-subprocess-1.5.3/changelog.d/releases/0.0.1.0.yaml000066400000000000000000000001721473623114700227000ustar00rootroot00000000000000entries: {} previous_release: null release_date: '2019-11-23' release_description: Initial release release_version: 0.1.0 pytest-subprocess-1.5.3/changelog.d/releases/1.0.1.1.yaml000066400000000000000000000003371473623114700227050ustar00rootroot00000000000000entries: other: - message: Enable support for Python 3.4, add CI tests for that version. pr_ids: - '1' - '2' previous_release: 0.1.0 release_date: '2019-11-24' release_description: '' release_version: 0.1.1 pytest-subprocess-1.5.3/changelog.d/releases/10.1.1.2.yaml000066400000000000000000000004131473623114700227620ustar00rootroot00000000000000entries: bug: - message: Prevent `allow_unregistered()` and `keep_last_process()` from affecting other tests. pr_ids: - '47' timestamp: 1626522235 previous_release: 1.1.1 release_date: '2021-07-17' release_description: '' release_version: 1.1.2 pytest-subprocess-1.5.3/changelog.d/releases/11.1.2.0.yaml000066400000000000000000000004661473623114700227720ustar00rootroot00000000000000entries: feature: - message: Add support for ``asyncio``. pr_ids: - '49' - '52' timestamp: 1632653299 other: - message: Change docs theme. pr_ids: - '50' timestamp: 1633768022 previous_release: 1.1.2 release_date: '2021-10-09' release_description: '' release_version: 1.2.0 pytest-subprocess-1.5.3/changelog.d/releases/12.1.3.0.yaml000066400000000000000000000003641473623114700227710ustar00rootroot00000000000000entries: feature: - message: Add support for ``terminate()``, ``kill()``, ``send_signal()``. pr_ids: - '55' timestamp: 1635002553 previous_release: 1.2.0 release_date: '2021-10-24' release_description: '' release_version: 1.3.0 pytest-subprocess-1.5.3/changelog.d/releases/13.1.3.1.yaml000066400000000000000000000003251473623114700227700ustar00rootroot00000000000000entries: bug: - message: Correctly handle file stream output. pr_ids: - '58' timestamp: 1635705281 previous_release: 1.3.0 release_date: '2021-11-01' release_description: '' release_version: 1.3.1 pytest-subprocess-1.5.3/changelog.d/releases/14.1.3.2.yaml000066400000000000000000000003471473623114700227760ustar00rootroot00000000000000entries: bug: - message: Fixed behavior of ``asyncio.create_subproess_exec()``. pr_ids: - '61' timestamp: 1636289056 previous_release: 1.3.1 release_date: '2021-11-07' release_description: '' release_version: 1.3.2 pytest-subprocess-1.5.3/changelog.d/releases/15.1.4.0.yaml000066400000000000000000000011641473623114700227740ustar00rootroot00000000000000entries: bug: - message: Make `stdout` and `stderr` an `asyncio.StreamReader` instance when using asyncio functions. pr_ids: - '68' timestamp: 1641746962 - message: Add missing items to `asyncio.subprocess`. pr_ids: - '63' - '67' timestamp: 1639330523 feature: - message: Add support for stdin with asyncio. pr_ids: - '71' timestamp: 1642946529 other: - message: Extracted code into separate files to improve navigation. pr_ids: - '69' timestamp: 1642333772 previous_release: 1.3.2 release_date: '2022-01-23' release_description: '' release_version: 1.4.0 pytest-subprocess-1.5.3/changelog.d/releases/16.1.4.1.yaml000066400000000000000000000004071473623114700227750ustar00rootroot00000000000000entries: other: - message: Add ``fp`` alias for the fixture, and ``register`` for the ``regisiter_subprocess``. pr_ids: - '74' timestamp: 1644310128 previous_release: 1.4.0 release_date: '2022-02-09' release_description: '' release_version: 1.4.1 pytest-subprocess-1.5.3/changelog.d/releases/17.1.4.2.yaml000066400000000000000000000016051473623114700230000ustar00rootroot00000000000000entries: bug: - message: Raise callback exceptions on `communicate()` calls. pr_ids: - '93' timestamp: 1662136001 feature: - message: Add support for Python 3.11. pr_ids: - '87' timestamp: 1659440672 - message: The `register()` method returns an auxiliary object that will contain all matching `FakePopen` instances. pr_ids: - '80' - '86' timestamp: 1651409374 other: - message: Fixed warnings in tests, treat warnings as errors. pr_ids: - '97' timestamp: 1664691594 - message: Use `sys.executable` instead just `"python"` in tests while invoking python subprocess. pr_ids: - '91' timestamp: 1661605450 - message: Fix documentation build, add CI check for it. pr_ids: - '90' timestamp: 1661604388 previous_release: 1.4.1 release_date: '2022-10-02' release_description: '' release_version: 1.4.2 pytest-subprocess-1.5.3/changelog.d/releases/18.1.5.0.yaml000066400000000000000000000006321473623114700227770ustar00rootroot00000000000000entries: feature: - message: Match also `os.PathLike`. pr_ids: - '109' timestamp: 1674900284 - message: Add program matcher. pr_ids: - '105' timestamp: 1674900223 other: - message: Produce TypeError on Win Py<3.8 for Path args. pr_ids: - '110' timestamp: 1674900321 previous_release: 1.4.2 release_date: '2023-01-28' release_description: '' release_version: 1.5.0 pytest-subprocess-1.5.3/changelog.d/releases/19.1.5.1.yaml000066400000000000000000000005721473623114700230040ustar00rootroot00000000000000entries: other: - message: Changed pytest entrypoint to avoid error while loading plugin with `-p` argument. pr_ids: - '160' timestamp: 1721758426 type: other - message: Add `tests` directory to sdist. pr_ids: - '128' timestamp: 1693138907 previous_release: 1.5.0 release_date: '2024-07-23' release_description: '' release_version: 1.5.1 pytest-subprocess-1.5.3/changelog.d/releases/2.0.1.2.yaml000066400000000000000000000012151473623114700227030ustar00rootroot00000000000000entries: bug: - message: Make ``wait()`` method to raise ``TimeoutError`` after the desired time will elapse. pr_ids: - '5' doc: - message: Create Sphinx documentation. pr_ids: - '7' - '8' - '9' feature: - message: Add basic support for process input. pr_ids: - '3' other: - message: ' Switch from ``tox`` to ``nox`` for running tests and tasks.' pr_ids: - '10' - message: Add classifier for Python 3.9. Update CI config to test also on that interpreter version. pr_ids: - '4' previous_release: 0.1.1 release_date: '2020-01-17' release_description: '' release_version: 0.1.2 pytest-subprocess-1.5.3/changelog.d/releases/20.1.5.2.yaml000066400000000000000000000005651473623114700227770ustar00rootroot00000000000000entries: bug: - message: Include tests (and docs) and sdist correctly, and stop installing them to site-packages. pr_ids: - '162' timestamp: 1721819311 other: - message: Add support for Python 3.12. pr_ids: - '163' timestamp: 1721819580 previous_release: 1.5.1 release_date: '2024-07-24' release_description: '' release_version: 1.5.2 pytest-subprocess-1.5.3/changelog.d/releases/21.1.5.3.yaml000066400000000000000000000012771473623114700230020ustar00rootroot00000000000000entries: bug: - message: Fixed an incorrect wait timeout calculation. pr_ids: - '180' timestamp: 1735744258 - message: Wrapped ProcessDispatcher.dispatch into FakePopenWrapper as it was causing TypeError when Popen is used as a type. pr_ids: - '170' timestamp: 1727883418 - message: Get rid of using thread in AsyncFakePopen as it causes thread.join() to hang indefinitely. pr_ids: - '169' timestamp: 1727847419 feature: - message: Allow to access keyword arguments passed to Popen. pr_ids: - '171' - '178' timestamp: 1728114595 previous_release: 1.5.2 release_date: '2025-01-04' release_description: '' release_version: 1.5.3 pytest-subprocess-1.5.3/changelog.d/releases/3.0.1.3.yaml000066400000000000000000000007311473623114700227070ustar00rootroot00000000000000entries: bug: - message: Properly raise exceptions from callback functions. pr_ids: - '12' timestamp: 1583143353 doc: - message: Add documentation chapter about the callback functions. pr_ids: - '15' timestamp: 1583308593 feature: - message: Allow passing keyword arguments into callbacks. pr_ids: - '13' timestamp: 1583158583 previous_release: 0.1.2 release_date: '2020-03-04' release_description: '' release_version: 0.1.3 pytest-subprocess-1.5.3/changelog.d/releases/4.0.1.4.yaml000066400000000000000000000012241473623114700227070ustar00rootroot00000000000000entries: bug: - message: The `returncode` will not be ignored when `callback` is used. pr_ids: - '22' timestamp: 1588058646 - message: The exception raised from callback will take precedence over those from subprocess. pr_ids: - '21' timestamp: 1588052678 - message: Registering process will be now consistent regardless of the command type. pr_ids: - '20' timestamp: 1587995575 - message: Fixed crash for stderr redirect with an empty stream definition. pr_ids: - '19' timestamp: 1587964114 previous_release: 0.1.3 release_date: '2020-04-28' release_description: '' release_version: 0.1.4 pytest-subprocess-1.5.3/changelog.d/releases/5.0.1.5.yaml000066400000000000000000000003671473623114700227200ustar00rootroot00000000000000entries: bug: - message: '`encoding` and `errors` arguments will properly trigger `text` mode.' pr_ids: - '26' timestamp: 1592481300 previous_release: 0.1.4 release_date: '2020-06-19' release_description: '' release_version: 0.1.5 pytest-subprocess-1.5.3/changelog.d/releases/6.1.0.0.yaml000066400000000000000000000006001473623114700227020ustar00rootroot00000000000000entries: feature: - message: Remember subprocess calls to check if expected commands were executed. pr_ids: - '29' timestamp: 1598091262 - message: Allow to match a command with variable arguments (non-exact matching). pr_ids: - '28' timestamp: 1598077558 previous_release: 0.1.5 release_date: '2020-08-22' release_description: '' release_version: 1.0.0 pytest-subprocess-1.5.3/changelog.d/releases/7.1.0.1.yaml000066400000000000000000000005401473623114700227070ustar00rootroot00000000000000entries: bug: - message: Prevent appending newlines to outputs unless defined as list/tuple. pr_ids: - '34' timestamp: 1616257599 other: - message: Make the ``Command`` class iterable. pr_ids: - '32' timestamp: 1599744085 previous_release: 1.0.0 release_date: '2021-03-20' release_description: '' release_version: 1.0.1 pytest-subprocess-1.5.3/changelog.d/releases/8.1.1.0.yaml000066400000000000000000000010511473623114700227060ustar00rootroot00000000000000entries: bug: - message: Preserve original command in `proc.args` to prevent leaking the internal `Command` type. pr_ids: - '37' timestamp: 1618473214 other: - message: Switched CI from Azure Pipelines to GitHub Actions. pr_ids: - '38' timestamp: 1618475848 - message: Drop support for python 3.4 and 3.5. Move type annotations from `.pyi` files into sources. pr_ids: - '35' timestamp: 1618203821 previous_release: 1.0.1 release_date: '2021-04-18' release_description: '' release_version: 1.1.0 pytest-subprocess-1.5.3/changelog.d/releases/9.1.1.1.yaml000066400000000000000000000005541473623114700227170ustar00rootroot00000000000000entries: bug: - message: Wait for callback thread to finish when calling ``communicate()``. pr_ids: - '43' timestamp: 1624031059 other: - message: Fix type annotations for `register_subprocess()`. pr_ids: - '42' timestamp: 1620915915 previous_release: 1.1.0 release_date: '2021-06-18' release_description: '' release_version: 1.1.1 pytest-subprocess-1.5.3/changelog.d/templates/000077500000000000000000000000001473623114700214155ustar00rootroot00000000000000pytest-subprocess-1.5.3/changelog.d/templates/entry.rst000066400000000000000000000002651473623114700233130ustar00rootroot00000000000000* {% if pr_ids is defined and pr_ids -%} {% for pr in pr_ids -%} `#{{ pr }} <{{ prs_url }}/{{ pr }}>`_{% if not loop.last %}, {% endif %} {%- endfor %}: {% endif -%} {{ message }} pytest-subprocess-1.5.3/changelog.d/templates/main.rst000066400000000000000000000001071473623114700230710ustar00rootroot00000000000000History ======= {% for release in releases %}{{ release }}{% endfor %} pytest-subprocess-1.5.3/changelog.d/templates/release.rst000066400000000000000000000005161473623114700235710ustar00rootroot00000000000000 {{ release_version }} ({{ release_date }}) {{ "-" * release_version|length }}---{{ "-" * release_date|length }} {% if release_description %} {{ release_description }} {% endif %}{% for group in entry_groups %} {{ group.title }} {{ "~" * group.title|length }} {% for entry in group.entries %}{{ entry }}{% endfor %}{% endfor %} pytest-subprocess-1.5.3/docs/000077500000000000000000000000001473623114700161765ustar00rootroot00000000000000pytest-subprocess-1.5.3/docs/Makefile000066400000000000000000000011721473623114700176370ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pytest-subprocess-1.5.3/docs/api.rst000066400000000000000000000011021473623114700174730ustar00rootroot00000000000000API Reference ============= fake_subprocess --------------- The main entrypoint class for all ``fake_subprocess`` operations is the ``FakeProcess`` class. This class is instantiated and returned when the ``fake_subprocess`` fixture is being used. .. autoclass:: pytest_subprocess.fake_process.FakeProcess :members: any() ----- For a non-exact matching, you can use the ``Any()`` class that is available to use via ``fake_subprocess.any``. This class can be used to replace a number of arguments that might occur, .. autoclass:: pytest_subprocess.utils.Any :members: pytest-subprocess-1.5.3/docs/conf.py000066400000000000000000000045641473623114700175060ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # 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 datetime from pathlib import Path import pkg_resources from changelogd import changelogd ROOT_PATH = Path(__file__).parents[1] changelogd.release(partial=True, output="history.rst", config=ROOT_PATH / "changelog.d") # -- Project information ----------------------------------------------------- project = "pytest-subprocess" copyright = f"2019-{datetime.datetime.now().year}, Andrzej Klajnert" author = "Andrzej Klajnert" # The full version, including alpha/beta/rc tags release = pkg_resources.get_distribution("pip").version # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinxcontrib.napoleon", "sphinx.ext.autodoc", "sphinx_autodoc_typehints", ] typehints_fully_qualified = False always_document_param_types = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "furo" html_title = "pytest-subprocess" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] pytest-subprocess-1.5.3/docs/index.rst000066400000000000000000000020041473623114700200330ustar00rootroot00000000000000.. pytest-subprocess documentation master file, created by sphinx-quickstart on Sat Nov 23 13:49:07 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. pytest-subprocess ================= This is a pytest plugin to fake the real subprocess behavior to make your tests more independent. Example ------- You can use the provided ``fake_process`` (or ``fp`` for short) fixture to register commands and specify their behavior before they will be executed. This will prevent a real subprocess execution. .. code-block:: python def test_process(fp): fp.register(["fake-command"]) process = subprocess.run(["fake-command"]) assert process.returncode == 0 Table of contents ----------------- .. toctree:: :maxdepth: 2 usage api history Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _`pip`: https://pypi.org/project/pip/ .. _`PyPI`: https://pypi.org/project pytest-subprocess-1.5.3/docs/make.bat000066400000000000000000000013701473623114700176040ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pytest-subprocess-1.5.3/docs/usage.rst000066400000000000000000000001261473623114700200330ustar00rootroot00000000000000.. include:: ../README.rst :start-after: include-start :end-before: include-end pytest-subprocess-1.5.3/noxfile.py000066400000000000000000000026221473623114700172660ustar00rootroot00000000000000from pathlib import Path import nox @nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.8"]) def tests(session): session.install(".[test]") session.run( "pytest", *session.posargs, env={"PYTHONPATH": str(Path(__file__).resolve().parent)} ) @nox.session def flake8(session): session.install("flake8") session.run("flake8", "pytest_subprocess", "tests", *session.posargs) @nox.session def mypy(session): session.install("mypy") session.run("mypy", "--version") session.run("mypy", "pytest_subprocess", "--config-file=setup.cfg") session.run("mypy", "tests/test_typing.py", "--config-file=setup.cfg") # Note sphinx-napoleon uses deprecated renames removed in 3.10 @nox.session(python="3.9") def docs(session): session.install(".[docs]") session.run("sphinx-build", "-b", "html", "docs", "docs/_build", "-v", "-W") @nox.session def create_dist(session): session.install("twine", "build") session.run("python", "-m", "build") session.run("twine", "check", "dist/*") @nox.session def publish(session): """Publish to pypi. Run `nox publish -- prod` to publish to the official repo.""" create_dist(session) twine_command = ["twine", "upload", "dist/*"] if "prod" not in session.posargs: twine_command.extend(["--repository-url", "https://test.pypi.org/legacy/"]) session.run(*twine_command) pytest-subprocess-1.5.3/pytest.ini000066400000000000000000000001541473623114700172770ustar00rootroot00000000000000[pytest] junit_family=legacy filterwarnings = error ignore::pytest.PytestUnraisableExceptionWarning pytest-subprocess-1.5.3/pytest_subprocess/000077500000000000000000000000001473623114700210465ustar00rootroot00000000000000pytest-subprocess-1.5.3/pytest_subprocess/__init__.py000066400000000000000000000003321473623114700231550ustar00rootroot00000000000000"""Main module""" from . import exceptions from .fake_process import FakeProcess ProcessNotRegisteredError = exceptions.ProcessNotRegisteredError __all__ = ["FakeProcess", "exceptions", "ProcessNotRegisteredError"] pytest-subprocess-1.5.3/pytest_subprocess/asyncio_subprocess.py000066400000000000000000000004201473623114700253310ustar00rootroot00000000000000from asyncio.subprocess import DEVNULL from asyncio.subprocess import PIPE from asyncio.subprocess import Process from asyncio.subprocess import STDOUT from asyncio.subprocess import SubprocessStreamProtocol _ = (DEVNULL, PIPE, STDOUT, Process, SubprocessStreamProtocol) pytest-subprocess-1.5.3/pytest_subprocess/exceptions.py000066400000000000000000000006551473623114700236070ustar00rootroot00000000000000class ProcessNotRegisteredError(Exception): """ Raised when the attempted command wasn't registered before. Use `fake_process.allow_unregistered(True)` if you want to use real subprocess. """ class IncorrectProcessDefinition(Exception): """Raised when the register() has been called with wrong arguments""" class PluginInternalError(Exception): """Raised in case of an internal error in the plugin""" pytest-subprocess-1.5.3/pytest_subprocess/fake_popen.py000066400000000000000000000360471473623114700235410ustar00rootroot00000000000000"""FakePopen class declaration""" import asyncio import collections.abc import concurrent.futures import copy import io import os import signal import subprocess import sys import time from functools import partial from typing import Any as AnyType from typing import Callable from typing import Dict from typing import IO from typing import List from typing import Optional from typing import Sequence from typing import Tuple from typing import Union from . import exceptions from .types import BUFFER from .types import OPTIONAL_TEXT from .types import OPTIONAL_TEXT_OR_ITERABLE from .utils import Thread if sys.platform.startswith("win") and sys.version_info < (3, 8): COMMAND_SEQ = Sequence[Union[str, bytes]] else: COMMAND_SEQ = Sequence[Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]] class FakePopen: """Base class that fakes the real subprocess.Popen()""" stdout: Optional[BUFFER] = None stderr: Optional[BUFFER] = None returncode: Optional[int] = None text_mode: bool = False pid: int = 0 def __init__( self, command: Union[ Union[bytes, str], COMMAND_SEQ, ], stdout: OPTIONAL_TEXT_OR_ITERABLE = None, stderr: OPTIONAL_TEXT_OR_ITERABLE = None, returncode: int = 0, wait: Optional[float] = None, callback: Optional[Callable] = None, callback_kwargs: Optional[Dict[str, AnyType]] = None, signal_callback: Optional[Callable] = None, stdin_callable: Optional[Callable] = None, **_: Dict[str, AnyType], ) -> None: if ( not isinstance(command, (str, bytes)) and sys.platform.startswith("win") and sys.version_info < (3, 8) ): for arg in command: if isinstance(arg, os.PathLike): msg = f"argument of type {arg.__class__.__name__!r} is not iterable" raise TypeError(msg) self.args = command self.__kwargs: Optional[Dict[str, AnyType]] = None self.__stdout: OPTIONAL_TEXT_OR_ITERABLE = stdout self.__stderr: OPTIONAL_TEXT_OR_ITERABLE = stderr self.__thread: Optional[Thread] = None self.__signal_callback: Optional[Callable] = signal_callback self.__stdin_callable: Optional[Optional[Callable]] = stdin_callable self.__universal_newlines: Optional[Dict[AnyType, AnyType]] = None self._signals: List[int] = [] self._returncode: Optional[int] = returncode self._wait_timeout: Optional[float] = wait self._callback: Optional[Optional[Callable]] = callback self._callback_kwargs: Optional[Dict[str, AnyType]] = callback_kwargs @property def kwargs(self) -> Optional[Dict[str, AnyType]]: return self.__kwargs def __enter__(self) -> "FakePopen": return self def __exit__(self, *args: List, **kwargs: Dict) -> None: if self.__thread and self.__thread.exception: raise self.__thread.exception def communicate( self, input: OPTIONAL_TEXT = None, timeout: Optional[float] = None ) -> Tuple[AnyType, AnyType]: self._handle_stdin(input) self._finalize_thread(timeout) if isinstance(self.stdout, asyncio.StreamReader) or isinstance( self.stderr, asyncio.StreamReader ): raise exceptions.PluginInternalError return ( self.stdout.getvalue() if self.stdout else None, self.stderr.getvalue() if self.stderr else None, ) def _handle_stdin(self, input: OPTIONAL_TEXT) -> None: if input and self.__stdin_callable: callable_output = self.__stdin_callable(input) if isinstance(callable_output, dict): self.stdout = self._extend_stream_from_dict( callable_output, "stdout", self.stdout ) self.stderr = self._extend_stream_from_dict( callable_output, "stderr", self.stderr ) def _finalize_thread(self, timeout: Optional[float]) -> None: if self.__thread is None: return self.__thread.join(timeout) if self.returncode is None and self._returncode is not None: self.returncode = self._returncode if self.__thread.exception: raise self.__thread.exception def _extend_stream_from_dict( self, dictionary: Dict[str, AnyType], key: str, stream: Optional[BUFFER] ) -> Optional[BUFFER]: data = dictionary.get(key) if data: return self._prepare_buffer(input=data, io_base=stream) return None def poll(self) -> Optional[int]: return self.returncode def wait(self, timeout: Optional[float] = None) -> int: if timeout and self._wait_timeout: self._wait_timeout -= timeout if timeout < self._wait_timeout: raise subprocess.TimeoutExpired(self.args, timeout) self._finalize_thread(timeout) if self.returncode is None: raise exceptions.PluginInternalError return self.returncode def send_signal(self, sig: int) -> None: self._signals.append(sig) if self.__signal_callback: self.__signal_callback(self, sig) def terminate(self) -> None: self.send_signal(signal.SIGTERM) def kill(self) -> None: if sys.platform == "win32": self.terminate() else: self.send_signal(signal.SIGKILL) def configure(self, **kwargs: Optional[Dict]) -> None: """Setup the FakePopen instance based on a real Popen arguments.""" self.__kwargs = self.safe_copy(kwargs) self.__universal_newlines = kwargs.get("universal_newlines", None) text = kwargs.get("text", None) encoding = kwargs.get("encoding", None) errors = kwargs.get("errors", None) if text and sys.version_info < (3, 7): raise TypeError("__init__() got an unexpected keyword argument 'text'") self.text_mode = bool(text or self.__universal_newlines or encoding or errors) # validation taken from the real subprocess if ( text is not None and self.__universal_newlines is not None and bool(self.__universal_newlines) != bool(text) ): raise subprocess.SubprocessError( "Cannot disambiguate when both text " "and universal_newlines are supplied but " "different. Pass one or the other." ) stdout = kwargs.get("stdout") if stdout == subprocess.PIPE: self.stdout = self._prepare_buffer(self.__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: 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 isinstance(stderr, (io.BufferedWriter, io.TextIOWrapper)): self._write_to_buffer(self.__stderr, stderr) @staticmethod def safe_copy(kwargs: Dict[str, AnyType]) -> Dict[str, AnyType]: """ Deepcopy can fail if the value is not serializable, fallback to shallow copy. """ try: return copy.deepcopy(kwargs) except TypeError: return dict(**kwargs) def _prepare_buffer( self, input: OPTIONAL_TEXT_OR_ITERABLE, io_base: Optional[BUFFER] = None, ) -> Union[io.BytesIO, io.StringIO, asyncio.StreamReader]: linesep = self._convert(os.linesep) if isinstance(input, (list, tuple)): # need to disable mypy, as input and linesep are unions, # mypy thinks that the types might be incompatible, but # the _convert() function handles that input = linesep.join(map(self._convert, input)) # type: ignore # Add trailing newline if data is present. if input: # same reason to disable mypy as above input += linesep # type: ignore 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 and self.__universal_newlines and isinstance(input, str): input = input.replace("\r\n", "\n") if ( io_base and not isinstance(io_base, asyncio.StreamReader) and io_base.tell() == 0 ): # same reason for disabling mypy as in `input = linesep.join...`: # both are union so could be incompatible if not _convert() input = io_base.getvalue() + (input) # type: ignore if io_base is None: io_base = self._get_empty_buffer(self.text_mode) if input is None: return io_base # similar as above - mypy has to be disabled because unions if isinstance(io_base, asyncio.StreamReader): io_base.feed_data(self._data_to_bytes(input)) else: io_base.write(input) # type: ignore return io_base def _get_empty_buffer(self, text: bool) -> BUFFER: return io.StringIO() if text else io.BytesIO() def _to_bytes(self, data: Sequence[Union[str, bytes]]) -> Sequence[bytes]: return [elem if isinstance(elem, bytes) else elem.encode() for elem in data] def _data_to_bytes(self, data: OPTIONAL_TEXT_OR_ITERABLE) -> bytes: if isinstance(data, collections.abc.Sequence) and not isinstance(data, bytes): return b"\n".join( (item if isinstance(item, bytes) else item.encode() for item in data) ) if isinstance(data, str): return data.encode() if not data: return b"" return data def _write_to_buffer(self, data: OPTIONAL_TEXT_OR_ITERABLE, buffer: IO) -> None: data_type: Callable = ( # mypy doesn't seem to recognize `partial` as a function partial(bytes, encoding=sys.getfilesystemencoding()) # type: ignore if "b" in buffer.mode else str ) if isinstance(data, (list, tuple)): buffer.writelines([data_type(line + "\n") for line in data]) else: buffer.write(data_type(data)) def _convert(self, input: Union[str, bytes]) -> Union[str, bytes]: if isinstance(input, bytes) and self.text_mode: return input.decode() if isinstance(input, str) and not self.text_mode: return input.encode() return input def _wait(self, wait_period: float) -> None: time.sleep(wait_period) if self.returncode is None: self._finish_process() def run_thread(self) -> None: """Run the user-defined callback or wait in a thread.""" if self._wait_timeout is None and self._callback is None: self._finish_process() else: if self._callback: self.__thread = Thread( target=self._callback, args=(self,), kwargs=self._callback_kwargs or {}, ) else: self.__thread = Thread(target=self._wait, args=(self._wait_timeout,)) self.__thread.start() def _finish_process(self) -> None: self.returncode = self._returncode self._finalize_streams() def _finalize_streams(self) -> None: self._finalize_stream(self.stdout) self._finalize_stream(self.stderr) def _finalize_stream(self, stream: Optional[BUFFER]) -> None: if isinstance(stream, asyncio.StreamReader): stream.feed_eof() elif stream: stream.seek(0) def received_signals(self) -> Tuple[int, ...]: """Get a tuple of signals received by the process.""" return tuple(self._signals) class AsyncFakePopen(FakePopen): """Class to handle async processes""" stdout: Optional[asyncio.StreamReader] stderr: Optional[asyncio.StreamReader] async def communicate( # type: ignore self, input: OPTIONAL_TEXT = None, timeout: Optional[float] = None ) -> Tuple[AnyType, AnyType]: if input: # streams were fed with eof, need to be reopened await self._reopen_streams() self._handle_stdin(input) # feed eof one more time as streams were opened self._finalize_streams() await self._finalize(timeout) return ( await self.stdout.read() if self.stdout else None, await self.stderr.read() if self.stderr else None, ) async def wait(self, timeout: Optional[float] = None) -> int: # type: ignore if timeout and self._wait_timeout and timeout < self._wait_timeout: self._wait_timeout -= timeout raise subprocess.TimeoutExpired(self.args, timeout) await self._finalize(timeout) if self.returncode is None: raise exceptions.PluginInternalError return self.returncode def _get_empty_buffer(self, _: bool) -> asyncio.StreamReader: return asyncio.StreamReader() async def _reopen_streams(self) -> None: self.stdout = await self._reopen_stream(self.stdout) self.stderr = await self._reopen_stream(self.stderr) async def _reopen_stream( self, stream: Optional[asyncio.StreamReader] ) -> Optional[asyncio.StreamReader]: if stream: data = await stream.read() fresh_stream = self._get_empty_buffer(False) fresh_stream.feed_data(data) return fresh_stream return None def run_thread(self) -> None: """Async impl should not contain any thread based implementation""" def evaluate(self) -> None: """Check if process needs to be finished.""" if self._wait_timeout is None and self._callback is None: self._finish_process() async def _run_callback_in_executor(self) -> None: """Run in executor the user-defined callback or wait.""" loop = asyncio.get_running_loop() with concurrent.futures.ThreadPoolExecutor() as pool: if self._callback: kwargs = self._callback_kwargs or {} cbk = partial(self._callback, **kwargs) await loop.run_in_executor(pool, cbk, self) elif self._wait_timeout is not None: await loop.run_in_executor(pool, self._wait, self._wait_timeout) async def _finalize(self, timeout: Optional[float] = None) -> None: """Run the user-defined callback or wait. Finish process""" if self.returncode is not None: return if timeout is not None: await asyncio.wait_for(self._run_callback_in_executor(), timeout=timeout) else: await self._run_callback_in_executor() if self.returncode is None: self.returncode = self._returncode self._finalize_streams() pytest-subprocess-1.5.3/pytest_subprocess/fake_process.py000066400000000000000000000123401473623114700240640ustar00rootroot00000000000000"""FakeProcess class declaration""" from collections import defaultdict from collections import deque from typing import Any as AnyType from typing import Callable from typing import ClassVar from typing import DefaultDict from typing import Deque from typing import Dict from typing import List from typing import Optional from typing import Type from typing import Union from . import exceptions from .process_dispatcher import ProcessDispatcher from .process_recorder import ProcessRecorder from .types import COMMAND from .types import OPTIONAL_TEXT_OR_ITERABLE from .utils import Any from .utils import Command from .utils import Program class FakeProcess: """Main class responsible for process operations""" any: ClassVar[Type[Any]] = Any program: ClassVar[Type[Program]] = Program def __init__(self) -> None: self.definitions: DefaultDict[Command, Deque[Union[Dict, bool]]] = defaultdict( deque ) self.calls: Deque[COMMAND] = deque() self._allow_unregistered: bool = False self._keep_last_process: bool = False self.exceptions = exceptions def register( self, command: COMMAND, stdout: OPTIONAL_TEXT_OR_ITERABLE = None, stderr: OPTIONAL_TEXT_OR_ITERABLE = None, returncode: int = 0, wait: Optional[float] = None, callback: Optional[Callable] = None, callback_kwargs: Optional[Dict[str, AnyType]] = None, signal_callback: Optional[Callable] = None, occurrences: int = 1, stdin_callable: Optional[Callable] = None, ) -> ProcessRecorder: """ Main method for registering the subprocess instances. Args: command: register the command that will be faked stdout: value of the standard output stderr: value of the error output returncode: return code of the faked process wait: artificially wait for the process to finish callback: function that will be executed instead of the process callback_kwargs: keyword arguments that will be passed into callback occurrences: allow multiple usages of the same command stdin_callable: function that will interact with stdin """ if wait is not None and callback is not None: raise exceptions.IncorrectProcessDefinition( "The 'callback' and 'wait' arguments cannot be used " "together. Add sleep() to your callback instead." ) if not isinstance(command, Command): command = Command(command) recorder = ProcessRecorder() self.definitions[command].extend( [ { "command": command, "stdout": stdout, "stderr": stderr, "returncode": returncode, "wait": wait, "callback": callback, "callback_kwargs": callback_kwargs, "signal_callback": signal_callback, "stdin_callable": stdin_callable, "recorder": recorder, } ] * occurrences ) return recorder register_subprocess = register def pass_command( self, command: COMMAND, occurrences: int = 1, ) -> None: """ Allow to use a real subprocess together with faked ones. Args: command: allow to execute the supplied command occurrences: allow multiple usages of the same command """ if not isinstance(command, Command): command = Command(command) self.definitions[command].extend([True] * occurrences) def __enter__(self) -> "FakeProcess": ProcessDispatcher.register(self) return self def __exit__(self, *args: List, **kwargs: Dict) -> None: ProcessDispatcher.deregister(self) def allow_unregistered(self, allow: bool) -> None: """ Allow / block unregistered processes execution. When allowed, the real subprocesses will be called. Blocking will raise the exception. Args: allow: decide whether the unregistered process shall be allowed """ self._allow_unregistered = allow def call_count(self, command: COMMAND) -> int: """ Count how many times a certain command was called. Can be used together with `fake_process.any()`. Args: command: lookup command Returns: number of times a command was called """ if not isinstance(command, Command): command_instance = Command(command) return len(tuple(filter(lambda elem: elem == command_instance, self.calls))) def keep_last_process(self, keep: bool) -> None: """ Keep last process definition from being removed. That can allow / block multiple execution of the same command. Args: keep: decide whether last process shall be kept """ self._keep_last_process = keep @classmethod def context(cls) -> "FakeProcess": """Return a new FakeProcess instance to use it as a context manager.""" return cls() pytest-subprocess-1.5.3/pytest_subprocess/fixtures.py000066400000000000000000000003671473623114700232770ustar00rootroot00000000000000from typing import Generator import pytest from . import FakeProcess @pytest.fixture def fp() -> Generator[FakeProcess, None, None]: """Fake subprocess calls.""" with FakeProcess() as process: yield process fake_process = fp pytest-subprocess-1.5.3/pytest_subprocess/process_dispatcher.py000066400000000000000000000211501473623114700253030ustar00rootroot00000000000000"""ProcessDispatcher class declaration""" import asyncio import subprocess import sys import typing from collections import deque from copy import deepcopy from functools import partial from typing import Any as AnyType from typing import AnyStr from typing import Awaitable from typing import Callable from typing import Deque from typing import Dict from typing import Generic from typing import List from typing import Optional from typing import Tuple from typing import Type from typing import Union from . import asyncio_subprocess from . import exceptions from .fake_popen import AsyncFakePopen from .fake_popen import FakePopen from .types import COMMAND from .utils import Command if typing.TYPE_CHECKING: from .fake_process import FakeProcess __all__ = ["ProcessDispatcher"] class ProcessDispatcher: """Main class for handling processes.""" process_list: List["FakeProcess"] = [] built_in_popen: Optional[Callable] = None built_in_async_subprocess: Optional[AnyType] = None _allow_unregistered: bool = False _cache: Dict["FakeProcess", Dict["FakeProcess", AnyType]] = dict() _keep_last_process: bool = False _pid: int = 0 @classmethod def register(cls, process: "FakeProcess") -> None: if not cls.process_list: cls.built_in_popen = subprocess.Popen subprocess.Popen = FakePopenWrapper # type: ignore cls.built_in_async_subprocess = asyncio.subprocess asyncio.create_subprocess_shell = cls.async_shell # type: ignore asyncio.create_subprocess_exec = cls.async_exec # type: ignore asyncio.subprocess = asyncio_subprocess cls._cache[process] = { proc: deepcopy(proc.definitions) for proc in cls.process_list } cls.process_list.append(process) @classmethod def deregister(cls, process: "FakeProcess") -> None: cls.process_list.remove(process) cache = cls._cache.pop(process) for proc, processes in cache.items(): proc.definitions = processes if not cls.process_list: subprocess.Popen = cls.built_in_popen # type: ignore cls.built_in_popen = None if cls.built_in_async_subprocess is None: raise exceptions.PluginInternalError asyncio.subprocess = cls.built_in_async_subprocess asyncio.create_subprocess_shell = ( cls.built_in_async_subprocess.create_subprocess_shell ) asyncio.create_subprocess_exec = ( cls.built_in_async_subprocess.create_subprocess_exec ) cls.built_in_async_subprocess = None @classmethod def dispatch( cls, command: COMMAND, **kwargs: Optional[Dict] ) -> Union[FakePopen, subprocess.Popen]: """This method will be used instead of the subprocess.Popen()""" process = cls.__dispatch(command) if process is None: if cls.built_in_popen is None: raise exceptions.PluginInternalError popen: subprocess.Popen = cls.built_in_popen(command, **kwargs) return popen result = cls._prepare_instance(FakePopen, command, kwargs, process) if not isinstance(result, FakePopen): raise exceptions.PluginInternalError result.run_thread() return result @classmethod async def async_shell( cls, cmd: Union[str, bytes], **kwargs: Dict ) -> Union[AsyncFakePopen, asyncio.subprocess.Process]: """Replacement of asyncio.create_subprocess_shell()""" if not isinstance(cmd, (str, bytes)): raise ValueError("cmd must be a string") method = partial( cls.built_in_async_subprocess.create_subprocess_shell, # type: ignore cmd, **kwargs ) if isinstance(cmd, bytes): cmd = cmd.decode() return await cls._call_async(cmd, method, kwargs) @classmethod async def async_exec( cls, program: Union[str, bytes], *args: Union[str, bytes], **kwargs: Dict ) -> Union[AsyncFakePopen, asyncio.subprocess.Process]: """Replacement of asyncio.create_subprocess_exec()""" if not isinstance(program, (str, bytes)): raise ValueError("program must be a string") method = partial( cls.built_in_async_subprocess.create_subprocess_exec, # type: ignore program, *args, **kwargs ) if isinstance(program, bytes): program = program.decode() command = [ program, *[arg.decode() if isinstance(arg, bytes) else arg for arg in args], ] return await cls._call_async(command, method, kwargs) @classmethod async def _call_async( cls, command: COMMAND, async_method: Callable, kwargs: Dict, ) -> Union[AsyncFakePopen, asyncio.subprocess.Process]: process = cls.__dispatch(command) if process is None: if cls.built_in_async_subprocess is None: raise exceptions.PluginInternalError async_shell: Awaitable[asyncio.subprocess.Process] = async_method() return await async_shell if sys.platform == "win32" and isinstance( asyncio.get_event_loop_policy().get_event_loop(), asyncio.SelectorEventLoop ): raise NotImplementedError( "The SelectorEventLoop doesn't support subprocess" ) result = cls._prepare_instance(AsyncFakePopen, command, kwargs, process) if not isinstance(result, AsyncFakePopen): raise exceptions.PluginInternalError result.evaluate() return result @classmethod def _prepare_instance( cls, klass: Union[Type[FakePopen], Type[AsyncFakePopen]], command: COMMAND, kwargs: dict, process: dict, ) -> Union[FakePopen, AsyncFakePopen]: # Update the command with the actual command specified by the caller. # This will ensure that Command objects do not end up unexpectedly in # caller's objects (e.g. proc.args, CalledProcessError.cmd). Take care # to preserve the dict that may still be referenced when using # keep_last_process. fake_popen_kwargs = process.copy() fake_popen_kwargs["command"] = command recorder = fake_popen_kwargs.pop("recorder") result = klass(**fake_popen_kwargs) recorder.calls.append(result) result.pid = cls._pid result.configure(**kwargs) return result @classmethod def __dispatch(cls, command: COMMAND) -> Optional[dict]: command_instance, processes, process_instance = cls._get_process(command) if process_instance: process_instance.calls.append(command) if not processes: if not cls.process_list[-1]._allow_unregistered: raise exceptions.ProcessNotRegisteredError( "The process '%s' was not registered." % ( ( command if isinstance(command, str) else " ".join(str(item) for item in command) ), ) ) else: return None process = processes.popleft() if not processes and process_instance is not None: if cls.process_list[-1]._keep_last_process: processes.append(process) elif command_instance: del process_instance.definitions[command_instance] cls._pid += 1 if isinstance(process, bool): # real process will be called return None return process @classmethod def _get_process( cls, command: COMMAND ) -> Tuple[ Optional[Command], Optional[Deque[Union[dict, bool]]], Optional["FakeProcess"] ]: for proc in reversed(cls.process_list): command_instance, processes = next( ( (key, value) for key, value in proc.definitions.items() if key == command ), (None, None), ) process_instance = proc if processes and isinstance(processes, deque): return command_instance, processes, process_instance return None, None, None class FakePopenWrapper(Generic[AnyStr]): def __new__( # type: ignore cls, command: COMMAND, **kwargs: Optional[Dict] ) -> FakePopen: return ProcessDispatcher.dispatch(command, **kwargs) # type: ignore pytest-subprocess-1.5.3/pytest_subprocess/process_recorder.py000066400000000000000000000036571473623114700247760ustar00rootroot00000000000000from typing import Iterator from typing import List from typing import Optional from typing import TYPE_CHECKING from typing import Union from .types import COMMAND from .utils import Command if TYPE_CHECKING: from .fake_popen import FakePopen, AsyncFakePopen class ProcessRecorder: """ Recorder class that holds all FakePopen (or AsyncFakePopen) instances that were created by the subprocess. The class contains auxiliary """ calls: List[Union["FakePopen", "AsyncFakePopen"]] def __init__(self) -> None: self.calls = [] @property def first_call(self) -> Optional[Union["FakePopen", "AsyncFakePopen"]]: """Get the first process call""" if not self.calls: return None return self.calls[0] @property def last_call(self) -> Optional[Union["FakePopen", "AsyncFakePopen"]]: """Get the last (latest) process call""" if not self.calls: return None return self.calls[-1] def call_count(self, command: Optional[COMMAND] = None) -> int: """Get process call count - optionally match with arguments""" if not command: return len(self.calls) return len(tuple(self.get_matching_calls(command))) def was_called(self, command: Optional[COMMAND] = None) -> bool: """Check if process was called - optionally match with arguments""" if not self.calls: return False if not command: return True return any(self.get_matching_calls(command)) def get_matching_calls( self, command: COMMAND ) -> Iterator[Union["FakePopen", "AsyncFakePopen"]]: """Get calls that match arguments""" if not isinstance(command, Command): command = Command(command) return (call for call in self.calls if self.calls if command == call.args) def clear(self) -> None: """Clear records""" self.calls.clear() pytest-subprocess-1.5.3/pytest_subprocess/py.typed000066400000000000000000000000001473623114700225330ustar00rootroot00000000000000pytest-subprocess-1.5.3/pytest_subprocess/types.py000066400000000000000000000007241473623114700225670ustar00rootroot00000000000000import asyncio import io import os from typing import Sequence from typing import Union from .utils import Any from .utils import Command from .utils import Program OPTIONAL_TEXT = Union[str, bytes, None] OPTIONAL_TEXT_OR_ITERABLE = Union[ str, bytes, None, Sequence[Union[str, bytes]], ] BUFFER = Union[io.BytesIO, io.StringIO, asyncio.StreamReader] ARGUMENT = Union[str, Any, os.PathLike, Program] COMMAND = Union[Sequence[ARGUMENT], str, Command] pytest-subprocess-1.5.3/pytest_subprocess/utils.py000066400000000000000000000121641473623114700225640ustar00rootroot00000000000000import os import sys import threading from pathlib import Path from typing import Any as AnyType from typing import Iterator from typing import Optional from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING from typing import Union if TYPE_CHECKING: from .types import COMMAND ARGUMENT = Union[str, "Any", os.PathLike] class Thread(threading.Thread): """Custom thread class to capture exceptions""" exception: Optional[Exception] = None def run(self) -> None: try: super().run() except Exception as exc: self.exception = exc class Command: """Command definition class.""" __slots__ = "command" def __init__( self, command: "COMMAND", ): if isinstance(command, str): command = tuple(command.split(" ")) if not isinstance(command, (list, tuple)): raise TypeError("Command can be only of type string, list or tuple.") self.command: Tuple[ARGUMENT, ...] = tuple( os.fspath(c) if isinstance(c, os.PathLike) else c for c in command ) for i, command_elem in enumerate(self.command): if isinstance(command_elem, Any) and isinstance( self._get_next_command_elem(i), Any ): raise AttributeError("Cannot use `Any()` one after another.") def __eq__(self, other: AnyType) -> bool: if isinstance(other, str): other = other.split(" ") norm_command = [ os.fspath(c) if isinstance(c, os.PathLike) else c for c in self.command ] norm_other = [os.fspath(c) if isinstance(c, os.PathLike) else c for c in other] if norm_other == norm_command: # straightforward matching return True for i, command_elem in enumerate(norm_command): if isinstance(command_elem, Any): next_command_elem = self._get_next_command_elem(i) if next_command_elem is None: if not self._are_thresholds_ok(command_elem, len(norm_other)): return False return True else: next_matching_elem = self._get_next_matching_elem_index( norm_other, next_command_elem ) if next_matching_elem is None: return False else: if not self._are_thresholds_ok( command_elem, next_matching_elem ): return False norm_other = norm_other[next_matching_elem:] else: if len(norm_other) == 0 or norm_other.pop(0) != command_elem: return False return len(norm_other) == 0 def __iter__(self) -> Iterator: return iter(self.command) @staticmethod def _are_thresholds_ok(command_elem: "Any", value: int) -> bool: if command_elem.max is not None and value > command_elem.max: return False if command_elem.min is not None and value < command_elem.min: return False return True def _get_next_command_elem(self, index: int) -> Optional[ARGUMENT]: try: return self.command[index + 1] except IndexError: return None @staticmethod def _get_next_matching_elem_index( other: Sequence[ARGUMENT], elem: ARGUMENT ) -> Optional[int]: return next( (i for i, other_elem in enumerate(other) if elem == other_elem), None ) def __hash__(self) -> int: return hash(self.command) def __repr__(self) -> str: return str(self.command) def __str__(self) -> str: return str(self.command) class Any: """Wildcard definition class.""" def __init__(self, *, min: Optional[int] = None, max: Optional[int] = None) -> None: if min is not None and max is not None and min > max: raise AttributeError("min cannot be greater than max") self.min: Optional[int] = min self.max: Optional[int] = max def __str__(self) -> str: return f"{self.__class__.__name__} (min={self.min}, max={self.max})" def __repr__(self) -> str: return str(self) class Program: """Specifies the name of the final program to be executed.""" def __init__(self, program: str) -> None: self.program: str = program def __repr__(self) -> str: return f"{self.__class__.__name__}({self.program!r})" def __eq__(self, other: AnyType) -> bool: if isinstance(other, str): if Path(other).name == self.program: return True if sys.platform.startswith("win"): for ext in os.environ.get("PATHEXT", "").split(os.pathsep): if ( Path(other).name.lower() == Path(self.program).with_suffix(ext).name.lower() ): return True return False def __hash__(self) -> int: return hash(self.program) pytest-subprocess-1.5.3/setup.cfg000066400000000000000000000003361473623114700170710ustar00rootroot00000000000000[flake8] ignore = E231,W503 max-line-length = 89 [mypy] python_version = 3.8 warn_return_any = True warn_unused_configs = True disallow_untyped_defs = True ignore_missing_imports = True [mypy-setup] ignore_errors = True pytest-subprocess-1.5.3/setup.py000066400000000000000000000051071473623114700167630ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os from setuptools import find_packages from setuptools import setup def read(fname): file_path = os.path.join(os.path.dirname(__file__), fname) with open(file_path, encoding="utf-8") as file_handle: return file_handle.read() requirements = ["pytest>=4.0.0"] setup( name="pytest-subprocess", version="1.5.3", author="Andrzej Klajnert", author_email="python@aklajnert.pl", maintainer="Andrzej Klajnert", maintainer_email="python@aklajnert.pl", license="MIT", project_urls={ "Documentation": "https://pytest-subprocess.readthedocs.io", "Source": "https://github.com/aklajnert/pytest-subprocess", "Tracker": "https://github.com/aklajnert/pytest-subprocess/issues", }, description="A plugin to fake subprocess for pytest", long_description=read("README.rst") + "\n" + read("HISTORY.rst"), python_requires=">=3.6", install_requires=requirements, extras_require={ "test": [ "pytest>=4.0", "docutils>=0.12", "Pygments>=2.0", "pytest-rerunfailures", "pytest-asyncio>=0.15.1", "pytest-timeout", "anyio", ], "dev": ["nox", "changelogd"], "docs": [ "sphinx", "furo", "sphinxcontrib-napoleon", "sphinx-autodoc-typehints", "changelogd", ], }, packages=find_packages(exclude=["docs", "tests"]), package_data={"pytest_subprocess": ["py.typed"]}, classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Pytest", "Intended Audience :: Developers", "Topic :: Software Development :: Testing", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Operating System :: OS Independent", "License :: OSI Approved :: MIT License", ], entry_points={ "pytest11": [ "pytest-subprocess = pytest_subprocess.fixtures", ], }, ) pytest-subprocess-1.5.3/tests/000077500000000000000000000000001473623114700164105ustar00rootroot00000000000000pytest-subprocess-1.5.3/tests/__init__.py000066400000000000000000000000001473623114700205070ustar00rootroot00000000000000pytest-subprocess-1.5.3/tests/conftest.py000066400000000000000000000003411473623114700206050ustar00rootroot00000000000000import faulthandler import os import sys import pytest pytest_plugins = "pytester" faulthandler.enable(file=sys.stderr, all_threads=True) @pytest.fixture(autouse=True) def setup(): os.chdir(os.path.dirname(__file__)) pytest-subprocess-1.5.3/tests/example_script.py000066400000000000000000000005711473623114700220040ustar00rootroot00000000000000"""An example script to test subprocess.""" import sys import time print("Stdout line 1") print("Stdout line 2") if "stderr" in sys.argv: print("Stderr line 1", file=sys.stderr) if "wait" in sys.argv: time.sleep(0.5) if "non-zero" in sys.argv: sys.exit(1) if "input" in sys.argv: user_input = input("Provide an input: ") print("Provided:", user_input) pytest-subprocess-1.5.3/tests/test_asyncio.py000066400000000000000000000266241473623114700215000ustar00rootroot00000000000000import asyncio import os import sys import time import anyio import pytest from pytest_subprocess.fake_popen import AsyncFakePopen PYTHON = sys.executable @pytest.fixture() def event_loop_policy(request): if sys.platform.startswith("win"): if request.node.name.startswith("test_invalid_event_loop"): return asyncio.WindowsSelectorEventLoopPolicy() else: return asyncio.WindowsProactorEventLoopPolicy() return asyncio.DefaultEventLoopPolicy() if sys.platform.startswith("win") and sys.version_info < (3, 8): @pytest.fixture(autouse=True) def event_loop(request, event_loop_policy): loop = event_loop_policy.new_event_loop() yield loop loop.close() @pytest.mark.asyncio @pytest.mark.parametrize("mode", ["shell", "exec"]) async def test_basic_usage(fp, mode): shell = mode == "shell" fp.register(["some-command-that-is-definitely-unavailable"], returncode=500) method = ( asyncio.create_subprocess_shell if shell else asyncio.create_subprocess_exec ) process = await method("some-command-that-is-definitely-unavailable") returncode = await process.wait() assert process.returncode == returncode assert process.returncode == 500 @pytest.mark.asyncio @pytest.mark.parametrize("fake", [True, False]) async def test_with_arguments_shell(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) process = await asyncio.create_subprocess_shell( f"{PYTHON} example_script.py stderr", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out, err = await process.communicate() assert err == os.linesep.encode().join([b"Stderr line 1", b""]) assert out == os.linesep.encode().join([b"Stdout line 1", b"Stdout line 2", b""]) assert process.returncode == 0 @pytest.mark.asyncio @pytest.mark.parametrize("fake", [True, False]) async def test_with_arguments_exec(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) process = await asyncio.create_subprocess_exec( PYTHON, "example_script.py", "stderr", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out, err = await process.communicate() assert err == os.linesep.encode().join([b"Stderr line 1", b""]) assert out == os.linesep.encode().join([b"Stdout line 1", b"Stdout line 2", b""]) assert process.returncode == 0 @pytest.mark.asyncio @pytest.mark.parametrize("fake", [True, False]) @pytest.mark.parametrize("mode", ["shell", "exec"]) async def test_incorrect_call(fp, fake, mode): """Asyncio doesn't support command as a list""" shell = mode == "shell" fp.allow_unregistered(not fake) if fake: fp.register(["test"]) method = ( asyncio.create_subprocess_shell if shell else asyncio.create_subprocess_exec ) name = "cmd" if shell else "program" with pytest.raises(ValueError, match=f"{name} must be a string"): await method(["test"]) @pytest.mark.asyncio @pytest.mark.skipif('sys.platform!="win32"') @pytest.mark.parametrize("fake", [True, False]) @pytest.mark.parametrize("mode", ["shell", "exec"]) async def test_invalid_event_loop(fp, fake, mode): """ The event_loop is changed by the `event_loop` fixture based on the test name (hack). """ shell = mode == "shell" fp.allow_unregistered(not fake) if fake: fp.register([PYTHON, "example_script.py"]) with pytest.raises(NotImplementedError): if shell: await asyncio.create_subprocess_shell(f"{PYTHON} example_script.py") else: await asyncio.create_subprocess_exec(PYTHON, "example_script.py") @pytest.mark.asyncio @pytest.mark.parametrize("fake", [False, True]) @pytest.mark.parametrize("mode", ["shell", "exec"]) async def test_wait(fp, fake, mode): """ Check that wait argument still works. Unfortunately asyncio doesn't have the timeout functionality. """ shell = mode == "shell" fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py", "wait", "stderr"], stdout="Stdout line 1\nStdout line 2", stderr="Stderr line 1", wait=0.5, ) method = ( asyncio.create_subprocess_shell if shell else asyncio.create_subprocess_exec ) command = f"{PYTHON} example_script.py wait stderr" if not shell: command = command.split() else: command = [command] process = await method( *command, cwd=os.path.dirname(__file__), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) assert process.returncode is None start_time = time.time() returncode = await process.wait() assert time.time() - start_time >= 0.45 assert returncode == 0 @pytest.mark.asyncio async def test_devnull_stdout(fp): """From GitHub #63 - make sure all the `asyncio.subprocess` consts are available.""" fp.register("cat") await asyncio.create_subprocess_exec( "cat", stdin=asyncio.subprocess.DEVNULL, stdout=asyncio.subprocess.STDOUT, stderr=asyncio.subprocess.PIPE, ) @pytest.mark.asyncio async def test_anyio(fp): await anyio.sleep(0.01) @pytest.mark.asyncio @pytest.mark.parametrize("fake", [False, True]) async def test_stdout_and_stderr(fp, fake): if fake: fp.register( [PYTHON, "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) else: fp.allow_unregistered(True) process = await asyncio.create_subprocess_exec( PYTHON, "example_script.py", "stderr", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout_list = [] stderr_list = [] loop = asyncio.get_event_loop() await asyncio.gather( loop.create_task(_read_stream(process.stdout, stdout_list)), loop.create_task(_read_stream(process.stderr, stderr_list)), loop.create_task(process.wait()), ) assert stdout_list == [f"Stdout line 1{os.linesep}", f"Stdout line 2{os.linesep}"] assert stderr_list == [f"Stderr line 1{os.linesep}"] @pytest.mark.asyncio @pytest.mark.parametrize("fake", [False, True]) async def test_combined_stdout_and_stderr(fp, fake): if fake: fp.register( [PYTHON, "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) else: fp.allow_unregistered(True) process = await asyncio.create_subprocess_exec( PYTHON, "example_script.py", "stderr", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, ) stdout_list = [] stderr_list = [] loop = asyncio.get_event_loop() await asyncio.gather( loop.create_task(_read_stream(process.stdout, stdout_list)), loop.create_task(_read_stream(process.stderr, stderr_list)), loop.create_task(process.wait()), ) # sorted() is necessary here, as the order here may be not deterministic, # and sometimes stderr comes before stdout or the opposite assert sorted(stdout_list) == [ f"Stderr line 1{os.linesep}", f"Stdout line 1{os.linesep}", f"Stdout line 2{os.linesep}", ] assert stderr_list == ["empty"] async def _read_stream(stream: asyncio.StreamReader, output_list): if stream is None: output_list.append("empty") return None while True: line = await stream.readline() if not line: break else: output_list.append(line.decode()) @pytest.mark.asyncio @pytest.mark.parametrize("fake", [False, True]) async def test_input(fp, fake): fp.allow_unregistered(not fake) if fake: def stdin_callable(input): return { "stdout": "Provide an input: Provided: {data}".format( data=input.decode() ) } fp.register( [PYTHON, "example_script.py", "input"], stdout=[b"Stdout line 1", b"Stdout line 2"], stdin_callable=stdin_callable, ) process = await asyncio.create_subprocess_exec( PYTHON, "example_script.py", "input", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, ) out, err = await process.communicate(input=b"test") assert out.splitlines() == [ b"Stdout line 1", b"Stdout line 2", b"Provide an input: Provided: test", ] assert err is None @pytest.mark.asyncio async def test_popen_recorder(fp): recorder = fp.register(["test_script"], occurrences=2) assert recorder.call_count() == 0 await asyncio.create_subprocess_exec("test_script") assert recorder.call_count() == 1 await asyncio.create_subprocess_shell("test_script") assert recorder.call_count() == 2 assert all(isinstance(instance, AsyncFakePopen) for instance in recorder.calls) @pytest.mark.asyncio @pytest.mark.parametrize( "callback", [ pytest.param(None, id="no-callback"), pytest.param( lambda process: process, id="with-callback", ), ], ) async def test_asyncio_subprocess_using_callback(callback, fp): async def my_async_func(): process = await asyncio.create_subprocess_exec( "test", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) await process.wait() return await process.stdout.read() fp.register(["test"], stdout=b"fizz", callback=callback) assert await my_async_func() == b"fizz" @pytest.mark.asyncio async def test_asyncio_subprocess_using_communicate_with_callback_kwargs(fp): expected_some_value = 2 def cbk(fake_obj, some_value=None): assert expected_some_value == some_value return fake_obj async def my_async_func(): process = await asyncio.create_subprocess_exec( "test", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) out, _ = await process.communicate() return out fp.register( ["test"], stdout=b"fizz", callback=cbk, callback_kwargs={"some_value": expected_some_value}, ) assert await my_async_func() == b"fizz" @pytest.mark.asyncio async def test_process_recorder_args(fp): fp.keep_last_process(True) recorder = fp.register(["test_script", fp.any()]) await asyncio.create_subprocess_exec( "test_script", "arg1", env={"foo": "bar"}, ) assert recorder.call_count() == 1 assert recorder.calls[0].args == ["test_script", "arg1"] assert recorder.calls[0].kwargs == {"env": {"foo": "bar"}} @pytest.fixture(autouse=True) def skip_on_pypy(): """Async test for some reason crash on pypy 3.6 on Windows""" if sys.platform == "win32" and sys.version.startswith("3.6"): try: import __pypy__ _ = __pypy__ pytest.skip() except ImportError: pass pytest-subprocess-1.5.3/tests/test_examples.py000066400000000000000000000031631473623114700216420ustar00rootroot00000000000000from pathlib import Path import pytest from docutils.core import publish_doctree ROOT_DIR = Path(__file__).parents[1] def is_code_block(node): return node.tagname == "literal_block" and "code" in node.attributes["classes"] def get_code_blocks(file_path): with file_path.open() as file_handle: content = file_handle.read() code_blocks = publish_doctree(content).findall(condition=is_code_block) return [block.astext() for block in code_blocks] @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() assert result.ret == 0 pytest-subprocess-1.5.3/tests/test_subprocess.py000066400000000000000000001155541473623114700222240ustar00rootroot00000000000000import contextlib import getpass import os import platform import signal import subprocess import sys import time from pathlib import Path import pytest import pytest_subprocess from pytest_subprocess.fake_popen import FakePopen PYTHON = sys.executable path_or_str = pytest.mark.parametrize( "rtype,ptype", [ pytest.param(str, str, id="str"), pytest.param(Path, str, id="path,str"), pytest.param(str, Path, id="str,path"), pytest.param(Path, Path, id="path"), ], ) def setup_fake_popen(monkeypatch): """Set the real Popen to a dummy function that just returns input arguments.""" monkeypatch.setattr( pytest_subprocess.process_dispatcher.ProcessDispatcher, "built_in_popen", lambda command, *args, **kwargs: (command, args, kwargs), ) def test_legacy_usage(fake_process): cmd = ["cmd"] fake_process.register_subprocess(cmd) proc = subprocess.run(cmd, check=True) assert proc.args == cmd assert isinstance(proc.args, type(cmd)) @pytest.mark.parametrize("cmd", [("cmd",), ["cmd"]]) def test_completedprocess_args(fp, cmd): fp.register(cmd) proc = subprocess.run(cmd, check=True) assert proc.args == cmd assert isinstance(proc.args, type(cmd)) @path_or_str def test_completedprocess_args_path(fp, rtype, ptype): fp.register([rtype("cmd")]) if sys.platform.startswith("win") and sys.version_info < (3, 8) and ptype is Path: condition = pytest.raises(TypeError) else: @contextlib.contextmanager def null_context(): yield condition = null_context() with condition: proc = subprocess.run([ptype("cmd")], check=True) assert proc.args == [ptype("cmd")] assert isinstance(proc.args[0], ptype) @pytest.mark.parametrize("cmd", [("cmd"), ["cmd"]]) def test_called_process_error(fp, cmd): fp.register(cmd, returncode=1) with pytest.raises(subprocess.CalledProcessError) as exc_info: subprocess.run(cmd, check=True) assert exc_info.value.cmd == cmd assert isinstance(exc_info.value.cmd, type(cmd)) @pytest.mark.parametrize("cmd", [("cmd"), ["cmd"]]) def test_called_process_error_with_any(fp, cmd): fp.register([fp.any()], returncode=1) with pytest.raises(subprocess.CalledProcessError) as exc_info: subprocess.run(cmd, check=True) assert exc_info.value.cmd == cmd assert isinstance(exc_info.value.cmd, type(cmd)) def test_keep_last_process_error_with_any(fp): fp.register([fp.any()], returncode=1) fp.keep_last_process(True) with pytest.raises(subprocess.CalledProcessError): subprocess.run(["cmd"], check=True) with pytest.raises(subprocess.CalledProcessError): subprocess.run(["cmd2"], check=True) def test_multiple_levels(fp): """Register fake process on different levels and check the behavior""" # process definition on the top level fp.register(("first_command"), stdout="first command top-level") with fp.context() as nested: # lower level, override the same command and define new one nested.register("first_command", stdout="first command lower-level") nested.register("second_command", stdout="second command lower-level") assert ( subprocess.check_output("first_command") == ("first command lower-level").encode() ) assert ( subprocess.check_output("second_command") == ("second command lower-level").encode() ) # first command definition shall be back at top-level definition, and the second # command is no longer defined so it shall raise an exception assert ( subprocess.check_output("first_command") == ("first command top-level").encode() ) with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("second_command") assert str(exc.value) == "The process 'second_command' was not registered." def test_not_registered(fp, monkeypatch): """ Scenario with attempt of running a command that is not registered. First two tries will raise an exception, but the last one will set `process.allow_unregistered(True)` which will allow to execute the process. """ assert fp # this one will use exception from `pytest_subprocess.ProcessNotRegisteredError` # to make sure it still works (for backwards compatibility) with pytest.raises(pytest_subprocess.ProcessNotRegisteredError) as exc: subprocess.Popen("test") assert str(exc.value) == "The process 'test' was not registered." with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.Popen(("test", "with", "args")) assert str(exc.value) == "The process 'test with args' was not registered." fp.allow_unregistered(True) setup_fake_popen(monkeypatch) result = subprocess.Popen("test", shell=True) assert result == ("test", (), {"shell": True}) def test_context(fp, monkeypatch): """Test context manager behavior.""" setup_fake_popen(monkeypatch) with fp.context() as nested: nested.register("test") subprocess.Popen("test") with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.Popen("test") assert str(exc.value) == "The process 'test' was not registered." @pytest.mark.parametrize("fake", [False, True]) @path_or_str def test_basic_process(fp, fake, rtype, ptype): fp.allow_unregistered(not fake) if fake: fp.register( [rtype(PYTHON), "example_script.py"], stdout=["Stdout line 1", "Stdout line 2"], stderr=None, ) if sys.platform.startswith("win") and sys.version_info < (3, 8) and ptype is Path: condition = pytest.raises(TypeError) else: @contextlib.contextmanager def null_context(): yield condition = null_context() with condition: process = subprocess.Popen( [ptype(PYTHON), "example_script.py"], cwd=os.path.dirname(__file__), stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out, err = process.communicate() assert process.poll() == 0 assert process.returncode == 0 assert process.pid > 0 # splitlines is required to ignore differences between LF and CRLF assert out.splitlines() == [b"Stdout line 1", b"Stdout line 2"] assert err == b"" @pytest.mark.parametrize("fake", [False, True]) def test_basic_process_merge_streams(fp, fake): """Stderr is merged into stdout.""" fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "-u", "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) process = subprocess.Popen( [PYTHON, "-u", "example_script.py", "stderr"], cwd=os.path.dirname(__file__), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) out, err = process.communicate() assert out.splitlines() == [ b"Stdout line 1", b"Stdout line 2", b"Stderr line 1", ] assert err is None @pytest.mark.parametrize("fake", [False, True]) def test_wait(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py", "wait", "stderr"], stdout="Stdout line 1\nStdout line 2", stderr="Stderr line 1", wait=0.5, ) process = subprocess.Popen( (PYTHON, "example_script.py", "wait", "stderr"), cwd=os.path.dirname(__file__), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) assert process.returncode is None with pytest.raises(subprocess.TimeoutExpired) as exc: process.wait(timeout=0.1) assert ( str(exc.value).replace("\\\\", "\\") == f"Command '('{PYTHON}', 'example_script.py', 'wait', 'stderr')' " "timed out after 0.1 seconds" ) assert process.wait() == 0 @pytest.mark.parametrize("fake", [False, True]) def test_check_output(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout="Stdout line 1\nStdout line 2", ) process = subprocess.check_output((PYTHON, "example_script.py")) assert process.splitlines() == [b"Stdout line 1", b"Stdout line 2"] @pytest.mark.parametrize("fake", [False, True]) def test_check_call(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout="Stdout line 1\nStdout line 2\n", ) fp.register([PYTHON, "example_script.py", "non-zero"], returncode=1) assert subprocess.check_call((PYTHON, "example_script.py")) == 0 # check also non-zero exit code with pytest.raises(subprocess.CalledProcessError) as exc: assert subprocess.check_call((PYTHON, "example_script.py", "non-zero")) == 1 if sys.version_info >= (3, 6): assert ( str(exc.value).replace("\\\\", "\\") == f"Command '('{PYTHON}', 'example_script.py', 'non-zero')' " "returned non-zero exit status 1." ) @pytest.mark.parametrize("fake", [False, True]) def test_call(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout="Stdout line 1\nStdout line 2\n", ) assert subprocess.call((PYTHON, "example_script.py")) == 0 @pytest.mark.parametrize("fake", [False, True]) @pytest.mark.skipif( sys.version_info <= (3, 5), reason="subprocess.run() was introduced in python3.4", ) def test_run(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout=["Stdout line 1", "Stdout line 2"], ) process = subprocess.run((PYTHON, "example_script.py"), stdout=subprocess.PIPE) assert process.returncode == 0 assert process.stdout == os.linesep.encode().join( [b"Stdout line 1", b"Stdout line 2", b""] ) assert process.stderr is None @pytest.mark.parametrize("fake", [False, True]) def test_universal_newlines(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout=b"Stdout line 1\r\nStdout line 2\r\n", ) process = subprocess.Popen( (PYTHON, "example_script.py"), universal_newlines=True, stdout=subprocess.PIPE ) process.wait() assert process.stdout.read() == "Stdout line 1\nStdout line 2\n" @pytest.mark.parametrize("fake", [False, True]) def test_text(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py"], stdout=[b"Stdout line 1", b"Stdout line 2"], ) if sys.version_info < (3, 7): with pytest.raises(TypeError) as exc: subprocess.Popen( (PYTHON, "example_script.py"), stdout=subprocess.PIPE, text=True ) assert str(exc.value) == "__init__() got an unexpected keyword argument 'text'" else: process = subprocess.Popen( (PYTHON, "example_script.py"), stdout=subprocess.PIPE, text=True ) process.wait() assert process.stdout.read().splitlines() == ["Stdout line 1", "Stdout line 2"] def test_binary(fp): fp.register( ["some-cmd"], stdout=bytes.fromhex("aabbcc"), ) process = subprocess.Popen(["some-cmd"], stdout=subprocess.PIPE) process.wait() assert process.stdout.read() == b"\xaa\xbb\xcc" def test_empty_stdout(fp): fp.register(["some-cmd"], stdout=b"") process = subprocess.Popen(["some-cmd"], stdout=subprocess.PIPE) process.wait() assert process.stdout.read() == b"" def test_empty_stdout_list(fp): fp.register(["some-cmd"], stdout=[]) process = subprocess.Popen(["some-cmd"], stdout=subprocess.PIPE) process.wait() assert process.stdout.read() == b"" @pytest.mark.parametrize("fake", [False, True]) def test_input(fp, fake): fp.allow_unregistered(not fake) if fake: def stdin_callable(input): return { "stdout": "Provide an input: Provided: {data}".format( data=input.decode() ) } fp.register( [PYTHON, "example_script.py", "input"], stdout=[b"Stdout line 1", b"Stdout line 2"], stdin_callable=stdin_callable, ) process = subprocess.Popen( (PYTHON, "example_script.py", "input"), stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) out, err = process.communicate(input=b"test") assert out.splitlines() == [ b"Stdout line 1", b"Stdout line 2", b"Provide an input: Provided: test", ] assert err is None @pytest.mark.skipif( sys.version_info < (3, 7), reason="No need to test since 'text' is available since 3.7", ) @pytest.mark.parametrize("fake", [False, True]) def test_ambiguous_input(fp, fake): fp.allow_unregistered(not fake) if fake: fp.register("test", occurrences=2) with pytest.raises(subprocess.SubprocessError) as exc: subprocess.run("test", universal_newlines=False, text=True) assert str(exc.value) == ( "Cannot disambiguate when both text " "and universal_newlines are supplied but " "different. Pass one or the other." ) with pytest.raises(subprocess.SubprocessError) as exc: subprocess.run("test", universal_newlines=True, text=False) assert str(exc.value) == ( "Cannot disambiguate when both text " "and universal_newlines are supplied but " "different. Pass one or the other." ) @pytest.mark.flaky(reruns=2, condition=platform.python_implementation() == "PyPy") @pytest.mark.parametrize("fake", [False, True]) def test_multiple_wait(fp, fake): """ Wait multiple times for 0.2 seconds with process lasting for 1s. Third wait shall be a bit longer and will not raise an exception, due to exceeding the subprocess runtime. """ fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "example_script.py", "wait"], wait=1, ) 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.9) assert process.returncode == 0 # one more wait shall do no harm process.wait(0.2) def test_wrong_arguments(fp): with pytest.raises(fp.exceptions.IncorrectProcessDefinition) as exc: fp.register("command", wait=1, callback=lambda _: True) assert str(exc.value) == ( "The 'callback' and 'wait' arguments cannot be used " "together. Add sleep() to your callback instead." ) def test_callback(fp, capsys): """ This test will show a usage of the callback argument. The callback argument will have access to the FakePopen so it will change the returncode. One callback execution will also pass a keyword argument. """ def callback(process, argument=None): print("from callback with argument={}".format(argument)) process.returncode = 1 fp.register("test", callback=callback) fp.register("test", callback=callback, callback_kwargs={"argument": "value"}) assert subprocess.call("test") == 1 assert capsys.readouterr().out == "from callback with argument=None\n" assert subprocess.call("test") == 1 assert capsys.readouterr().out == "from callback with argument=value\n" def test_mutiple_occurrences(fp): # register 3 occurrences of the same command at once fp.register("test", occurrences=3) process_1 = subprocess.Popen("test") assert process_1.returncode == 0 process_2 = subprocess.Popen("test") assert process_2.returncode == 0 assert process_2.pid == process_1.pid + 1 process_3 = subprocess.Popen("test") assert process_3.returncode == 0 assert process_3.pid == process_2.pid + 1 # 4-th time shall raise an exception with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." def test_different_output(fp): # register process with output changing each execution fp.register("test", stdout="first execution") fp.register("test", stdout="second execution") # the third execution will return non-zero exit code fp.register("test", stdout="third execution", returncode=1) assert subprocess.check_output("test") == b"first execution" assert subprocess.check_output("test") == b"second execution" third_process = subprocess.Popen("test", stdout=subprocess.PIPE) assert third_process.stdout.read() == b"third execution" assert third_process.returncode == 1 # 4-th time shall raise an exception with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." # now, register two processes once again, but the last one will be kept forever fp.register("test", stdout="first execution") fp.register("test", stdout="second execution") fp.keep_last_process(True) # now the processes can be called forever assert subprocess.check_output("test") == b"first execution" assert subprocess.check_output("test") == b"second execution" assert subprocess.check_output("test") == b"second execution" assert subprocess.check_output("test") == b"second execution" def test_different_output_with_context(fp): """ Leaving one context shall bring back the upper contexts processes even if they were already consumed. This functionality is important to allow a broader-level fixtures that register own processes and keep them predictable. """ fp.register("test", stdout="top-level") with fp.context() as nested: nested.register("test", stdout="nested") assert subprocess.check_output("test") == b"nested" assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." with fp.context() as nested2: # another nest level, the top level shall reappear nested2.register("test", stdout="nested2") assert subprocess.check_output("test") == b"nested2" assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." def test_different_output_with_context_multilevel(fp): """ This is a similar test to the previous one, but here the nesting will be deeper """ fp.register("test", stdout="top-level") with fp.context() as first_level: first_level.register("test", stdout="first-level") with fp.context() as second_level: second_level.register("test", stdout="second-level") assert subprocess.check_output("test") == b"second-level" assert subprocess.check_output("test") == b"first-level" assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call("test") assert subprocess.check_output("test") == b"first-level" assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") def test_multiple_level_early_consuming(fp): """ The top-level will be declared with two ocurrences, but the first one will be consumed before entering the context manager. """ fp.register("test", stdout="top-level", occurrences=2) assert subprocess.check_output("test") == b"top-level" with fp.context(): assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." assert subprocess.check_output("test") == b"top-level" with pytest.raises(fp.exceptions.ProcessNotRegisteredError) as exc: subprocess.check_call("test") assert str(exc.value) == "The process 'test' was not registered." def test_keep_last_process(fp): """ The ProcessNotRegisteredError will never be raised for the process that has been registered at least once. """ fp.keep_last_process(True) fp.register("test", stdout="First run") fp.register("test", stdout="Second run") assert subprocess.check_output("test") == b"First run" assert subprocess.check_output("test") == b"Second run" assert subprocess.check_output("test") == b"Second run" assert subprocess.check_output("test") == b"Second run" def test_git(fp): fp.register(["git", "branch"], stdout=["* fake_branch", " master"]) process = subprocess.Popen( ["git", "branch"], stdout=subprocess.PIPE, universal_newlines=True ) out, _ = process.communicate() assert process.returncode == 0 assert out == "* fake_branch\n master\n" def test_use_real(fp): fp.pass_command([PYTHON, "example_script.py"], occurrences=3) fp.register([PYTHON, "example_script.py"], stdout=["Fake line 1", "Fake line 2"]) for _ in range(0, 3): assert ( subprocess.check_output( [PYTHON, "example_script.py"], universal_newlines=True ) == "Stdout line 1\nStdout line 2\n" ) assert ( subprocess.check_output([PYTHON, "example_script.py"], universal_newlines=True) == "Fake line 1\nFake line 2\n" ) @pytest.mark.skipif(os.name == "nt", reason="Skip on windows") def test_real_process(fp): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # this will fail, as "ls" command is not registered subprocess.call("ls") fp.pass_command("ls") # now it should be fine assert subprocess.call("ls") == 0 # allow all commands to be called by real subprocess fp.allow_unregistered(True) assert subprocess.call(["ls", "-l"]) == 0 def test_context_manager(fp): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): # command not registered, so will raise an exception subprocess.check_call("test") with fp.context() as nested_process: nested_process.register("test", occurrences=3) # now, we can call the command 3 times without error assert subprocess.check_call("test") == 0 assert subprocess.check_call("test") == 0 # the command was called 2 times, so one occurrence left, but since the # context manager has been left, it is not registered anymore with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call("test") def test_raise_exception(fp): def callback_function(process): process.returncode = 1 raise PermissionError("exception raised by subprocess") fp.register(["test"], callback=callback_function) with pytest.raises(PermissionError, match="exception raised by subprocess"): process = subprocess.Popen(["test"]) process.wait() assert process.returncode == 1 def test_callback_with_arguments(fp): def callback_function(process, return_code): process.returncode = return_code return_code = 127 fp.register( ["test"], callback=callback_function, callback_kwargs={"return_code": return_code}, ) process = subprocess.Popen(["test"]) process.wait() assert process.returncode == return_code def test_subprocess_pipe_without_stream_definition(fp): """ From GitHub #17 - the fake_subprocess was crashing if the subprocess was called with stderr=subprocess.PIPE but the stderr was not defined during the process registration. """ fp.register( ["test-no-stderr"], stdout="test", ) fp.register( ["test-no-stdout"], stderr="test", ) fp.register( ["test-no-streams"], ) assert ( subprocess.check_output(["test-no-stderr"], stderr=subprocess.STDOUT).decode() == "test" ) assert ( subprocess.check_output(["test-no-stdout"], stderr=subprocess.STDOUT).decode() == "test" ) assert ( subprocess.check_output(["test-no-streams"], stderr=subprocess.STDOUT).decode() == "" ) @pytest.mark.parametrize("command", (("test",), "test")) def test_different_command_type(fp, command): """ From GitHub #18 - registering process as ["command"] or "command" should make no difference, and none of those command usage attempts shall raise error. """ fp.keep_last_process(True) fp.register(command) assert subprocess.check_call("test") == 0 assert subprocess.check_call(["test"]) == 0 @pytest.mark.parametrize( "command", (("test", "with", "arguments"), "test with arguments") ) def test_different_command_type_complex_command(fp, command): """ Similar to previous test, but the command is more complex. """ fp.keep_last_process(True) fp.register(command) assert subprocess.check_call("test with arguments") == 0 assert subprocess.check_call(["test", "with", "arguments"]) == 0 @pytest.mark.flaky(reruns=2, condition=platform.python_implementation() == "PyPy") def test_raise_exception_check_output(fp): """ From GitHub#16 - the check_output raises the CalledProcessError exception when the exit code is not zero. The exception should not shadow the exception from the callback, if any. For some reason, this test is flaky on PyPy. Further investigation required. """ def callback_function(_): raise FileNotFoundError("raised in callback") fp.register("regular-behavior", returncode=1) fp.register("custom-exception", returncode=1, callback=callback_function) with pytest.raises(subprocess.CalledProcessError): subprocess.check_output("regular-behavior") with pytest.raises(FileNotFoundError, match="raised in callback"): subprocess.check_output("custom-exception") def test_callback_and_return_code(fp): """Regression - the returncode was ignored when callback_function was present.""" def dummy_callback(_): pass def override_returncode(process): process.returncode = 5 return_code = 1 fp.register("test-dummy", returncode=return_code, callback=dummy_callback) process = subprocess.Popen("test-dummy") process.wait() assert process.returncode == return_code fp.register("test-increment", returncode=return_code, callback=override_returncode) process = subprocess.Popen("test-increment") process.wait() assert process.returncode == 5 @pytest.mark.skipif( sys.version_info <= (3, 6), reason="encoding and errors has been introduced in 3.6", ) @pytest.mark.parametrize("argument", ["encoding", "errors"]) @pytest.mark.parametrize("fake", [False, True]) def test_encoding(fp, fake, argument): """If encoding or errors is provided, the `text=True` behavior should be enabled.""" username = getpass.getuser() values = {"encoding": "utf-8", "errors": "strict"} fp.allow_unregistered(not fake) if fake: fp.register(["whoami"], stdout=username) output = subprocess.check_output( ["whoami"], **{argument: values.get(argument)} ).strip() assert isinstance(output, str) assert output.endswith(username) @pytest.mark.parametrize("command", ["ls -lah", ["ls", "-lah"]]) def test_string_or_tuple(fp, command): """ It doesn't matter how you register the command, it should work as string or list. """ fp.register(command, occurrences=2) assert subprocess.check_call("ls -lah") == 0 assert subprocess.check_call(["ls", "-lah"]) == 0 def test_with_wildcards(fp): """Use Any() with real example""" fp.keep_last_process(True) fp.register(("ls", fp.any())) assert subprocess.check_call("ls -lah") == 0 assert subprocess.check_call(["ls", "-lah", "/tmp"]) == 0 assert subprocess.check_call(["ls"]) == 0 fp.register(["cp", fp.any(min=2)]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call("cp /source/dir") assert subprocess.check_call("cp /source/dir /tmp/random-dir") == 0 fp.register(["cd", fp.any(max=1)]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call(["cd ~/ /tmp"]) assert subprocess.check_call("cd ~/") == 0 def test_with_program(fp, monkeypatch): """Use Program() with real example""" fp.keep_last_process(True) fp.register((fp.program("ls"), fp.any())) assert subprocess.check_call("ls -lah") == 0 assert subprocess.check_call(["/ls", "-lah", "/tmp"]) == 0 assert subprocess.check_call(["/usr/bin/ls"]) == 0 with monkeypatch.context(): monkeypatch.setattr(sys, "platform", "win32") monkeypatch.setenv("PATHEXT", ".EXE") assert subprocess.check_call("ls.EXE -lah") == 0 assert subprocess.check_call("ls.exe -lah") == 0 fp.register([fp.program("cp"), fp.any()]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call(["other"]) assert subprocess.check_call("cp /source/dir /tmp/random-dir") == 0 fp.register([fp.program("cd")]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.check_call(["cq"]) assert subprocess.check_call("cd") == 0 def test_call_count(fp): """Check if commands are registered and counted properly""" fp.keep_last_process(True) fp.register([fp.any()]) assert subprocess.check_call("ls -lah") == 0 assert subprocess.check_call(["cp", "/tmp/source", "/source"]) == 0 assert subprocess.check_call(["cp", "/source", "/destination"]) == 0 assert subprocess.check_call(["cp", "/source", "/other/destination"]) == 0 assert "ls -lah" in fp.calls assert ["cp", "/tmp/source", "/source"] in fp.calls assert ["cp", "/source", "/destination"] in fp.calls assert ["cp", "/source", "/other/destination"] in fp.calls assert fp.call_count("cp /tmp/source /source") == 1 assert fp.call_count(["cp", "/source", fp.any()]) == 2 assert fp.call_count(["cp", fp.any()]) == 3 assert fp.call_count(["ls", "-lah"]) == 1 def test_called_process_waits_for_the_callback_to_finish(fp, tmp_path): output_file_path = tmp_path / "output" def callback(process): # simulate a long-running process that creates an output file at the very end time.sleep(1) output_file_path.touch() fp.register([fp.any()], callback=callback) subprocess.run(["ls", "-al"], stdin="abc") assert output_file_path.exists() @pytest.mark.parametrize("method", [FakePopen.wait, FakePopen.communicate]) def test_raises_exceptions_from_callback(fp, method): """Make sure that both .wait() and .communicate() raise exception from callback""" class MyException(Exception): pass def callback(process): raise MyException() fp.register(["test"], callback=callback) proc = subprocess.Popen("test") with pytest.raises(MyException): method(proc) def test_allow_unregistered_cleaning(fp): """ GitHub: #46. The `allow_unregistered()` function should affect only the level where it was applied The setting shouldn't leak to a higher levels or other tests. """ fp.allow_unregistered(False) with fp.context() as context: context.allow_unregistered(True) subprocess.run([PYTHON, "example_script.py"]) subprocess.run([PYTHON, "example_script.py"]) subprocess.run([PYTHON, "example_script.py"]) with fp.context(): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.run([PYTHON, "example_script.py"]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.run(["test"]) def test_keep_last_process_cleaning(fp): """ GitHub: #46. The `keep_last_process()` function should affect only the level where it was applied The setting shouldn't leak to a higher levels or other tests. """ fp.keep_last_process(False) with fp.context() as context: context.keep_last_process(True) context.register(["test"]) subprocess.run(["test"]) subprocess.run(["test"]) subprocess.run(["test"]) with fp.context(): with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.run(["test"]) fp.register(["test"]) subprocess.run(["test"]) with pytest.raises(fp.exceptions.ProcessNotRegisteredError): subprocess.run(["test"]) def test_signals(fp): """Test signal receiving functionality""" fp.register("test") process = subprocess.Popen("test") process.kill() process.terminate() process.send_signal(signal.SIGSEGV) if sys.platform == "win32": expected_signals = (signal.SIGTERM, signal.SIGTERM, signal.SIGSEGV) else: expected_signals = (signal.SIGKILL, signal.SIGTERM, signal.SIGSEGV) assert process.received_signals() == expected_signals def test_signal_callback(fp): """Test that signal callbacks work.""" def callback(process, sig): if sig == signal.SIGTERM: process.returncode = -1 fp.register("test", signal_callback=callback, occurrences=3) # no signal process = subprocess.Popen("test") process.wait() assert process.returncode == 0 # other signal process = subprocess.Popen("test") process.send_signal(signal.SIGSEGV) process.wait() assert process.returncode == 0 # sigterm process = subprocess.Popen("test") process.send_signal(signal.SIGTERM) process.wait() assert process.returncode == -1 @pytest.mark.parametrize("fake", [False, True]) @pytest.mark.parametrize("bytes", [True, False]) def test_non_piped_streams(tmpdir, fp, fake, bytes): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "-u", "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr=["Stderr line 1"], ) stdout_path = tmpdir.join("stdout") stderr_path = tmpdir.join("stderr") mode = "wb" if bytes else "w" with open(stdout_path, mode) as stdout, open(stderr_path, mode) as stderr: process = subprocess.Popen( [PYTHON, "-u", "example_script.py", "stderr"], stdout=stdout, stderr=stderr, ) err, out = process.communicate() assert out is None assert err is None with open(stdout_path, "r") as stdout, open(stderr_path, "r") as stderr: out = stdout.readlines() err = stderr.readlines() assert out == ["Stdout line 1\n", "Stdout line 2\n"] assert err == ["Stderr line 1\n"] @pytest.mark.parametrize("fake", [False, True]) @pytest.mark.parametrize("bytes", [True, False]) def test_non_piped_same_file(tmpdir, fp, fake, bytes): fp.allow_unregistered(not fake) if fake: fp.register( [PYTHON, "-u", "example_script.py", "stderr"], stdout=["Stdout line 1", "Stdout line 2"], stderr="Stderr line 1\n", ) output_path = tmpdir.join("output") mode = "wb" if bytes else "w" with open(output_path, mode) as out_file: process = subprocess.Popen( [PYTHON, "-u", "example_script.py", "stderr"], stdout=out_file, stderr=out_file, ) err, out = process.communicate() assert out is None assert err is None with open(output_path, "r") as out_file: output = out_file.readlines() assert output == ["Stdout line 1\n", "Stdout line 2\n", "Stderr line 1\n"] def test_process_recorder(fp): fp.keep_last_process(True) recorder = fp.register(["test_script", fp.any()]) assert recorder.calls == [] assert recorder.call_count() == 0 assert not recorder.was_called() subprocess.call(("test_script", "random_argument")) assert recorder.call_count() == 1 assert recorder.was_called() assert recorder.was_called(("test_script", "random_argument")) assert not recorder.was_called(("test_script", "another_argument")) subprocess.Popen(["test_script", "another_argument"]) assert recorder.call_count() == 2 assert recorder.was_called(("test_script", "another_argument")) assert recorder.call_count(["test_script", "random_argument"]) == 1 assert recorder.call_count(["test_script", "another_argument"]) == 1 assert recorder.first_call.args == ("test_script", "random_argument") assert recorder.last_call.args == ["test_script", "another_argument"] assert all(isinstance(instance, FakePopen) for instance in recorder.calls) recorder.clear() assert not recorder.was_called() def test_process_recorder_args(fp): fp.keep_last_process(True) recorder = fp.register(["test_script", fp.any()]) subprocess.call(("test_script", "arg1")) subprocess.run(("test_script", "arg2"), env={"foo": "bar"}, cwd="/home/user") subprocess.Popen( ["test_script", "arg3"], env={"foo": "bar1"}, executable="test_script", shell=True, ) assert recorder.call_count() == 3 assert recorder.calls[0].args == ("test_script", "arg1") assert recorder.calls[0].kwargs == {} assert recorder.calls[1].args == ("test_script", "arg2") assert recorder.calls[1].kwargs == {"cwd": "/home/user", "env": {"foo": "bar"}} assert recorder.calls[2].args == ["test_script", "arg3"] assert recorder.calls[2].kwargs == { "env": {"foo": "bar1"}, "executable": "test_script", "shell": True, } def test_fake_popen_is_typed(fp): fp.allow_unregistered(True) fp.register( [PYTHON, "example_script.py"], stdout=b"Stdout line 1\r\nStdout line 2\r\n", ) def spawn_process() -> subprocess.Popen[str]: import subprocess return subprocess.Popen( (PYTHON, "example_script.py"), universal_newlines=True, stdout=subprocess.PIPE, ) proc = spawn_process() proc.wait() assert proc.stdout.read() == "Stdout line 1\nStdout line 2\n" pytest-subprocess-1.5.3/tests/test_typing.py000066400000000000000000000003701473623114700213330ustar00rootroot00000000000000"""Additional target for mypy type checking""" from pytest_subprocess import FakeProcess def test_typing() -> None: fp = FakeProcess() cmd = ["ls", "-l"] output = ["some", "lines", "of", "output"] fp.register(cmd, stdout=output) pytest-subprocess-1.5.3/tests/test_utils.py000066400000000000000000000143331473623114700211650ustar00rootroot00000000000000import pytest from pytest_subprocess.utils import Any from pytest_subprocess.utils import Command def check_match(command_instance, command): assert command_instance == command assert command_instance == tuple(command) assert command_instance == " ".join(command) return True def check_not_match(command_instance, command): assert command_instance != command assert command_instance != tuple(command) assert command_instance != " ".join(command) return True @pytest.mark.parametrize("command", ("whoami", ["whoami"])) def test_simple_command(command): command = Command(command) assert check_match(command, ["whoami"]) @pytest.mark.parametrize("command", ("test command", ("test", "command"))) def test_more_complex_command(command): command = Command(command) assert check_match(command, ["test", "command"]) assert check_not_match(command, ["other", "command"]) def test_simple_wildcards(): command = Command([Any()]) assert check_match(command, ["test"]) assert check_match(command, ["not_test"]) assert check_match(command, ["something", "a", "bit", "longer"]) assert check_match(command, ["basically", "everything", "will", "match"]) command = Command(["test", Any()]) assert check_match(command, ["test", "something"]) assert check_match(command, ["test", "something_else"]) assert check_not_match(command, ["something", "test"]) assert check_match(command, ["test"]) command = Command([Any(), "test"]) assert check_match(command, ["something", "test"]) assert check_match(command, ["something_else", "test"]) assert check_match(command, ["test"]) assert check_not_match(command, ["test", "something"]) command = Command(["test", Any(), "other_test"]) assert check_match(command, ["test", "something", "other_test"]) assert check_match(command, ["test", "something_else", "other_test"]) assert check_match(command, ["test", "other_test"]) assert check_not_match(command, ["test", "something_else"]) def test_more_complex_wildcards(): command = Command(["test", Any()]) assert check_match(command, ["test", "with", "more", "arguments"]) command = Command(["test", Any(), "end"]) assert check_match(command, ["test", "blah", "end"]) assert check_match(command, ["test", "with", "more", "arguments", "end"]) assert check_not_match( command, ["test", "with", "more", "arguments", "invalid_end"] ) def test_any_max(): command = Command([Any(max=1)]) assert check_match(command, ["test"]) assert check_match(command, ["other_test"]) assert check_not_match(command, ["two", "arguments"]) assert check_not_match(command, ["th", "ree", "arguments"]) command = Command(["test", Any(max=3)]) assert check_match(command, ["test", "max", "3", "args"]) assert check_match(command, ["test", "can_be", "less"]) assert check_match(command, ["test"]) assert check_not_match(command, ["wrong", "first", "argument"]) assert check_not_match(command, ["test", "more", "than", "3", "args"]) command = Command(["test", Any(max=2), "end"]) assert check_match(command, ["test", "two", "args", "end"]) assert check_match(command, ["test", "one_arg", "end"]) assert check_not_match(command, ["test", "oops", "three", "args", "end"]) assert check_not_match(command, ["test", "two", "args", "wrong_end"]) command = Command(["test", Any(max=1), "middle", Any(max=2), "end"]) assert check_match( command, ["test", "one_argument", "middle", "two", "args", "end"] ) assert check_match(command, ["test", "middle", "one_arg", "end"]) assert check_not_match( command, ["test", "two", "args", "middle", "two", "args", "end"] ) def test_any_min(): command = Command([Any(min=2)]) assert check_match(command, ["any", "two"]) assert check_match(command, ["or", "even", "three"]) assert check_not_match(command, ["but_not_one"]) command = Command(["test", Any(min=1), "end"]) assert check_match(command, ["test", "with", "more", "arguments", "end"]) assert check_match(command, ["test", "only_one", "end"]) assert check_not_match(command, ["only_one", "end"]) assert check_not_match(command, ["test", "only_one"]) assert check_not_match(command, ["test", "end"]) command = Command(["test", Any(min=1), "middle", Any(min=2), "end"]) assert check_match( command, ["test", "one_argument", "middle", "two", "args", "end"] ) assert check_not_match(command, ["test", "middle", "two", "args", "end"]) assert check_not_match( command, ["test", "one_argument", "middle", "one_argument", "end"] ) def test_min_max_combined(): command = Command([Any(min=2, max=4)]) assert check_match(command, ["just", "two"]) assert check_match(command, ["now", "three", "arguments"]) assert check_match(command, ["up", "to", "even", "four"]) assert check_not_match(command, ["single_argument"]) assert check_not_match(command, ["five", "is", "little", "too", "many"]) command = Command(["start", Any(min=1, max=1), "end"]) assert check_match(command, ["start", "anything", "end"]) assert check_not_match(command, ["start", "end"]) assert check_not_match(command, ["start", "too", "many", "end"]) def test_invalid_instantiation(): with pytest.raises(AttributeError, match="min cannot be greater than max"): Any(min=3, max=2) with pytest.raises( AttributeError, match=r"Cannot use `Any\(\)` one after another." ): Command([Any(), Any()]) with pytest.raises( TypeError, match="Command can be only of type string, list or tuple." ): Command(dict(command="ls")) def test_str_conversions(): no_arguments = Any() assert str(no_arguments) == "Any (min=None, max=None)" min_max = Any(min=1, max=3) assert str(min_max) == "Any (min=1, max=3)" simple_command = Command(["ls", "-lah"]) assert str(simple_command) == "('ls', '-lah')" command_with_any = Command(["ls", Any()]) assert str(command_with_any) == "('ls', Any (min=None, max=None))" def test_command_iter(): """Make sure Command supports iteration""" command = Command(["a", "a", "a"]) assert all(elem == "a" for elem in command)