Skip to content

Commit a24c5ff

Browse files
committed
Merge branch 'resolver_exec'
Signed-off-by: Cleber Rosa <[email protected]>
2 parents fc470ad + 978e409 commit a24c5ff

File tree

10 files changed

+316
-58
lines changed

10 files changed

+316
-58
lines changed

avocado/plugins/list.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,21 @@ def configure(self, parser):
214214
allow_multiple=True,
215215
)
216216

217+
settings.add_argparser_to_option(
218+
namespace="resolver.run_executables",
219+
parser=parser,
220+
long_arg="--resolver-run-executables",
221+
allow_multiple=True,
222+
)
223+
224+
settings.add_argparser_to_option(
225+
namespace="resolver.exec_runnables_recipe.arguments",
226+
metavar="ARGS",
227+
parser=parser,
228+
long_arg="--resolver-exec-arguments",
229+
allow_multiple=True,
230+
)
231+
217232
help_msg = "Writes runnable recipe files to a directory."
218233
settings.register_option(
219234
section="list.recipes",

avocado/plugins/resolvers.py

Lines changed: 122 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
import json
2020
import os
2121
import re
22+
import shlex
23+
import subprocess
2224

2325
from avocado.core.extension_manager import PluginPriority
2426
from avocado.core.nrunner.runnable import Runnable
25-
from avocado.core.plugin_interfaces import Resolver
27+
from avocado.core.plugin_interfaces import Init, Resolver
2628
from avocado.core.references import reference_split
2729
from avocado.core.resolver import (
2830
ReferenceResolution,
@@ -31,16 +33,12 @@
3133
get_file_assets,
3234
)
3335
from avocado.core.safeloader import find_avocado_tests, find_python_unittests
36+
from avocado.core.settings import settings
3437

3538

36-
class ExecTestResolver(Resolver):
37-
38-
name = "exec-test"
39-
description = "Test resolver for executable files to be handled as tests"
40-
priority = PluginPriority.VERY_LOW
41-
42-
def resolve(self, reference):
43-
39+
class BaseExec:
40+
@staticmethod
41+
def check_exec(reference):
4442
criteria_check = check_file(
4543
reference,
4644
reference,
@@ -52,6 +50,18 @@ def resolve(self, reference):
5250
if criteria_check is not True:
5351
return criteria_check
5452

53+
54+
class ExecTestResolver(BaseExec, Resolver):
55+
56+
name = "exec-test"
57+
description = "Test resolver for executable files to be handled as tests"
58+
priority = PluginPriority.VERY_LOW
59+
60+
def resolve(self, reference):
61+
exec_criteria = self.check_exec(reference)
62+
if exec_criteria is not None:
63+
return exec_criteria
64+
5565
runnable = Runnable("exec-test", reference, assets=get_file_assets(reference))
5666
return ReferenceResolution(
5767
reference, ReferenceResolutionResult.SUCCESS, [runnable]
@@ -121,24 +131,16 @@ def resolve(self, reference):
121131
)
122132

123133

124-
class TapResolver(Resolver):
134+
class TapResolver(BaseExec, Resolver):
125135

126136
name = "tap"
127137
description = "Test resolver for executable files to be handled as TAP tests"
128138
priority = PluginPriority.LAST_RESORT
129139

130140
def resolve(self, reference):
131-
132-
criteria_check = check_file(
133-
reference,
134-
reference,
135-
suffix=None,
136-
type_name="executable file",
137-
access_check=os.R_OK | os.X_OK,
138-
access_name="executable",
139-
)
140-
if criteria_check is not True:
141-
return criteria_check
141+
exec_criteria = self.check_exec(reference)
142+
if exec_criteria is not None:
143+
return exec_criteria
142144

143145
runnable = Runnable("tap", reference, assets=get_file_assets(reference))
144146
return ReferenceResolution(
@@ -196,3 +198,102 @@ def resolve(self, reference):
196198
return criteria_check
197199

198200
return self._validate_and_load_runnables(reference)
201+
202+
203+
class ExecRunnablesRecipeInit(Init):
204+
name = "exec-runnables-recipe"
205+
description = 'Configuration for resolver plugin "exec-runnables-recipe" plugin'
206+
207+
def initialize(self):
208+
help_msg = (
209+
'Whether resolvers (such as "exec-runnables-recipe") should '
210+
"execute files given as test references that have executable "
211+
"permissions. This is disabled by default due to security "
212+
"implications of running executables that may not be trusted."
213+
)
214+
settings.register_option(
215+
section="resolver",
216+
key="run_executables",
217+
key_type=bool,
218+
default=False,
219+
help_msg=help_msg,
220+
)
221+
222+
help_msg = (
223+
"Command line options (space separated) that will be added "
224+
"to the executable when executing it as a producer of "
225+
"runnables-recipe JSON content."
226+
)
227+
settings.register_option(
228+
section="resolver.exec_runnables_recipe",
229+
key="arguments",
230+
key_type=str,
231+
default="",
232+
help_msg=help_msg,
233+
)
234+
235+
236+
class ExecRunnablesRecipeResolver(BaseExec, Resolver):
237+
name = "exec-runnables-recipe"
238+
description = "Test resolver for executables that output JSON runnable recipes"
239+
priority = PluginPriority.LOW
240+
241+
def resolve(self, reference):
242+
if not self.config.get("resolver.run_executables"):
243+
return ReferenceResolution(
244+
reference,
245+
ReferenceResolutionResult.NOTFOUND,
246+
info=(
247+
"Running executables is not enabled. Refer to "
248+
'"resolver.run_executables" configuration option'
249+
),
250+
)
251+
252+
exec_criteria = self.check_exec(reference)
253+
if exec_criteria is not None:
254+
return exec_criteria
255+
256+
args = self.config.get("resolver.exec_runnables_recipe.arguments")
257+
if args:
258+
cmd = [reference] + shlex.split(args)
259+
else:
260+
cmd = reference
261+
try:
262+
process = subprocess.Popen(
263+
cmd,
264+
stdin=subprocess.DEVNULL,
265+
stdout=subprocess.PIPE,
266+
stderr=subprocess.PIPE,
267+
)
268+
except (FileNotFoundError, PermissionError) as exc:
269+
return ReferenceResolution(
270+
reference,
271+
ReferenceResolutionResult.NOTFOUND,
272+
info=(f'Failure while running running executable "{reference}": {exc}'),
273+
)
274+
275+
content, _ = process.communicate()
276+
try:
277+
runnables = json.loads(content)
278+
except json.JSONDecodeError:
279+
return ReferenceResolution(
280+
reference,
281+
ReferenceResolutionResult.NOTFOUND,
282+
info=f'Content generated by running executable "{reference}" is not JSON',
283+
)
284+
285+
if not (
286+
isinstance(runnables, list)
287+
and all([isinstance(r, dict) for r in runnables])
288+
):
289+
return ReferenceResolution(
290+
reference,
291+
ReferenceResolutionResult.NOTFOUND,
292+
info=f"Content generated by running executable {reference} does not look like a runnables recipe JSON content",
293+
)
294+
295+
return ReferenceResolution(
296+
reference,
297+
ReferenceResolutionResult.SUCCESS,
298+
[Runnable.from_dict(r) for r in runnables],
299+
)

avocado/plugins/run.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,21 @@ def configure(self, parser):
267267
long_arg="--log-test-data-directories",
268268
)
269269

270+
settings.add_argparser_to_option(
271+
namespace="resolver.run_executables",
272+
parser=parser,
273+
long_arg="--resolver-run-executables",
274+
allow_multiple=True,
275+
)
276+
277+
settings.add_argparser_to_option(
278+
namespace="resolver.exec_runnables_recipe.arguments",
279+
metavar="ARGS",
280+
parser=parser,
281+
long_arg="--resolver-exec-arguments",
282+
allow_multiple=True,
283+
)
284+
270285
parser_common_args.add_tag_filter_args(parser)
271286

272287
def run(self, config):

docs/source/guides/writer/chapters/recipes.rst

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,93 @@ That will be parsed by the ``runnables-recipe`` resolver, like in
7676

7777
exec-test /bin/true
7878
exec-test /bin/false
79+
80+
Using dynamically generated recipes
81+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82+
83+
The ``exec-runnables-recipe`` resolver allows a user to point to a
84+
file that will be executed, and that is expected to generate (on its
85+
``STDOUT``) content compatible with the Runnable recipe format
86+
mentioned previously.
87+
88+
For security reasons, Avocado won't execute files indiscriminately
89+
when looking for tests (at the resolution phase). One must set the
90+
``--resolver-run-executables`` command line option (or the underlying
91+
``resolver.run_executables`` configuration option) to allow running
92+
executables at the resolver stage.
93+
94+
.. warning:: It's the user's responsibility to give test references
95+
(to be resolved and thus executed) that are well behaved
96+
in the sense that they will finish executing quickly,
97+
won't execute unintended code (such as running tests),
98+
won't destroy data, etc.
99+
100+
A script such as:
101+
102+
.. literalinclude:: ../../../../../examples/nrunner/resolvers/exec_runnables_recipe.sh
103+
104+
Will output JSON that is compatible with the runnable recipe format.
105+
That can be used directly via either ``avocado list`` or ``avocado
106+
run``. Example::
107+
108+
$ avocado list --resolver-run-executables examples/nrunner/resolvers/exec_runnables_recipe.sh
109+
110+
exec-test true-test
111+
exec-test false-test
112+
113+
If the executable to be run needs arguments, you can pass it via the
114+
``--resolver-exec-arguments`` or the underlying
115+
``resolver.exec_runnable_recipe.arguments`` option. The following
116+
script receives an optional parameter that can change the type of the
117+
tests it generates:
118+
119+
.. literalinclude:: ../../../../../examples/nrunner/resolvers/exec_runnables_recipe_kind.sh
120+
121+
In order to have those tests resolved as ``tap`` tests, one can run::
122+
123+
$ avocado list --resolver-run-executables --resolver-exec-arguments tap examples/nrunner/resolvers/exec_runnables_recipe_kind.sh
124+
125+
tap true-test
126+
tap false-test
127+
128+
Behavior of ``exec-runnables-recipe`` and ``exec-test`` resolvers
129+
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
130+
131+
The ``exec-runnables-recipe`` resolver has a higher priority than
132+
(that is, it runs before) the ``exec-test`` resolver. That means that
133+
if, and only if, a user enables the feature itself (by means of the
134+
``--resolver-run-executables`` command line option or the underlying
135+
``resolver.run_executables`` configuration option), it
136+
``exec-runnables-recipe`` will perform any meaningful action.
137+
138+
Even if the ``exec-runnables-recipe`` is activated (through the
139+
command line or configuration option mentioned before), it may still
140+
coexist with ``exec-test`` resolver, example::
141+
142+
$ avocado list --resolver-run-executables examples/nrunner/resolvers/exec_runnables_recipe.sh /bin/uname
143+
144+
exec-test true-test
145+
exec-test false-test
146+
exec-test /bin/uname
147+
148+
The reason (that can be seen with ``avocado -V list ...``) for that is
149+
the ``exec-runnables-recipe`` returns a "not found" resolution with
150+
the message::
151+
152+
Resolver Reference Info
153+
...
154+
exec-runnables-recipe /bin/uname Content generated by running executable "/bin/uname" is not JSON
155+
156+
.. warning:: Even though it's possible to have ``exec-test`` and
157+
``exec-runnable-recipes`` in the same Avocado test suite
158+
(for instance in an ``avocado run`` command execution)
159+
it's not recommended on most cases because ``exec-tests``
160+
will end up being run at the test resolution phase
161+
in addition to the test execution phase. It's
162+
recommended to use multiple ``avocado run``
163+
commands or use the Job API and multiple
164+
:class:`avocado.core.suite.TestSuite`, one for
165+
``exec-runnable-recipes`` with the
166+
``resolver.run_executables`` options enabled, and
167+
another for ``exec-tests`` with that option in its
168+
default state (disabled).
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh
2+
echo '[{"kind": "exec-test","uri": "/bin/true","identifier": "true-test"},{"kind": "exec-test","uri": "/bin/false","identifier": "false-test"}]'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
kind=${1:-exec-test}
3+
echo "[{\"kind\": \"$kind\",\"uri\": \"/bin/true\",\"identifier\": \"true-test\"},{\"kind\": \"$kind\",\"uri\": \"/bin/false\",\"identifier\": \"false-test\"}]"

selftests/.data/whiteboard.py

Lines changed: 0 additions & 35 deletions
This file was deleted.

selftests/check.py

Lines changed: 1 addition & 1 deletion
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": 309,
32+
"functional-parallel": 312,
3333
"functional-serial": 7,
3434
"optional-plugins": 0,
3535
"optional-plugins-golang": 2,

0 commit comments

Comments
 (0)