From 35f623a996a2d55994a2bc3ca7c5631509253471 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/resolver.py | 1 + selftests/functional/runner_pip.py | 62 ++++++++++++++ setup.py | 2 + 7 files changed, 162 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..4df36353cb 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 packages 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/resolver.py b/selftests/functional/resolver.py index d7c2145293..9c0dd44a96 100644 --- a/selftests/functional/resolver.py +++ b/selftests/functional/resolver.py @@ -250,6 +250,7 @@ def test_runnables_recipe(self): exec-test: 3 noop: 3 package: 1 +pip: 1 python-unittest: 1 sysinfo: 1""" cmd_line = f"{AVOCADO} -V list {runnables_recipe_path}" diff --git a/selftests/functional/runner_pip.py b/selftests/functional/runner_pip.py new file mode 100644 index 0000000000..cbca127f5b --- /dev/null +++ b/selftests/functional/runner_pip.py @@ -0,0 +1,62 @@ +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) + + @unittest.skipUnless( + os.getenv("CI"), + "This test runs on CI environments" + " only as it depends on the system package manager," + " and some environments don't have it available.", + ) + 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", ],