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..4424fe8824 --- /dev/null +++ b/selftests/functional/runner_pip.py @@ -0,0 +1,61 @@ +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", ],