1919import json
2020import os
2121import re
22+ import shlex
23+ import subprocess
2224
2325from avocado .core .extension_manager import PluginPriority
2426from avocado .core .nrunner .runnable import Runnable
25- from avocado .core .plugin_interfaces import Resolver
27+ from avocado .core .plugin_interfaces import Init , Resolver
2628from avocado .core .references import reference_split
2729from avocado .core .resolver import (
2830 ReferenceResolution ,
3133 get_file_assets ,
3234)
3335from 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+ )
0 commit comments