From 76c51fd36fa794d317c36ec8e9348f72bd38492d Mon Sep 17 00:00:00 2001 From: Jan Richter Date: Thu, 21 Nov 2024 14:33:56 +0100 Subject: [PATCH] PIP runner introduction This commit introduces a new dependency runner called `pip`. With this runner, avocado will be able to manipulate with python packages in test environment based on the test dependency configuration. The runner will install pip into the test environment, and then it can call `pip install` or `pip uninstall` commands. For example, this feature can be used for running `coverage.py` inside different environments than process. Signed-off-by: Jan Richter --- avocado/plugins/runners/pip.py | 84 +++++++++++++++++++ .../guides/user/chapters/dependencies.rst | 11 +++ .../recipes/runnable/pip_coverage.json | 1 + selftests/check.py | 2 +- selftests/functional/runner_pip.py | 55 ++++++++++++ setup.py | 2 + 6 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 avocado/plugins/runners/pip.py create mode 100644 examples/nrunner/recipes/runnable/pip_coverage.json create mode 100644 selftests/functional/runner_pip.py diff --git a/avocado/plugins/runners/pip.py b/avocado/plugins/runners/pip.py new file mode 100644 index 0000000000..3c1e511e14 --- /dev/null +++ b/avocado/plugins/runners/pip.py @@ -0,0 +1,84 @@ +import sys +import traceback +from multiprocessing import set_start_method + +from avocado.core.nrunner.app import BaseRunnerApp +from avocado.core.nrunner.runner import BaseRunner +from avocado.core.utils import messages +from avocado.utils import process + + +class PipRunner(BaseRunner): + """Runner for dependencies of type pip + + This runner handles, the installation, verification and removal of + packages using the pip. + + Runnable attributes usage: + + * kind: 'pip' + + * uri: not used + + * args: not used + + * kwargs: + - name: the package name (required) + - action: one of 'install' or 'uninstall' (optional, defaults + to 'install') + """ + + name = "pip" + description = "Runner for dependencies of type pip" + + def run(self, runnable): + try: + yield messages.StartedMessage.get() + # check if there is a valid 'action' argument + cmd = runnable.kwargs.get("action", "install") + # avoid invalid arguments + if cmd not in ["install", "uninstall"]: + stderr = f"Invalid action {cmd}. Use one of 'install' or 'remove'" + yield messages.StderrMessage.get(stderr.encode()) + yield messages.FinishedMessage.get("error") + return + + package = runnable.kwargs.get("name") + # if package was passed correctly, run python -m pip + if package is not None: + try: + cmd = f"python3 -m ensurepip && python3 -m pip {cmd} {package}" + result = process.run(cmd, shell=True) + except Exception as e: + yield messages.StderrMessage.get(str(e)) + yield messages.FinishedMessage.get("error") + return + + yield messages.StdoutMessage.get(result.stdout) + yield messages.StderrMessage.get(result.stderr) + yield messages.FinishedMessage.get("pass") + except Exception as e: + yield messages.StderrMessage.get(traceback.format_exc()) + yield messages.FinishedMessage.get( + "error", + fail_reason=str(e), + fail_class=e.__class__.__name__, + traceback=traceback.format_exc(), + ) + + +class RunnerApp(BaseRunnerApp): + PROG_NAME = "avocado-runner-pip" + PROG_DESCRIPTION = "nrunner application for dependencies of type pip" + RUNNABLE_KINDS_CAPABLE = ["pip"] + + +def main(): + if sys.platform == "darwin": + set_start_method("fork") + app = RunnerApp(print) + app.run() + + +if __name__ == "__main__": + main() diff --git a/docs/source/guides/user/chapters/dependencies.rst b/docs/source/guides/user/chapters/dependencies.rst index 23bfb5b6f0..d25f668cee 100644 --- a/docs/source/guides/user/chapters/dependencies.rst +++ b/docs/source/guides/user/chapters/dependencies.rst @@ -159,6 +159,17 @@ Following is an example of a test using the Package dependency: .. literalinclude:: ../../../../../examples/tests/passtest_with_dependency.py +Pip ++++ + +Support managing python pacages via pip. The +parameters available to use the asset `type` of dependencies are: + + * `type`: `pip` + * `name`: the package name (required) + * `action`: `install` or `uninstall` + (optional, defaults to `install`) + Asset +++++ diff --git a/examples/nrunner/recipes/runnable/pip_coverage.json b/examples/nrunner/recipes/runnable/pip_coverage.json new file mode 100644 index 0000000000..61a8627707 --- /dev/null +++ b/examples/nrunner/recipes/runnable/pip_coverage.json @@ -0,0 +1 @@ +{"kind": "pip", "kwargs": {"action": "install", "name": "coverage"}} diff --git a/selftests/check.py b/selftests/check.py index da86be24ce..068f217d15 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -29,7 +29,7 @@ "nrunner-requirement": 28, "unit": 678, "jobs": 11, - "functional-parallel": 314, + "functional-parallel": 317, "functional-serial": 7, "optional-plugins": 0, "optional-plugins-golang": 2, diff --git a/selftests/functional/runner_pip.py b/selftests/functional/runner_pip.py new file mode 100644 index 0000000000..4d306e0e95 --- /dev/null +++ b/selftests/functional/runner_pip.py @@ -0,0 +1,55 @@ +import os +import sys +import unittest + +from avocado.utils import process +from selftests.utils import BASEDIR + +RUNNER = f"{sys.executable} -m avocado.plugins.runners.pip" + + +class RunnableRun(unittest.TestCase): + def test_no_kwargs(self): + res = process.run(f"{RUNNER} runnable-run -k pip", ignore_status=True) + self.assertIn(b"'status': 'started'", res.stdout) + self.assertIn(b"'status': 'finished'", res.stdout) + self.assertIn(b"'time': ", res.stdout) + self.assertEqual(res.exit_status, 0) + + def test_recipe(self): + recipe = os.path.join( + BASEDIR, + "examples", + "nrunner", + "recipes", + "runnable", + "pip_coverage.json", + ) + cmd = f"{RUNNER} runnable-run-recipe {recipe}" + res = process.run(cmd, ignore_status=True) + lines = res.stdout_text.splitlines() + if len(lines) == 1: + first_status = final_status = lines[0] + else: + first_status = lines[0] + final_status = lines[-1] + self.assertIn("'status': 'started'", first_status) + self.assertIn("'time': ", first_status) + self.assertIn("'status': 'finished'", final_status) + self.assertIn("'time': ", final_status) + self.assertEqual(res.exit_status, 0) + + +class TaskRun(unittest.TestCase): + def test_no_kwargs(self): + res = process.run( + f"{RUNNER} task-run -i XXXreq-pacXXX -k pip", ignore_status=True + ) + self.assertIn(b"'status': 'finished'", res.stdout) + self.assertIn(b"'result': 'error'", res.stdout) + self.assertIn(b"'id': 'XXXreq-pacXXX'", res.stdout) + self.assertEqual(res.exit_status, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/setup.py b/setup.py index 5a915fc095..aa7931ca0c 100755 --- a/setup.py +++ b/setup.py @@ -375,6 +375,7 @@ def run(self): "avocado-runner-tap = avocado.plugins.runners.tap:main", "avocado-runner-asset = avocado.plugins.runners.asset:main", "avocado-runner-package = avocado.plugins.runners.package:main", + "avocado-runner-pip = avocado.plugins.runners.pip:main", "avocado-runner-podman-image = avocado.plugins.runners.podman_image:main", "avocado-runner-sysinfo = avocado.plugins.runners.sysinfo:main", "avocado-software-manager = avocado.utils.software_manager.main:main", @@ -479,6 +480,7 @@ def run(self): "python-unittest = avocado.plugins.runners.python_unittest:PythonUnittestRunner", "asset = avocado.plugins.runners.asset:AssetRunner", "package = avocado.plugins.runners.package:PackageRunner", + "pip = avocado.plugins.runners.pip:PipRunner", "podman-image = avocado.plugins.runners.podman_image:PodmanImageRunner", "sysinfo = avocado.plugins.runners.sysinfo:SysinfoRunner", ],