diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 687dc0b2..61f7298f 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -129,6 +129,7 @@ def _parser() -> argparse.ArgumentParser: _set_service_config_validate_parser(subparsers) _set_service_key_import_parser(subparsers) _set_service_repositories(subparsers) + _set_service_run(subparsers) _set_service_setup_parser(subparsers) _set_service_shell_parser(subparsers) _set_service_tree_migrate_parser(subparsers) @@ -902,6 +903,27 @@ def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_service_run(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for multicommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("service-run", aliases=["run"], help="run multiple commands", + description="run multiple commands on success run of the previous command", + epilog="Commands must be quoted by using usual bash rules. Processes will be spawned " + "under the same user as this command", + formatter_class=_formatter) + parser.add_argument("command", help="command to be run (quoted) without ``ahriman``", nargs="+") + parser.set_defaults(handler=handlers.Run, architecture="", lock=None, report=False, repository="", + unsafe=True, parser=_parser) + return parser + + def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for setup subcommand diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index fa3cbabd..c4275d10 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -32,6 +32,7 @@ from ahriman.application.handlers.remove import Remove from ahriman.application.handlers.remove_unknown import RemoveUnknown from ahriman.application.handlers.repositories import Repositories from ahriman.application.handlers.restore import Restore +from ahriman.application.handlers.run import Run from ahriman.application.handlers.search import Search from ahriman.application.handlers.service_updates import ServiceUpdates from ahriman.application.handlers.setup import Setup diff --git a/src/ahriman/application/handlers/run.py b/src/ahriman/application/handlers/run.py new file mode 100644 index 00000000..a1ade544 --- /dev/null +++ b/src/ahriman/application/handlers/run.py @@ -0,0 +1,66 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import argparse +import shlex + +from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId + + +class Run(Handler): + """ + multicommand handler + """ + + ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + repository_id(RepositoryId): repository unique identifier + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + """ + parser = args.parser() + for command in args.command: + status = Run.run_command(shlex.split(command), parser) + Run.check_if_empty(True, not status) + + @staticmethod + def run_command(command: list[str], parser: argparse.ArgumentParser) -> bool: + """ + run command specified by the argument + + Args: + command(list[str]): command to run + parser(argparse.ArgumentParser): generated argument parser + + Returns: + bool: status of the command + """ + args = parser.parse_args(command) + handler: Handler = args.handler + return handler.execute(args) == 0 diff --git a/tests/ahriman/application/handlers/test_handler_run.py b/tests/ahriman/application/handlers/test_handler_run.py new file mode 100644 index 00000000..f49beb0b --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_run.py @@ -0,0 +1,66 @@ +import argparse +import pytest + +from pytest_mock import MockerFixture + +from ahriman.application.ahriman import _parser +from ahriman.application.handlers import Run +from ahriman.core.configuration import Configuration +from ahriman.core.exceptions import ExitCode + + +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + """ + default arguments for these test cases + + Args: + args(argparse.Namespace): command line arguments fixture + + Returns: + argparse.Namespace: generated arguments for these test cases + """ + args.command = ["help"] + args.parser = _parser + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + application_mock = mocker.patch("ahriman.application.handlers.Run.run_command") + + _, repository_id = configuration.check_loaded() + Run.run(args, repository_id, configuration, report=False) + application_mock.assert_called_once_with(["help"], pytest.helpers.anyvar(int)) + + +def test_run_failed(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run commands until success + """ + args = _default_args(args) + args.command = ["help", "config"] + application_mock = mocker.patch("ahriman.application.handlers.Run.run_command", return_value=False) + + _, repository_id = configuration.check_loaded() + with pytest.raises(ExitCode): + Run.run(args, repository_id, configuration, report=False) + application_mock.assert_called_once_with(["help"], pytest.helpers.anyvar(int)) + + +def test_run_command(mocker: MockerFixture) -> None: + """ + must correctly run external command + """ + execute_mock = mocker.patch("ahriman.application.handlers.Help.execute") + Run.run_command(["help"], _parser()) + execute_mock.assert_called_once_with(pytest.helpers.anyvar(int)) + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Run.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index e7cba747..84d2af82 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -1060,6 +1060,34 @@ def test_subparsers_service_repositories_option_repository(parser: argparse.Argu assert args.repository == "" +def test_subparsers_service_run(parser: argparse.ArgumentParser) -> None: + """ + service-run command must imply architecture, lock, report, repository and parser + """ + args = parser.parse_args(["service-run", "help"]) + assert args.architecture == "" + assert args.lock is None + assert not args.report + assert args.repository == "" + assert args.parser is not None and args.parser() + + +def test_subparsers_service_run_option_architecture(parser: argparse.ArgumentParser) -> None: + """ + service-run command must correctly parse architecture list + """ + args = parser.parse_args(["-a", "x86_64", "service-run", "help"]) + assert args.architecture == "" + + +def test_subparsers_service_run_option_repository(parser: argparse.ArgumentParser) -> None: + """ + service-run command must correctly parse repository list + """ + args = parser.parse_args(["-r", "repo", "service-run", "help"]) + assert args.repository == "" + + def test_subparsers_service_setup(parser: argparse.ArgumentParser) -> None: """ service-setup command must imply lock, quiet, report and unsafe