From 6e25f5423678f33cfaf5005dc0fd31f20476aebb 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 + examples/tests/dependency_pip.py | 11 +++ python-avocado.spec | 1 + selftests/check.py | 7 +- selftests/functional/resolver.py | 1 + selftests/functional/runner_pip.py | 49 +++++++++++ setup.py | 2 + 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 avocado/plugins/runners/pip.py create mode 100644 examples/nrunner/recipes/runnable/pip_coverage.json create mode 100644 examples/tests/dependency_pip.py 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/examples/tests/dependency_pip.py b/examples/tests/dependency_pip.py new file mode 100644 index 0000000000..24f8030056 --- /dev/null +++ b/examples/tests/dependency_pip.py @@ -0,0 +1,11 @@ +from avocado import Test, fail_on + + +class Pip(Test): + """ + :avocado: dependency={"type": "pip", "name": "pip", "action": "install"} + """ + + @fail_on(ImportError) + def test(self): + import pip # pylint: disable=W0611 diff --git a/python-avocado.spec b/python-avocado.spec index e3dfeccce8..6e4c4ee587 100644 --- a/python-avocado.spec +++ b/python-avocado.spec @@ -236,6 +236,7 @@ PATH=%{buildroot}%{_bindir}:%{buildroot}%{_libexecdir}/avocado:$PATH \ %{_bindir}/avocado-runner-tap %{_bindir}/avocado-runner-asset %{_bindir}/avocado-runner-package +%{_bindir}/avocado-runner-pip %{_bindir}/avocado-runner-podman-image %{_bindir}/avocado-runner-sysinfo %{_bindir}/avocado-software-manager diff --git a/selftests/check.py b/selftests/check.py index 9059b120d5..c3d7a433d8 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -25,11 +25,11 @@ "job-api-check-file-exists": 11, "job-api-check-output-file": 4, "job-api-check-tmp-directory-exists": 1, - "nrunner-interface": 70, + "nrunner-interface": 80, "nrunner-requirement": 28, "unit": 682, "jobs": 11, - "functional-parallel": 314, + "functional-parallel": 317, "functional-serial": 7, "optional-plugins": 0, "optional-plugins-golang": 2, @@ -627,6 +627,9 @@ def create_suites(args): # pylint: disable=W0621 { "runner": "avocado-runner-podman-image", }, + { + "runner": "avocado-runner-pip", + }, ], } 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..09aa58f9ea --- /dev/null +++ b/selftests/functional/runner_pip.py @@ -0,0 +1,49 @@ +import os +import sys +import unittest + +from avocado.utils import process +from selftests.utils import AVOCADO, BASEDIR, TestCaseTmpDir + +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) + + +class TaskRun(unittest.TestCase): + def test_no_kwargs(self): + res = process.run(f"{RUNNER} task-run -i pip_1 -k pip", ignore_status=True) + self.assertIn(b"'status': 'finished'", res.stdout) + self.assertIn(b"'result': 'error'", res.stdout) + self.assertIn(b"'id': 'pip_1'", res.stdout) + self.assertEqual(res.exit_status, 0) + + +class PipTest(TestCaseTmpDir): + def test_pip_dependencies(self): + test_path = os.path.join( + BASEDIR, + "examples", + "tests", + "dependency_pip.py", + ) + res = process.run( + f"{AVOCADO} run --job-results-dir {self.tmpdir.name} {test_path}", + ignore_status=True, + ) + self.assertIn( + b"RESULTS : PASS 1 | ERROR 0 | FAIL 0 | SKIP 0 | WARN 0 | INTERRUPT 0 | CANCEL 0", + 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", ],