From e3eecd82cf7ff8ec96ae5a98b251e493ccced258 Mon Sep 17 00:00:00 2001 From: Joaquin Campero Date: Sat, 28 Oct 2023 16:54:38 +0200 Subject: [PATCH] feat(execute): arg replacement --- .idea/.gitignore | 1 + hexagon/domain/tool/__init__.py | 3 +- hexagon/runtime/execute/action.py | 51 ++++++++++++++++++--------- hexagon/runtime/execute/tool.py | 1 + hexagon/support/input/args.py | 15 ++++++++ tests_e2e/__specs/execute_tool_2.py | 53 +++++++++++++++++++++++++++++ tests_e2e/execute_tool_2/app.yml | 33 ++++++++++++++++++ 7 files changed, 140 insertions(+), 17 deletions(-) diff --git a/.idea/.gitignore b/.idea/.gitignore index d654e232..ef501fa9 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -7,3 +7,4 @@ # Editor-based HTTP Client requests /httpRequests/ /inspectionProfiles/ +appmap.xml \ No newline at end of file diff --git a/hexagon/domain/tool/__init__.py b/hexagon/domain/tool/__init__.py index 4720cf13..92d21980 100644 --- a/hexagon/domain/tool/__init__.py +++ b/hexagon/domain/tool/__init__.py @@ -36,7 +36,8 @@ class ActionTool(Tool): def executable_str(self): if isinstance(self.action, list): return "\n".join(self.action) - return self.action + cmd = str(self.action) + return cmd[:-1] if cmd.endswith("\n") else cmd class FunctionTool(Tool): diff --git a/hexagon/runtime/execute/action.py b/hexagon/runtime/execute/action.py index c980eb8f..80c0a618 100644 --- a/hexagon/runtime/execute/action.py +++ b/hexagon/runtime/execute/action.py @@ -32,7 +32,9 @@ @execution_hook -def execute_action(tool: ActionTool, env_args: Any, env: Env, cli_args: CliArgs): +def execute_action( + tool: ActionTool, env_args: Any, env: Optional[Env], cli_args: CliArgs +): custom_tools_path = configuration.custom_tools_path action_to_execute: str = tool.executable_str script_interpreter, script_abs_path = __get_interpreter_and_path(action_to_execute) @@ -59,7 +61,7 @@ def execute_action(tool: ActionTool, env_args: Any, env: Env, cli_args: CliArgs) return split_action = action_to_execute.split(" ") - return_code, executed_command = _execute_command( + return_code, executed_command = _execute_inline_command( split_action[0], env_args, cli_args, @@ -144,7 +146,7 @@ def __args_with_optional_env(cli_args: CliArgs, env: Optional[Env]): ) + cli_args.raw_extra_args -def _execute_command( +def _execute_inline_command( command: str, env_args: Any, cli_args: CliArgs, @@ -153,20 +155,16 @@ def _execute_command( action_args: List[str] = None, ): action_args = action_args if action_args else [] - hexagon_args = __sanitize_args_for_command( - env_args, *__args_with_optional_env(cli_args, env) + cmd_as_string = " ".join( + [command] + action_args + __args_with_optional_env(cli_args, env) + ).format( + tool=tool, + env=env, + env_args=env_args, + cli_args=cli_args.format_friendly_extra_args, # TODO: make format_friendly_extra_args work here ) - cmd_as_string = " ".join([command] + action_args + hexagon_args) - env_vars = os.environ.copy() - env_vars[ENVVAR_EXECUTION_TOOL] = tool.json() - if env: - env_vars[ENVVAR_EXECUTION_ENV] = env.json() - - return ( - subprocess.call(cmd_as_string, shell=True, env=env_vars), - cmd_as_string, - ) + return __call_subprocess(cmd_as_string, env, tool) def _execute_script( @@ -177,7 +175,17 @@ def _execute_script( env: Env, cli_args: CliArgs, ): - _execute_command(command, env_args, cli_args, tool, env, [script_path]) + action_args = [script_path] + hexagon_args = __sanitize_args_for_command( + env_args, *__args_with_optional_env(cli_args, env) + ) + cmd_as_string = " ".join([command] + action_args + hexagon_args).format( + tool=tool, + env=env, + env_args=env_args, + cli_args=cli_args.extra_args, + ) + return __call_subprocess(cmd_as_string, env, tool) def __sanitize_args_for_command(*args: Union[List[any], Dict, Env]): @@ -218,3 +226,14 @@ def __load_module(module: str): return sys.modules[module] return importlib.import_module(module) + + +def __call_subprocess(command: str, env: Optional[Env], tool: ActionTool): + env_vars = os.environ.copy() + env_vars[ENVVAR_EXECUTION_TOOL] = tool.json() + if env: + env_vars[ENVVAR_EXECUTION_ENV] = env.json() + return ( + subprocess.call(command, shell=True, env=env_vars), + command, + ) diff --git a/hexagon/runtime/execute/tool.py b/hexagon/runtime/execute/tool.py index fab61ce2..19e9feb8 100644 --- a/hexagon/runtime/execute/tool.py +++ b/hexagon/runtime/execute/tool.py @@ -23,6 +23,7 @@ def select_and_execute_tool( ) -> List[str]: tool = search_by_name_or_alias(tools, cli_args.tool and cli_args.tool.value) env = search_by_name_or_alias(envs, cli_args.env and cli_args.env.value) + # FIXME: validate selected env: if tool has envs defined and env is None -> should fail tool = select_tool(tools, tool) if tool.traced: diff --git a/hexagon/support/input/args.py b/hexagon/support/input/args.py index 03c4884e..da105ea4 100644 --- a/hexagon/support/input/args.py +++ b/hexagon/support/input/args.py @@ -149,6 +149,21 @@ def count(self): def key_value_arg(key, arg): return f"{key}={arg}" + @property + def format_friendly_extra_args(self): + return ( + { + "positional": sorted( + [v for k, v in self.extra_args.items() if k.isdigit()] + ), + "optional": { + k: v for k, v in self.extra_args.items() if not k.isdigit() + }, + } + if self.extra_args + else {} + ) + class ToolArgs(BaseModel): __tracer__ = None diff --git a/tests_e2e/__specs/execute_tool_2.py b/tests_e2e/__specs/execute_tool_2.py index d463b639..46e1cb4e 100644 --- a/tests_e2e/__specs/execute_tool_2.py +++ b/tests_e2e/__specs/execute_tool_2.py @@ -191,6 +191,59 @@ def test_execute_multiline_command_with_input_as_list(): ) +def test_execute_a_formatted_command_with_env_args(): + ( + as_a_user(__file__) + .run_hexagon(["a-simple-formatted-command", "dev"]) + .then_output_should_be(["environment: development, tool: A formatted command"]) + .exit() + ) + ( + as_a_user(__file__) + .run_hexagon(["a-simple-formatted-command", "qa"]) + .then_output_should_be( + ["environment: quality assurance, tool: A formatted command"] + ) + .exit() + ) + + +def test_execute_a_formatted_command_with_object_env_args(): + ( + as_a_user(__file__) + .run_hexagon(["a-complex-formatted-command", "dev"]) + .then_output_should_be(["Hello John, you are 30 years old"]) + .exit() + ) + ( + as_a_user(__file__) + .run_hexagon(["a-complex-formatted-command", "qa"]) + .then_output_should_be(["Hello Jane, you are 40 years old"]) + .exit() + ) + + +def test_execute_a_formatted_command_with_all_action_args(): + ( + as_a_user(__file__) + .run_hexagon( + [ + "all-tool-args-are-replaced", + "dev", + "cli_positional_value", + "--cli_optional='value'", + ] + ) + .then_output_should_be( + [ + "tool alias is ataar, selected env is dev", + "env_args is 32, cli_args are cli_positional_value and 'value'", + ] + ) + .exit() + ) + + def test_execute_inline_command_with_path(): path = os.environ["PATH"] ( diff --git a/tests_e2e/execute_tool_2/app.yml b/tests_e2e/execute_tool_2/app.yml index 38bc7224..778e3ff2 100644 --- a/tests_e2e/execute_tool_2/app.yml +++ b/tests_e2e/execute_tool_2/app.yml @@ -48,6 +48,39 @@ tools: alias: mcal long_name: A multiline command as list + - name: a-simple-formatted-command + alias: asfc + long_name: A formatted command + type: shell + envs: + dev: development + qa: quality assurance + action: 'echo "environment: {env_args}, tool: {tool.long_name}"' + + - name: a-complex-formatted-command + alias: acfc + long_name: A formatted command + type: shell + envs: + dev: + name: 'John' + age: 30 + qa: + name: 'Jane' + age: 40 + action: 'echo "Hello {env_args[name]}, you are {env_args[age]} years old"' + + - name: all-tool-args-are-replaced + alias: ataar + long_name: A formatted command + type: shell + envs: + dev: 32 + qa: 123 + action: | + echo "tool alias is {tool.alias}, selected env is {env.long_name}" + echo "env_args is {env_args}, cli_args are {cli_args[positional][0]} and {cli_args[optional][cli_optional]}" + - name: inline-command-with-PATH action: 'echo "$PATH"' type: shell