Skip to content

Commit f7e4e99

Browse files
committed
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 <[email protected]>
1 parent 7c4af2d commit f7e4e99

File tree

7 files changed

+161
-1
lines changed

7 files changed

+161
-1
lines changed

avocado/plugins/runners/pip.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import sys
2+
import traceback
3+
from multiprocessing import set_start_method
4+
5+
from avocado.core.nrunner.app import BaseRunnerApp
6+
from avocado.core.nrunner.runner import BaseRunner
7+
from avocado.core.utils import messages
8+
from avocado.utils import process
9+
10+
11+
class PipRunner(BaseRunner):
12+
"""Runner for dependencies of type pip
13+
14+
This runner handles, the installation, verification and removal of
15+
packages using the pip.
16+
17+
Runnable attributes usage:
18+
19+
* kind: 'pip'
20+
21+
* uri: not used
22+
23+
* args: not used
24+
25+
* kwargs:
26+
- name: the package name (required)
27+
- action: one of 'install' or 'uninstall' (optional, defaults
28+
to 'install')
29+
"""
30+
31+
name = "pip"
32+
description = "Runner for dependencies of type pip"
33+
34+
def run(self, runnable):
35+
try:
36+
yield messages.StartedMessage.get()
37+
# check if there is a valid 'action' argument
38+
cmd = runnable.kwargs.get("action", "install")
39+
# avoid invalid arguments
40+
if cmd not in ["install", "uninstall"]:
41+
stderr = f"Invalid action {cmd}. Use one of 'install' or 'remove'"
42+
yield messages.StderrMessage.get(stderr.encode())
43+
yield messages.FinishedMessage.get("error")
44+
return
45+
46+
package = runnable.kwargs.get("name")
47+
# if package was passed correctly, run python -m pip
48+
if package is not None:
49+
try:
50+
cmd = f"python3 -m ensurepip && python3 -m pip {cmd} {package}"
51+
result = process.run(cmd, shell=True)
52+
except Exception as e:
53+
yield messages.StderrMessage.get(str(e))
54+
yield messages.FinishedMessage.get("error")
55+
return
56+
57+
yield messages.StdoutMessage.get(result.stdout)
58+
yield messages.StderrMessage.get(result.stderr)
59+
yield messages.FinishedMessage.get("pass")
60+
except Exception as e:
61+
yield messages.StderrMessage.get(traceback.format_exc())
62+
yield messages.FinishedMessage.get(
63+
"error",
64+
fail_reason=str(e),
65+
fail_class=e.__class__.__name__,
66+
traceback=traceback.format_exc(),
67+
)
68+
69+
70+
class RunnerApp(BaseRunnerApp):
71+
PROG_NAME = "avocado-runner-pip"
72+
PROG_DESCRIPTION = "nrunner application for dependencies of type pip"
73+
RUNNABLE_KINDS_CAPABLE = ["pip"]
74+
75+
76+
def main():
77+
if sys.platform == "darwin":
78+
set_start_method("fork")
79+
app = RunnerApp(print)
80+
app.run()
81+
82+
83+
if __name__ == "__main__":
84+
main()

docs/source/guides/user/chapters/dependencies.rst

+11
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@ Following is an example of a test using the Package dependency:
159159

160160
.. literalinclude:: ../../../../../examples/tests/passtest_with_dependency.py
161161

162+
Pip
163+
+++
164+
165+
Support managing python packages via pip. The
166+
parameters available to use the asset `type` of dependencies are:
167+
168+
* `type`: `pip`
169+
* `name`: the package name (required)
170+
* `action`: `install` or `uninstall`
171+
(optional, defaults to `install`)
172+
162173
Asset
163174
+++++
164175

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"kind": "pip", "kwargs": {"action": "install", "name": "coverage"}}

selftests/check.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"nrunner-requirement": 28,
3030
"unit": 678,
3131
"jobs": 11,
32-
"functional-parallel": 314,
32+
"functional-parallel": 317,
3333
"functional-serial": 7,
3434
"optional-plugins": 0,
3535
"optional-plugins-golang": 2,

selftests/functional/resolver.py

+1
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def test_runnables_recipe(self):
250250
exec-test: 3
251251
noop: 3
252252
package: 1
253+
pip: 1
253254
python-unittest: 1
254255
sysinfo: 1"""
255256
cmd_line = f"{AVOCADO} -V list {runnables_recipe_path}"

selftests/functional/runner_pip.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
import sys
3+
import unittest
4+
5+
from avocado.utils import process
6+
from selftests.utils import BASEDIR
7+
8+
RUNNER = f"{sys.executable} -m avocado.plugins.runners.pip"
9+
10+
11+
class RunnableRun(unittest.TestCase):
12+
def test_no_kwargs(self):
13+
res = process.run(f"{RUNNER} runnable-run -k pip", ignore_status=True)
14+
self.assertIn(b"'status': 'started'", res.stdout)
15+
self.assertIn(b"'status': 'finished'", res.stdout)
16+
self.assertIn(b"'time': ", res.stdout)
17+
self.assertEqual(res.exit_status, 0)
18+
19+
@unittest.skipUnless(
20+
os.getenv("CI"),
21+
"This test runs on CI environments"
22+
" only as it depends on the system package manager,"
23+
" and some environments don't have it available.",
24+
)
25+
def test_recipe(self):
26+
recipe = os.path.join(
27+
BASEDIR,
28+
"examples",
29+
"nrunner",
30+
"recipes",
31+
"runnable",
32+
"pip_coverage.json",
33+
)
34+
cmd = f"{RUNNER} runnable-run-recipe {recipe}"
35+
res = process.run(cmd, ignore_status=True)
36+
lines = res.stdout_text.splitlines()
37+
if len(lines) == 1:
38+
first_status = final_status = lines[0]
39+
else:
40+
first_status = lines[0]
41+
final_status = lines[-1]
42+
self.assertIn("'status': 'started'", first_status)
43+
self.assertIn("'time': ", first_status)
44+
self.assertIn("'status': 'finished'", final_status)
45+
self.assertIn("'time': ", final_status)
46+
self.assertEqual(res.exit_status, 0)
47+
48+
49+
class TaskRun(unittest.TestCase):
50+
def test_no_kwargs(self):
51+
res = process.run(
52+
f"{RUNNER} task-run -i XXXreq-pacXXX -k pip", ignore_status=True
53+
)
54+
self.assertIn(b"'status': 'finished'", res.stdout)
55+
self.assertIn(b"'result': 'error'", res.stdout)
56+
self.assertIn(b"'id': 'XXXreq-pacXXX'", res.stdout)
57+
self.assertEqual(res.exit_status, 0)
58+
59+
60+
if __name__ == "__main__":
61+
unittest.main()

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ def run(self):
375375
"avocado-runner-tap = avocado.plugins.runners.tap:main",
376376
"avocado-runner-asset = avocado.plugins.runners.asset:main",
377377
"avocado-runner-package = avocado.plugins.runners.package:main",
378+
"avocado-runner-pip = avocado.plugins.runners.pip:main",
378379
"avocado-runner-podman-image = avocado.plugins.runners.podman_image:main",
379380
"avocado-runner-sysinfo = avocado.plugins.runners.sysinfo:main",
380381
"avocado-software-manager = avocado.utils.software_manager.main:main",
@@ -479,6 +480,7 @@ def run(self):
479480
"python-unittest = avocado.plugins.runners.python_unittest:PythonUnittestRunner",
480481
"asset = avocado.plugins.runners.asset:AssetRunner",
481482
"package = avocado.plugins.runners.package:PackageRunner",
483+
"pip = avocado.plugins.runners.pip:PipRunner",
482484
"podman-image = avocado.plugins.runners.podman_image:PodmanImageRunner",
483485
"sysinfo = avocado.plugins.runners.sysinfo:SysinfoRunner",
484486
],

0 commit comments

Comments
 (0)