aboutsummaryrefslogtreecommitdiff
path: root/tests/aiet/test_cli_application.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/aiet/test_cli_application.py')
-rw-r--r--tests/aiet/test_cli_application.py1153
1 files changed, 1153 insertions, 0 deletions
diff --git a/tests/aiet/test_cli_application.py b/tests/aiet/test_cli_application.py
new file mode 100644
index 0000000..f1ccc44
--- /dev/null
+++ b/tests/aiet/test_cli_application.py
@@ -0,0 +1,1153 @@
+# SPDX-FileCopyrightText: Copyright 2022, Arm Limited and/or its affiliates.
+# SPDX-License-Identifier: Apache-2.0
+# pylint: disable=attribute-defined-outside-init,no-member,line-too-long,too-many-arguments,too-many-locals,redefined-outer-name,too-many-lines
+"""Module for testing CLI application subcommand."""
+import base64
+import json
+import re
+import time
+from contextlib import contextmanager
+from contextlib import ExitStack
+from pathlib import Path
+from typing import Any
+from typing import Generator
+from typing import IO
+from typing import List
+from typing import Optional
+from typing import TypedDict
+from unittest.mock import MagicMock
+
+import click
+import pytest
+from click.testing import CliRunner
+from filelock import FileLock
+
+from aiet.backend.application import Application
+from aiet.backend.config import ApplicationConfig
+from aiet.backend.config import LocalProtocolConfig
+from aiet.backend.config import SSHConfig
+from aiet.backend.config import SystemConfig
+from aiet.backend.config import UserParamConfig
+from aiet.backend.output_parser import Base64OutputParser
+from aiet.backend.protocol import SSHProtocol
+from aiet.backend.system import load_system
+from aiet.cli.application import application_cmd
+from aiet.cli.application import details_cmd
+from aiet.cli.application import execute_cmd
+from aiet.cli.application import install_cmd
+from aiet.cli.application import list_cmd
+from aiet.cli.application import parse_payload_run_config
+from aiet.cli.application import remove_cmd
+from aiet.cli.application import run_cmd
+from aiet.cli.common import MiddlewareExitCode
+
+
+def test_application_cmd() -> None:
+ """Test application commands."""
+ commands = ["list", "details", "install", "remove", "execute", "run"]
+ assert all(command in application_cmd.commands for command in commands)
+
+
+@pytest.mark.parametrize("format_", ["json", "cli"])
+def test_application_cmd_context(cli_runner: CliRunner, format_: str) -> None:
+ """Test setting command context parameters."""
+ result = cli_runner.invoke(application_cmd, ["--format", format_])
+ # command should fail if no subcommand provided
+ assert result.exit_code == 2
+
+ result = cli_runner.invoke(application_cmd, ["--format", format_, "list"])
+ assert result.exit_code == 0
+
+
+@pytest.mark.parametrize(
+ "format_, system_name, expected_output",
+ [
+ (
+ "json",
+ None,
+ '{"type": "application", "available": ["application_1", "application_2"]}\n',
+ ),
+ (
+ "json",
+ "system_1",
+ '{"type": "application", "available": ["application_1"]}\n',
+ ),
+ ("cli", None, "Available applications:\n\napplication_1\napplication_2\n"),
+ ("cli", "system_1", "Available applications:\n\napplication_1\n"),
+ ],
+)
+def test_list_cmd(
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+ format_: str,
+ system_name: str,
+ expected_output: str,
+) -> None:
+ """Test available applications commands."""
+ # Mock some applications
+ mock_application_1 = MagicMock(spec=Application)
+ mock_application_1.name = "application_1"
+ mock_application_1.can_run_on.return_value = system_name == "system_1"
+ mock_application_2 = MagicMock(spec=Application)
+ mock_application_2.name = "application_2"
+ mock_application_2.can_run_on.return_value = system_name == "system_2"
+
+ # Monkey patch the call get_available_applications
+ mock_available_applications = MagicMock()
+ mock_available_applications.return_value = [mock_application_1, mock_application_2]
+
+ monkeypatch.setattr(
+ "aiet.backend.application.get_available_applications",
+ mock_available_applications,
+ )
+
+ obj = {"format": format_}
+ args = []
+ if system_name:
+ list_cmd.params[0].type = click.Choice([system_name])
+ args = ["--system", system_name]
+ result = cli_runner.invoke(list_cmd, obj=obj, args=args)
+ assert result.output == expected_output
+
+
+def get_test_application() -> Application:
+ """Return test system details."""
+ config = ApplicationConfig(
+ name="application",
+ description="test",
+ build_dir="",
+ supported_systems=[],
+ deploy_data=[],
+ user_params={},
+ commands={
+ "clean": ["clean"],
+ "build": ["build"],
+ "run": ["run"],
+ "post_run": ["post_run"],
+ },
+ )
+
+ return Application(config)
+
+
+def get_details_cmd_json_output() -> str:
+ """Get JSON output for details command."""
+ json_output = """
+[
+ {
+ "type": "application",
+ "name": "application",
+ "description": "test",
+ "supported_systems": [],
+ "commands": {
+ "clean": {
+ "command_strings": [
+ "clean"
+ ],
+ "user_params": []
+ },
+ "build": {
+ "command_strings": [
+ "build"
+ ],
+ "user_params": []
+ },
+ "run": {
+ "command_strings": [
+ "run"
+ ],
+ "user_params": []
+ },
+ "post_run": {
+ "command_strings": [
+ "post_run"
+ ],
+ "user_params": []
+ }
+ }
+ }
+]"""
+ return json.dumps(json.loads(json_output)) + "\n"
+
+
+def get_details_cmd_console_output() -> str:
+ """Get console output for details command."""
+ return (
+ 'Application "application" details'
+ + "\nDescription: test"
+ + "\n\nSupported systems: "
+ + "\n\nclean commands:"
+ + "\nCommands: ['clean']"
+ + "\n\nbuild commands:"
+ + "\nCommands: ['build']"
+ + "\n\nrun commands:"
+ + "\nCommands: ['run']"
+ + "\n\npost_run commands:"
+ + "\nCommands: ['post_run']"
+ + "\n"
+ )
+
+
+@pytest.mark.parametrize(
+ "application_name,format_, expected_output",
+ [
+ ("application", "json", get_details_cmd_json_output()),
+ ("application", "cli", get_details_cmd_console_output()),
+ ],
+)
+def test_details_cmd(
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+ application_name: str,
+ format_: str,
+ expected_output: str,
+) -> None:
+ """Test application details command."""
+ monkeypatch.setattr(
+ "aiet.cli.application.get_application",
+ MagicMock(return_value=[get_test_application()]),
+ )
+
+ details_cmd.params[0].type = click.Choice(["application"])
+ result = cli_runner.invoke(
+ details_cmd, obj={"format": format_}, args=["--name", application_name]
+ )
+ assert result.exception is None
+ assert result.output == expected_output
+
+
+def test_details_cmd_wrong_system(cli_runner: CliRunner, monkeypatch: Any) -> None:
+ """Test details command fails if application is not supported by the system."""
+ monkeypatch.setattr(
+ "aiet.backend.execution.get_application", MagicMock(return_value=[])
+ )
+
+ details_cmd.params[0].type = click.Choice(["application"])
+ details_cmd.params[1].type = click.Choice(["system"])
+ result = cli_runner.invoke(
+ details_cmd, args=["--name", "application", "--system", "system"]
+ )
+ assert result.exit_code == 2
+ assert (
+ "Application 'application' doesn't support the system 'system'" in result.stdout
+ )
+
+
+def test_install_cmd(cli_runner: CliRunner, monkeypatch: Any) -> None:
+ """Test install application command."""
+ mock_install_application = MagicMock()
+ monkeypatch.setattr(
+ "aiet.cli.application.install_application", mock_install_application
+ )
+
+ args = ["--source", "test"]
+ cli_runner.invoke(install_cmd, args=args)
+ mock_install_application.assert_called_once_with(Path("test"))
+
+
+def test_remove_cmd(cli_runner: CliRunner, monkeypatch: Any) -> None:
+ """Test remove application command."""
+ mock_remove_application = MagicMock()
+ monkeypatch.setattr(
+ "aiet.cli.application.remove_application", mock_remove_application
+ )
+ remove_cmd.params[0].type = click.Choice(["test"])
+
+ args = ["--directory_name", "test"]
+ cli_runner.invoke(remove_cmd, args=args)
+ mock_remove_application.assert_called_once_with("test")
+
+
+class ExecutionCase(TypedDict, total=False):
+ """Execution case."""
+
+ args: List[str]
+ lock_path: str
+ can_establish_connection: bool
+ establish_connection_delay: int
+ app_exit_code: int
+ exit_code: int
+ output: str
+
+
+@pytest.mark.parametrize(
+ "application_config, system_config, executions",
+ [
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ config_location=Path("wrong_location"),
+ commands={"build": ["echo build {application.name}"]},
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ config_location=Path("wrong_location"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "build"],
+ exit_code=MiddlewareExitCode.CONFIGURATION_ERROR,
+ output="Error: Application test_application has wrong config location\n",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ build_dir="build",
+ deploy_data=[("sample_file", "/tmp/sample_file")],
+ commands={"build": ["echo build {application.name}"]},
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "run"],
+ exit_code=MiddlewareExitCode.CONFIGURATION_ERROR,
+ output="Error: System test_system does not support data deploy\n",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ commands={"build": ["echo build {application.name}"]},
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "build"],
+ exit_code=MiddlewareExitCode.CONFIGURATION_ERROR,
+ output="Error: No build directory defined for the app test_application\n",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["new_system"],
+ build_dir="build",
+ commands={
+ "build": ["echo build {application.name} with {user_params:0}"]
+ },
+ user_params={
+ "build": [
+ UserParamConfig(
+ name="param",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "build"],
+ exit_code=1,
+ output="Error: Application 'test_application' doesn't support the system 'test_system'\n",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ build_dir="build",
+ commands={"build": ["false"]},
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "build"],
+ exit_code=MiddlewareExitCode.BACKEND_ERROR,
+ output="""Running: false
+Error: Execution failed. Please check output for the details.\n""",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ lock=True,
+ build_dir="build",
+ commands={
+ "build": ["echo build {application.name} with {user_params:0}"]
+ },
+ user_params={
+ "build": [
+ UserParamConfig(
+ name="param",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ lock=True,
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "build"],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Running: echo build test_application with param default
+build test_application with param default\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "build"],
+ lock_path="/tmp/middleware_test_application_test_system.lock",
+ exit_code=MiddlewareExitCode.CONCURRENT_ERROR,
+ output="Error: Another instance of the system is running\n",
+ ),
+ ExecutionCase(
+ args=["-c", "build", "--param=param=val3"],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Running: echo build test_application with param val3
+build test_application with param val3\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "build", "--param=param=newval"],
+ exit_code=1,
+ output="Error: Application parameter 'param=newval' not valid for command 'build'\n",
+ ),
+ ExecutionCase(
+ args=["-c", "some_command"],
+ exit_code=MiddlewareExitCode.CONFIGURATION_ERROR,
+ output="Error: Unsupported command some_command\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run"],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Generating commands to execute
+Running: echo run test_application on test_system
+run test_application on test_system\n""",
+ ),
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ deploy_data=[("sample_file", "/tmp/sample_file")],
+ commands={
+ "run": [
+ "echo run {application.name} with {user_params:param} on {system.name}"
+ ]
+ },
+ user_params={
+ "run": [
+ UserParamConfig(
+ name="param=",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ alias="param",
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ lock=True,
+ data_transfer=SSHConfig(
+ protocol="ssh",
+ username="username",
+ password="password",
+ hostname="localhost",
+ port="8022",
+ ),
+ commands={"run": ["sleep 100"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "run"],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds .
+Deploying {application.config_location}/sample_file onto /tmp/sample_file
+Running: echo run test_application with param=default on test_system
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "run"],
+ lock_path="/tmp/middleware_test_system.lock",
+ exit_code=MiddlewareExitCode.CONCURRENT_ERROR,
+ output="Error: Another instance of the system is running\n",
+ ),
+ ExecutionCase(
+ args=[
+ "-c",
+ "run",
+ "--deploy={application.config_location}/sample_file:/tmp/sample_file",
+ ],
+ exit_code=0,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds .
+Deploying {application.config_location}/sample_file onto /tmp/sample_file
+Deploying {application.config_location}/sample_file onto /tmp/sample_file
+Running: echo run test_application with param=default on test_system
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "run"],
+ app_exit_code=1,
+ exit_code=0,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds .
+Deploying {application.config_location}/sample_file onto /tmp/sample_file
+Running: echo run test_application with param=default on test_system
+Application exited with exit code 1
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "run"],
+ exit_code=MiddlewareExitCode.CONNECTION_ERROR,
+ can_establish_connection=False,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds ..........................................................................................
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.
+Error: Couldn't connect to 'localhost:8022'.\n""",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy=bad_format"],
+ exit_code=1,
+ output="Error: Invalid deploy parameter 'bad_format' for command run\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy=:"],
+ exit_code=1,
+ output="Error: Invalid deploy parameter ':' for command run\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy= : "],
+ exit_code=1,
+ output="Error: Invalid deploy parameter ' : ' for command run\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy=some_src_file:"],
+ exit_code=1,
+ output="Error: Invalid deploy parameter 'some_src_file:' for command run\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy=:some_dst_file"],
+ exit_code=1,
+ output="Error: Invalid deploy parameter ':some_dst_file' for command run\n",
+ ),
+ ExecutionCase(
+ args=["-c", "run", "--deploy=unknown_file:/tmp/dest"],
+ exit_code=1,
+ output="Error: Path unknown_file does not exist\n",
+ ),
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ commands={
+ "run": [
+ "echo run {application.name} with {user_params:param} on {system.name}"
+ ]
+ },
+ user_params={
+ "run": [
+ UserParamConfig(
+ name="param=",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ alias="param",
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=SSHConfig(
+ protocol="ssh",
+ username="username",
+ password="password",
+ hostname="localhost",
+ port="8022",
+ ),
+ commands={"run": ["echo Unable to start system"]},
+ ),
+ [
+ ExecutionCase(
+ args=["-c", "run"],
+ exit_code=4,
+ can_establish_connection=False,
+ establish_connection_delay=1,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds .
+
+---------- test_system execution failed ----------
+Unable to start system
+
+
+
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.
+Error: Execution failed. Please check output for the details.\n""",
+ )
+ ],
+ ],
+ ],
+)
+def test_application_command_execution(
+ application_config: ApplicationConfig,
+ system_config: SystemConfig,
+ executions: List[ExecutionCase],
+ tmpdir: Any,
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+) -> None:
+ """Test application command execution."""
+
+ @contextmanager
+ def lock_execution(lock_path: str) -> Generator[None, None, None]:
+ lock = FileLock(lock_path)
+ lock.acquire(timeout=1)
+
+ try:
+ yield
+ finally:
+ lock.release()
+
+ def replace_vars(str_val: str) -> str:
+ """Replace variables."""
+ application_config_location = str(
+ application_config["config_location"].absolute()
+ )
+
+ return str_val.replace(
+ "{application.config_location}", application_config_location
+ )
+
+ for execution in executions:
+ init_execution_test(
+ monkeypatch,
+ tmpdir,
+ application_config,
+ system_config,
+ can_establish_connection=execution.get("can_establish_connection", True),
+ establish_conection_delay=execution.get("establish_connection_delay", 0),
+ remote_app_exit_code=execution.get("app_exit_code", 0),
+ )
+
+ lock_path = execution.get("lock_path")
+
+ with ExitStack() as stack:
+ if lock_path:
+ stack.enter_context(lock_execution(lock_path))
+
+ args = [replace_vars(arg) for arg in execution["args"]]
+
+ result = cli_runner.invoke(
+ execute_cmd,
+ args=["-n", application_config["name"], "-s", system_config["name"]]
+ + args,
+ )
+ output = replace_vars(execution["output"])
+ assert result.exit_code == execution["exit_code"]
+ assert result.stdout == output
+
+
+@pytest.fixture(params=[False, True], ids=["run-cli", "run-json"])
+def payload_path_or_none(request: Any, tmp_path_factory: Any) -> Optional[Path]:
+ """Drives tests for run command so that it executes them both to use a json file, and to use CLI."""
+ if request.param:
+ ret: Path = tmp_path_factory.getbasetemp() / "system_config_payload_file.json"
+ return ret
+ return None
+
+
+def write_system_payload_config(
+ payload_file: IO[str],
+ application_config: ApplicationConfig,
+ system_config: SystemConfig,
+) -> None:
+ """Write a json payload file for the given test configuration."""
+ payload_dict = {
+ "id": system_config["name"],
+ "arguments": {
+ "application": application_config["name"],
+ },
+ }
+ json.dump(payload_dict, payload_file)
+
+
+@pytest.mark.parametrize(
+ "application_config, system_config, executions",
+ [
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ build_dir="build",
+ commands={
+ "build": ["echo build {application.name} with {user_params:0}"]
+ },
+ user_params={
+ "build": [
+ UserParamConfig(
+ name="param",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ ),
+ [
+ ExecutionCase(
+ args=[],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Running: echo build test_application with param default
+build test_application with param default
+Generating commands to execute
+Running: echo run test_application on test_system
+run test_application on test_system\n""",
+ )
+ ],
+ ],
+ [
+ ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ commands={
+ "run": [
+ "echo run {application.name} with {user_params:param} on {system.name}"
+ ]
+ },
+ user_params={
+ "run": [
+ UserParamConfig(
+ name="param=",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ alias="param",
+ )
+ ]
+ },
+ ),
+ SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=SSHConfig(
+ protocol="ssh",
+ username="username",
+ password="password",
+ hostname="localhost",
+ port="8022",
+ ),
+ commands={"run": ["sleep 100"]},
+ ),
+ [
+ ExecutionCase(
+ args=[],
+ exit_code=MiddlewareExitCode.SUCCESS,
+ output="""Generating commands to execute
+Trying to establish connection with 'localhost:8022' - 90 retries every 15.0 seconds .
+Running: echo run test_application with param=default on test_system
+Shutting down sequence...
+Stopping test_system... (It could take few seconds)
+test_system stopped successfully.\n""",
+ )
+ ],
+ ],
+ ],
+)
+def test_application_run(
+ application_config: ApplicationConfig,
+ system_config: SystemConfig,
+ executions: List[ExecutionCase],
+ tmpdir: Any,
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+ payload_path_or_none: Path,
+) -> None:
+ """Test application command execution."""
+ for execution in executions:
+ init_execution_test(monkeypatch, tmpdir, application_config, system_config)
+
+ if payload_path_or_none:
+ with open(payload_path_or_none, "w", encoding="utf-8") as payload_file:
+ write_system_payload_config(
+ payload_file, application_config, system_config
+ )
+
+ result = cli_runner.invoke(
+ run_cmd,
+ args=["--config", str(payload_path_or_none)],
+ )
+ else:
+ result = cli_runner.invoke(
+ run_cmd,
+ args=["-n", application_config["name"], "-s", system_config["name"]]
+ + execution["args"],
+ )
+
+ assert result.stdout == execution["output"]
+ assert result.exit_code == execution["exit_code"]
+
+
+@pytest.mark.parametrize(
+ "cmdline,error_pattern",
+ [
+ [
+ "--config {payload} -s test_system",
+ "when --config is set, the following parameters should not be provided",
+ ],
+ [
+ "--config {payload} -n test_application",
+ "when --config is set, the following parameters should not be provided",
+ ],
+ [
+ "--config {payload} -p mypar:3",
+ "when --config is set, the following parameters should not be provided",
+ ],
+ [
+ "-p mypar:3",
+ "when --config is not set, the following parameters are required",
+ ],
+ ["-s test_system", "when --config is not set, --name is required"],
+ ["-n test_application", "when --config is not set, --system is required"],
+ ],
+)
+def test_application_run_invalid_param_combinations(
+ cmdline: str,
+ error_pattern: str,
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+ tmp_path: Any,
+ tmpdir: Any,
+) -> None:
+ """Test that invalid combinations arguments result in error as expected."""
+ application_config = ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ build_dir="build",
+ commands={"build": ["echo build {application.name} with {user_params:0}"]},
+ user_params={
+ "build": [
+ UserParamConfig(
+ name="param",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ )
+ ]
+ },
+ )
+ system_config = SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={"run": ["echo run {application.name} on {system.name}"]},
+ )
+
+ init_execution_test(monkeypatch, tmpdir, application_config, system_config)
+
+ payload_file = tmp_path / "payload.json"
+ payload_file.write_text("dummy")
+ result = cli_runner.invoke(
+ run_cmd,
+ args=cmdline.format(payload=payload_file).split(),
+ )
+ found = re.search(error_pattern, result.stdout)
+ assert found, f"Cannot find pattern: [{error_pattern}] in \n[\n{result.stdout}\n]"
+
+
+@pytest.mark.parametrize(
+ "payload,expected",
+ [
+ pytest.param(
+ {"arguments": {}},
+ None,
+ marks=pytest.mark.xfail(reason="no system 'id''", strict=True),
+ ),
+ pytest.param(
+ {"id": "testsystem"},
+ None,
+ marks=pytest.mark.xfail(reason="no arguments object", strict=True),
+ ),
+ (
+ {"id": "testsystem", "arguments": {"application": "testapp"}},
+ ("testsystem", "testapp", [], [], [], None),
+ ),
+ (
+ {
+ "id": "testsystem",
+ "arguments": {"application": "testapp", "par1": "val1"},
+ },
+ ("testsystem", "testapp", ["par1=val1"], [], [], None),
+ ),
+ (
+ {
+ "id": "testsystem",
+ "arguments": {"application": "testapp", "application/par1": "val1"},
+ },
+ ("testsystem", "testapp", ["par1=val1"], [], [], None),
+ ),
+ (
+ {
+ "id": "testsystem",
+ "arguments": {"application": "testapp", "system/par1": "val1"},
+ },
+ ("testsystem", "testapp", [], ["par1=val1"], [], None),
+ ),
+ (
+ {
+ "id": "testsystem",
+ "arguments": {"application": "testapp", "deploy/par1": "val1"},
+ },
+ ("testsystem", "testapp", [], [], ["par1"], None),
+ ),
+ (
+ {
+ "id": "testsystem",
+ "arguments": {
+ "application": "testapp",
+ "appar1": "val1",
+ "application/appar2": "val2",
+ "system/syspar1": "val3",
+ "deploy/depploypar1": "val4",
+ "application/appar3": "val5",
+ "system/syspar2": "val6",
+ "deploy/depploypar2": "val7",
+ },
+ },
+ (
+ "testsystem",
+ "testapp",
+ ["appar1=val1", "appar2=val2", "appar3=val5"],
+ ["syspar1=val3", "syspar2=val6"],
+ ["depploypar1", "depploypar2"],
+ None,
+ ),
+ ),
+ ],
+)
+def test_parse_payload_run_config(payload: dict, expected: tuple) -> None:
+ """Test parsing of the JSON payload for the run_config command."""
+ assert parse_payload_run_config(payload) == expected
+
+
+def test_application_run_report(
+ tmpdir: Any,
+ cli_runner: CliRunner,
+ monkeypatch: Any,
+) -> None:
+ """Test flag '--report' of command 'application run'."""
+ app_metrics = {"app_metric": 3.14}
+ app_metrics_b64 = base64.b64encode(json.dumps(app_metrics).encode("utf-8"))
+ application_config = ApplicationConfig(
+ name="test_application",
+ description="Test application",
+ supported_systems=["test_system"],
+ build_dir="build",
+ commands={"build": ["echo build {application.name} with {user_params:0}"]},
+ user_params={
+ "build": [
+ UserParamConfig(
+ name="param",
+ description="sample parameter",
+ default_value="default",
+ values=["val1", "val2", "val3"],
+ ),
+ UserParamConfig(
+ name="p2",
+ description="another parameter, not overridden",
+ default_value="the-right-choice",
+ values=["the-right-choice", "the-bad-choice"],
+ ),
+ ]
+ },
+ )
+ system_config = SystemConfig(
+ name="test_system",
+ description="Test system",
+ data_transfer=LocalProtocolConfig(protocol="local"),
+ commands={
+ "run": [
+ "echo run {application.name} on {system.name}",
+ f"echo build <{Base64OutputParser.TAG_NAME}>{app_metrics_b64.decode('utf-8')}</{Base64OutputParser.TAG_NAME}>",
+ ]
+ },
+ reporting={
+ "regex": {
+ "app_name": {
+ "pattern": r"run (.\S*) ",
+ "type": "str",
+ },
+ "sys_name": {
+ "pattern": r"on (.\S*)",
+ "type": "str",
+ },
+ }
+ },
+ )
+ report_file = Path(tmpdir) / "test_report.json"
+ param_val = "param=val1"
+ exit_code = MiddlewareExitCode.SUCCESS
+
+ init_execution_test(monkeypatch, tmpdir, application_config, system_config)
+
+ result = cli_runner.invoke(
+ run_cmd,
+ args=[
+ "-n",
+ application_config["name"],
+ "-s",
+ system_config["name"],
+ "--report",
+ str(report_file),
+ "--param",
+ param_val,
+ ],
+ )
+ assert result.exit_code == exit_code
+ assert report_file.is_file()
+ with open(report_file, "r", encoding="utf-8") as file:
+ report = json.load(file)
+
+ assert report == {
+ "application": {
+ "metrics": {"0": {"app_metric": 3.14}},
+ "name": "test_application",
+ "params": {"param": "val1", "p2": "the-right-choice"},
+ },
+ "system": {
+ "metrics": {"app_name": "test_application", "sys_name": "test_system"},
+ "name": "test_system",
+ "params": {},
+ },
+ }
+
+
+def init_execution_test(
+ monkeypatch: Any,
+ tmpdir: Any,
+ application_config: ApplicationConfig,
+ system_config: SystemConfig,
+ can_establish_connection: bool = True,
+ establish_conection_delay: float = 0,
+ remote_app_exit_code: int = 0,
+) -> None:
+ """Init execution test."""
+ application_name = application_config["name"]
+ system_name = system_config["name"]
+
+ execute_cmd.params[0].type = click.Choice([application_name])
+ execute_cmd.params[1].type = click.Choice([system_name])
+ execute_cmd.params[2].type = click.Choice(["build", "run", "some_command"])
+
+ run_cmd.params[0].type = click.Choice([application_name])
+ run_cmd.params[1].type = click.Choice([system_name])
+
+ if "config_location" not in application_config:
+ application_path = Path(tmpdir) / "application"
+ application_path.mkdir()
+ application_config["config_location"] = application_path
+
+ # this file could be used as deploy parameter value or
+ # as deploy parameter in application configuration
+ sample_file = application_path / "sample_file"
+ sample_file.touch()
+ monkeypatch.setattr(
+ "aiet.backend.application.get_available_applications",
+ MagicMock(return_value=[Application(application_config)]),
+ )
+
+ ssh_protocol_mock = MagicMock(spec=SSHProtocol)
+
+ def mock_establish_connection() -> bool:
+ """Mock establish connection function."""
+ # give some time for the system to start
+ time.sleep(establish_conection_delay)
+ return can_establish_connection
+
+ ssh_protocol_mock.establish_connection.side_effect = mock_establish_connection
+ ssh_protocol_mock.connection_details.return_value = ("localhost", 8022)
+ ssh_protocol_mock.run.return_value = (
+ remote_app_exit_code,
+ bytearray(),
+ bytearray(),
+ )
+ monkeypatch.setattr(
+ "aiet.backend.protocol.SSHProtocol", MagicMock(return_value=ssh_protocol_mock)
+ )
+
+ if "config_location" not in system_config:
+ system_path = Path(tmpdir) / "system"
+ system_path.mkdir()
+ system_config["config_location"] = system_path
+ monkeypatch.setattr(
+ "aiet.backend.system.get_available_systems",
+ MagicMock(return_value=[load_system(system_config)]),
+ )
+
+ monkeypatch.setattr("aiet.backend.execution.wait", MagicMock())