Skip to content

Commit

Permalink
PIP runner introduction
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
richtja committed Nov 26, 2024
1 parent 7c4af2d commit 76c51fd
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 1 deletion.
84 changes: 84 additions & 0 deletions avocado/plugins/runners/pip.py
Original file line number Diff line number Diff line change
@@ -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()
11 changes: 11 additions & 0 deletions docs/source/guides/user/chapters/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
+++++

Expand Down
1 change: 1 addition & 0 deletions examples/nrunner/recipes/runnable/pip_coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kind": "pip", "kwargs": {"action": "install", "name": "coverage"}}
2 changes: 1 addition & 1 deletion selftests/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions selftests/functional/runner_pip.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
],
Expand Down

0 comments on commit 76c51fd

Please sign in to comment.