{"id":18984929,"url":"https://github.com/aklajnert/pytest-subprocess","last_synced_at":"2025-04-08T11:09:46.187Z","repository":{"id":36337054,"uuid":"222118012","full_name":"aklajnert/pytest-subprocess","owner":"aklajnert","description":"Pytest plugin to fake subprocess.","archived":false,"fork":false,"pushed_at":"2025-02-04T05:09:25.000Z","size":318,"stargazers_count":109,"open_issues_count":9,"forks_count":15,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-01T09:36:06.440Z","etag":null,"topics":["pytest","pytest-plugin","python","python3","subprocess"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aklajnert.png","metadata":{"files":{"readme":"README.rst","changelog":"HISTORY.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-11-16T15:21:01.000Z","updated_at":"2025-03-19T02:16:29.000Z","dependencies_parsed_at":"2023-02-15T15:31:48.044Z","dependency_job_id":"9322e719-92d3-439f-b3f6-e541cb5490bd","html_url":"https://github.com/aklajnert/pytest-subprocess","commit_stats":{"total_commits":224,"total_committers":11,"mean_commits":"20.363636363636363","dds":0.25,"last_synced_commit":"038b35d655a0a69010169a92841cdfda244b5275"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aklajnert%2Fpytest-subprocess","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aklajnert%2Fpytest-subprocess/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aklajnert%2Fpytest-subprocess/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aklajnert%2Fpytest-subprocess/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aklajnert","download_url":"https://codeload.github.com/aklajnert/pytest-subprocess/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247829511,"owners_count":21002997,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pytest","pytest-plugin","python","python3","subprocess"],"created_at":"2024-11-08T16:23:45.621Z","updated_at":"2025-04-08T11:09:46.152Z","avatar_url":"https://github.com/aklajnert.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\npytest-subprocess\n=================\n\n.. image:: https://img.shields.io/pypi/v/pytest-subprocess.svg\n    :target: https://pypi.org/project/pytest-subprocess\n    :alt: PyPI version\n\n.. image:: https://img.shields.io/pypi/pyversions/pytest-subprocess.svg\n    :target: https://pypi.org/project/pytest-subprocess\n    :alt: Python versions\n\n.. image:: https://readthedocs.org/projects/pytest-subprocess/badge/?version=latest\n   :target: https://pytest-subprocess.readthedocs.io/en/latest/?badge=latest\n   :alt: Documentation Status\n\nPytest plugin to fake subprocess.\n\n.. contents:: :local:\n\n.. include-start\n\nUsage\n=====\n\nThe plugin adds the ``fake_process`` fixture (and ``fp`` as an alias).\nIt can be used it to register subprocess results so you won't need to rely on\nthe real processes. The plugin hooks on the ``subprocess.Popen()``, which is\nthe base for other subprocess functions. That makes the ``subprocess.run()``,\n``subprocess.call()``, ``subprocess.check_call()`` and\n``subprocess.check_output()`` methods also functional.\n\nInstallation\n------------\n\nYou can install ``pytest-subprocess`` via `pip`_ from `PyPI`_::\n\n    $ pip install pytest-subprocess\n\n\nBasic usage\n-----------\n\nThe most important method is ``fp.register()``\n(or ``register_subprocess`` if you prefer to be more verbose), which\nallows defining the fake processes behavior.\n\n.. code-block:: python\n\n    def test_echo_null_byte(fp):\n        fp.register([\"echo\", \"-ne\", \"\\x00\"], stdout=bytes.fromhex(\"00\"))\n\n        process = subprocess.Popen(\n            [\"echo\", \"-ne\", \"\\x00\"],\n            stdout=subprocess.PIPE,\n        )\n        out, _ = process.communicate()\n\n        assert process.returncode == 0\n        assert out == b\"\\x00\"\n\nOptionally, the ``stdout`` and ``stderr`` parameters can be a list (or tuple)\nof lines to be joined together with a trailing ``os.linesep`` on each line.\n\n.. code-block:: python\n\n    def test_git(fp):\n        fp.register([\"git\", \"branch\"], stdout=[\"* fake_branch\", \"  master\"])\n\n        process = subprocess.Popen(\n            [\"git\", \"branch\"],\n            stdout=subprocess.PIPE,\n            universal_newlines=True,\n        )\n        out, _ = process.communicate()\n\n        assert process.returncode == 0\n        assert out == \"* fake_branch\\n  master\\n\"\n\nPassing input\n-------------\n\nBy default, if you use ``input`` argument to the ``Popen.communicate()``\nmethod, it won't crash, but also won't do anything useful. By passing\na function as ``stdin_callable`` argument for the\n``fp.register()`` method you can specify the behavior\nbased on the input. The function shall accept one argument, which will be\nthe input data. If the function will return a dictionary with ``stdout`` or\n``stderr`` keys, its value will be appended to according stream.\n\n.. code-block:: python\n\n    def test_pass_input(fp):\n        def stdin_function(input):\n            return {\n                \"stdout\": \"This input was added: {data}\".format(\n                    data=input.decode()\n                )\n            }\n\n        fp.register(\n            [\"command\"],\n            stdout=[b\"Just stdout\"],\n            stdin_callable=stdin_function,\n        )\n\n        process = subprocess.Popen(\n            [\"command\"],\n            stdin=subprocess.PIPE,\n            stdout=subprocess.PIPE,\n        )\n        out, _ = process.communicate(input=b\"sample input\\n\")\n\n        assert out.splitlines() == [\n            b\"Just stdout\",\n            b\"This input was added: sample input\",\n        ]\n\nUnregistered commands\n---------------------\n\nBy default, when the ``fp`` fixture is being used, any attempt to\nrun subprocess that has not been registered will raise\nthe ``ProcessNotRegisteredError`` exception. To allow it, use\n``fp.allow_unregistered(True)``, which will execute all unregistered\nprocesses with real ``subprocess``, or use\n``fp.pass_command(\"command\")`` to allow just a single command.\n\n.. code-block:: python\n\n    def test_real_process(fp):\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            # this will fail, as \"ls\" command is not registered\n            subprocess.call(\"ls\")\n\n        fp.pass_command(\"ls\")\n        # now it should be fine\n        assert subprocess.call(\"ls\") == 0\n\n        # allow all commands to be called by real subprocess\n        fp.allow_unregistered(True)\n        assert subprocess.call([\"ls\", \"-l\"]) == 0\n\n\nDiffering results\n-----------------\n\nEach ``register()`` or ``pass_command()`` method call will register\nonly one command execution. You can call those methods multiple times, to\nchange the faked output on each subprocess run. When you call subprocess more\nwill be raised. To prevent that, call ``fp.keep_last_process(True)``,\nwhich will keep the last registered process forever.\n\n.. code-block:: python\n\n    def test_different_output(fp):\n        # register process with output changing each execution\n        fp.register(\"test\", stdout=\"first execution\")\n        # the second execution will return non-zero exit code\n        fp.register(\"test\", stdout=\"second execution\", returncode=1)\n\n        assert subprocess.check_output(\"test\") == b\"first execution\"\n        second_process = subprocess.run(\"test\", stdout=subprocess.PIPE)\n        assert second_process.stdout == b\"second execution\"\n        assert second_process.returncode == 1\n\n        # 3rd time shall raise an exception\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            subprocess.check_call(\"test\")\n\n        # now, register two processes once again,\n        # but the last one will be kept forever\n        fp.register(\"test\", stdout=\"first execution\")\n        fp.register(\"test\", stdout=\"second execution\")\n        fp.keep_last_process(True)\n\n        # now the processes can be called forever\n        assert subprocess.check_output(\"test\") == b\"first execution\"\n        assert subprocess.check_output(\"test\") == b\"second execution\"\n        assert subprocess.check_output(\"test\") == b\"second execution\"\n        assert subprocess.check_output(\"test\") == b\"second execution\"\n\n\nUsing callbacks\n---------------\n\nYou can pass a function as ``callback`` argument to the ``register()``\nmethod which will be executed instead of the real subprocess. The callback function\ncan raise exceptions which will be interpreted in tests as an exception raised\nby the subprocess. The fixture will pass ``FakePopen`` class instance into the\ncallback function, that can be used to change the return code or modify output\nstreams.\n\n.. code-block:: python\n\n    def callback_function(process):\n        process.returncode = 1\n        raise PermissionError(\"exception raised by subprocess\")\n\n\n    def test_raise_exception(fp):\n        fp.register([\"test\"], callback=callback_function)\n\n        with pytest.raises(\n            PermissionError, match=\"exception raised by subprocess\"\n        ):\n            process = subprocess.Popen([\"test\"])\n            process.wait()\n\n        assert process.returncode == 1\n\nIt is possible to pass additional keyword arguments into ``callback`` by using\nthe ``callback_kwargs`` argument:\n\n.. code-block:: python\n\n    def callback_function_with_kwargs(process, return_code):\n        process.returncode = return_code\n\n\n    def test_callback_with_arguments(fp):\n        return_code = 127\n\n        fp.register(\n            [\"test\"],\n            callback=callback_function_with_kwargs,\n            callback_kwargs={\"return_code\": return_code},\n        )\n\n        process = subprocess.Popen([\"test\"])\n        process.wait()\n\n        assert process.returncode == return_code\n\nAs a context manager\n--------------------\n\nThe ``fp`` fixture provides ``context()`` method that allows us to\nuse it as a context manager. It can be used to limit the scope when a certain\ncommand is allowed, e.g. to make sure that the code doesn't want to execute\nit somewhere else.\n\n.. code-block:: python\n\n    def test_context_manager(fp):\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            # command not registered, so will raise an exception\n            subprocess.check_call(\"test\")\n\n        with fp.context() as nested_process:\n            nested_process.register(\"test\", occurrences=3)\n            # now, we can call the command 3 times without error\n            assert subprocess.check_call(\"test\") == 0\n            assert subprocess.check_call(\"test\") == 0\n\n        # the command was called 2 times, so one occurrence left, but since the\n        # context manager has been left, it is not registered anymore\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            subprocess.check_call(\"test\")\n\nNon-exact command matching\n--------------------------\n\nIf you need to catch a command with some non-predictable elements, like a path\nto a randomly-generated file name, you can use ``fake_subprocess.any()`` for\nthat purpose. The number of arguments that should be matched can be controlled\nby ``min`` and ``max`` arguments. To use ``fake_subprocess.any()`` you need\nto define the command as a ``tuple`` or ``list``. The matching will work even\nif the subprocess command will be called with a string argument.\n\n.. code-block:: python\n\n    def test_non_exact_matching(fp):\n        # define a command that will take any number of arguments\n        fp.register([\"ls\", fp.any()])\n        assert subprocess.check_call(\"ls -lah\") == 0\n\n        # `fake_subprocess.any()` is OK even with no arguments\n        fp.register([\"ls\", fp.any()])\n        assert subprocess.check_call(\"ls\") == 0\n\n        # but it can force a minimum amount of arguments\n        fp.register([\"cp\", fp.any(min=2)])\n\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            # only one argument is used, so registered command won't match\n            subprocess.check_call(\"cp /source/dir\")\n        # but two arguments will be fine\n        assert subprocess.check_call(\"cp /source/dir /tmp/random-dir\") == 0\n\n        # the `max` argument can be used to limit maximum amount of arguments\n        fp.register([\"cd\", fp.any(max=1)])\n\n        with pytest.raises(fp.exceptions.ProcessNotRegisteredError):\n            # cd with two arguments won't match with max=1\n            subprocess.check_call(\"cd ~/ /tmp\")\n        # but any single argument is fine\n        assert subprocess.check_call(\"cd ~/\") == 0\n\n        # `min` and `max` can be used together\n        fp.register([\"my_app\", fp.any(min=1, max=2)])\n        assert subprocess.check_call([\"my_app\", \"--help\"]) == 0\n\n\nYou can also specify just the command name, and have it match any command with\nthe same name, regardless of the location. This is accomplished with\n``fake_subprocess.program(\"name\")``.\n\n.. code-block:: python\n\n    def test_any_matching_program(fp):\n        # define a command that can come from anywhere\n        fp.register([fp.program(\"ls\")])\n        assert subprocess.check_call(\"/bin/ls\") == 0\n\n\nCheck if process was called\n---------------------------\n\nYou may want to simply check if a certain command was called, you can do this\nby accessing ``fp.calls``, where all commands are stored as-called.\nYou can also use a utility function ``fp.call_count()`` to see\nhow many a command has been called. The latter supports ``fp.any()``.\n\n.. code-block:: python\n\n    def test_check_if_called(fp):\n        fp.keep_last_process(True)\n        # any command can be called\n        fp.register([fp.any()])\n\n        subprocess.check_call([\"cp\", \"/tmp/source\", \"/source\"])\n        subprocess.check_call([\"cp\", \"/source\", \"/destination\"])\n        subprocess.check_call([\"cp\", \"/source\", \"/other/destination\"])\n\n        # you can check if command is in ``fp.calls``\n        assert [\"cp\", \"/tmp/source\", \"/source\"] in fp.calls\n        assert [\"cp\", \"/source\", \"/destination\"] in fp.calls\n        assert [\"cp\", \"/source\", \"/other/destination\"] in fp.calls\n\n        # or check how many it was called, possibly with wildcard arguments\n        assert fp.call_count([\"cp\", \"/source\", \"/destination\"]) == 1\n\n        # with ``call_count()`` you don't need to use the same type as\n        # the subprocess was called\n        assert fp.call_count(\"cp /tmp/source /source\") == 1\n\n        # can be used with ``fp.any()`` to match more calls\n        assert fp.call_count([\"cp\", fp.any()]) == 3\n\n\nCheck Popen arguments\n---------------------\n\nYou can use the recorded calls functionality to introspect the keyword\narguments that were passed to `Popen`.\n\n.. code-block:: python\n\n    def test_process_recorder_kwargs(fp):\n        fp.keep_last_process(True)\n        recorder = fp.register([\"test_script\", fp.any()])\n\n        subprocess.run(\n            (\"test_script\", \"arg1\"), env={\"foo\": \"bar\"}, cwd=\"/home/user\"\n        )\n        subprocess.Popen(\n            [\"test_script\", \"arg2\"],\n            env={\"foo\": \"bar1\"},\n            executable=\"test_script\",\n            shell=True,\n        )\n\n        assert recorder.calls[0].args == (\"test_script\", \"arg1\")\n        assert recorder.calls[0].kwargs == {\n            \"cwd\": \"/home/user\",\n            \"env\": {\"foo\": \"bar\"},\n        }\n        assert recorder.calls[1].args == [\"test_script\", \"arg2\"]\n        assert recorder.calls[1].kwargs == {\n            \"env\": {\"foo\": \"bar1\"},\n            \"executable\": \"test_script\",\n            \"shell\": True,\n        }\n\nHandling signals\n----------------\n\nYou can use standard ``kill()``, ``terminate()`` or ``send_signal()`` methods\nin ``Popen`` instances. There is an additional ``received_signals()`` method\nto get a tuple of all signals received by the process. It is also possible to\nset up an optional callback function for signals.\n\n.. code-block:: python\n\n    import signal\n\n\n    def test_signal_callback(fp):\n        \"\"\"Test that signal callbacks work.\"\"\"\n\n        def callback(process, sig):\n            if sig == signal.SIGTERM:\n                process.returncode = -1\n\n        # the `register()` method returns a ProgressRecorder object, where\n        # all future matching `Popen()` instances will be appended\n        process_recorder = fp.register(\"test\", signal_callback=callback)\n\n        process = subprocess.Popen(\"test\")\n        process.send_signal(signal.SIGTERM)\n        process.wait()\n\n        assert process.returncode == -1\n        assert process.received_signals() == (signal.SIGTERM,)\n\n        # the instance appended to `register()` output is the `Popen` instance\n        # created later\n        assert process_recorder.first_call is process\n\n\nAsyncio support\n---------------\n\nThe plugin now supports asyncio and works for ``asyncio.create_subprocess_shell``\nand ``asyncio.create_subprocess_exec``:\n\n.. code-block:: python\n\n    @pytest.mark.asyncio\n    async def test_basic_usage(\n        fp,\n    ):\n        fp.register(\n            [\"some-command-that-is-definitely-unavailable\"], returncode=500\n        )\n\n        process = await asyncio.create_subprocess_shell(\n            \"some-command-that-is-definitely-unavailable\"\n        )\n        returncode = await process.wait()\n\n        assert process.returncode == returncode\n        assert process.returncode == 500\n\n.. _`pip`: https://pypi.org/project/pip/\n.. _`PyPI`: https://pypi.org/project\n\n\n.. include-end\n\nDocumentation\n-------------\n\nFor full documentation, including API reference, please see https://pytest-subprocess.readthedocs.io/en/latest/.\n\nContributing\n------------\nContributions are very welcome. Tests can be run with `tox`_, please ensure\nthe coverage at least stays the same before you submit a pull request.\n\nLicense\n-------\n\nDistributed under the terms of the `MIT`_ license, \"pytest-subprocess\" is free and open source software\n\n\nIssues\n------\n\nIf you encounter any problems, please `file an issue`_ along with a detailed description.\n\n----\n\nThis `pytest`_ plugin was generated with `Cookiecutter`_ along with `@hackebrot`_'s `cookiecutter-pytest-plugin`_ template.\n\n.. _`Cookiecutter`: https://github.com/audreyr/cookiecutter\n.. _`@hackebrot`: https://github.com/hackebrot\n.. _`MIT`: http://opensource.org/licenses/MIT\n.. _`BSD-3`: http://opensource.org/licenses/BSD-3-Clause\n.. _`GNU GPL v3.0`: http://www.gnu.org/licenses/gpl-3.0.txt\n.. _`Apache Software License 2.0`: http://www.apache.org/licenses/LICENSE-2.0\n.. _`cookiecutter-pytest-plugin`: https://github.com/pytest-dev/cookiecutter-pytest-plugin\n.. _`file an issue`: https://github.com/aklajnert/pytest-subprocess/issues\n.. _`pytest`: https://github.com/pytest-dev/pytest\n.. _`tox`: https://tox.readthedocs.io/en/latest/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faklajnert%2Fpytest-subprocess","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faklajnert%2Fpytest-subprocess","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faklajnert%2Fpytest-subprocess/lists"}