Port the CLI from the argparse/oslo wrapper to typer

Replace the custom oslo.config/argparse CLI wrapper with a typer
application. typer (over its vendored click) now owns the whole CLI surface
(help, validation, strict rejection of unknown arguments); oslo.config and
oslo.log are used only as libraries for config-file loading and logging
setup.

- Global options are generated at runtime from the live oslo Opt objects
  and appended to the root callback signature, then translated back into an
  argv slice for rally.api.API, so they stay in sync with oslo.log.
- The six command groups become plain module-level functions on per-group
  sub-apps; the API instance is injected through a ContextVar instead of a
  parameter, keeping it out of the generated help and completion.
- Each command's primary identifier is an ArgumentOrKeyword: it is accepted
  both positionally (rally task status <uuid>) and via its historical flag
  (--uuid), which is registered on the parser and hidden from help.
- task start/validate accept "-" to read the input task from stdin.
- The bash completion script is generated from the command tree
  (rally/cli/bashcomplete.py) and guarded by tests/unit/test_resources.
- The cli_reference sphinx extension and the CHANGELOG are updated to match.

Backward-incompatible: documented usage is preserved, but help and error
messages are now rendered by typer/click (different wording and layout), and
bash completion no longer suggests the primary-id flags (--uuid, --id,
--env, --deployment, --task) -- they still work, and the identifiers can now
also be passed positionally.

Change-Id: I99341a53d8f79c2afa382f977bbddd9202eb6d09
Signed-off-by: Andriy Kurilin <andr.kurilin@gmail.com>
This commit is contained in:
Andriy Kurilin
2026-07-10 17:42:00 +02:00
parent 4a07817be8
commit 62546ec666
34 changed files with 4690 additions and 4103 deletions
+19
View File
@@ -26,10 +26,29 @@ Added
~~~~~
* CI jobs for checking compatibility with python 3.13
* ``rally task start`` and ``rally task validate`` can now read the task from
stdin -- pass ``-`` in place of the file name, for example
``cat task.yaml | rally task start -``.
Changed
~~~~~~~
* The command-line interface has been rebuilt on `typer
<https://typer.tiangolo.com>`_, replacing the custom argparse/oslo.config
wrapper it grew up on. The change is backward compatible for documented
usage: every command, option and primary identifier continues to work, and
each primary identifier may now also be supplied positionally, for example
``rally task status <uuid>`` in addition to ``rally task status --uuid
<uuid>``. The only user-visible differences are cosmetic: help and error
messages are now rendered by typer, and bash completion no longer lists the
primary-id flags (``--uuid``, ``--id``, ``--env``, ``--deployment``,
``--task``), which still work when typed explicitly.
* We no longer suppress PyMySQL's connect-time warnings. That workaround only
existed to hide the old ``@@tx_isolation`` deprecation warning (SQLAlchemy
#4120 / PyMySQL #614), which modern SQLAlchemy and PyMySQL have since fixed,
so the ``mysql`` extra now pins ``PyMySQL>=0.9.0``.
* The ``pep8`` gate now runs ruff alongside flake8: ruff owns the pycodestyle,
pyflakes and import-convention checks, while flake8 keeps the remaining
hacking and Rally-specific rules.
+107 -124
View File
@@ -13,64 +13,44 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import inspect
import typing as t
from docutils.parsers import rst
import typer
from rally.cli import cliutils
from rally.cli import main
from . import utils
class Parser(object):
"""A simplified interface of argparse.ArgumentParser"""
def __init__(self):
self.parsers = {}
self.subparser = None
self.defaults = {}
self.arguments = []
def add_parser(self, name, help=None, description=None,
formatter_class=None):
parser = Parser()
self.parsers[name] = {"description": description,
"help": help,
"fclass": formatter_class,
"parser": parser}
return parser
def set_defaults(self, command_object=None, action_fn=None,
action_kwargs=None):
if command_object:
self.defaults["command_object"] = command_object
if action_fn:
self.defaults["action_fn"] = action_fn
if action_kwargs:
self.defaults["action_kwargs"] = action_kwargs
def add_subparsers(self, dest):
# NOTE(andreykurilin): there is only one expected call
if self.subparser:
raise ValueError("Can't add one more subparser.")
self.subparser = Parser()
return self.subparser
def add_argument(self, *args, **kwargs):
if "action_args" in args:
return
self.arguments.append((args, kwargs))
if t.TYPE_CHECKING:
from docutils import nodes
DEFAULT_UUIDS_CMD = {
"deployment": ["rally deployment create"],
"task": ["rally task start"],
"verification": ["rally verify start", "rally verify import_results"]
"verification": ["rally verify start", "rally verify import"]
}
# Maps the "use"-command hint for each default-from-environment id.
USE_CMD = {
"deployment": "rally deployment use",
"task": "rally task use",
"verification": "rally verify use",
}
# Maps a parameter's env var to a default-uuid id.
_ENVVAR_DEST = {
"RALLY_ENV": "deployment",
"RALLY_DEPLOYMENT": "deployment",
"RALLY_TASK": "task",
"RALLY_VERIFICATION": "verification",
}
def compose_note_about_default_uuids(argument, dest):
def compose_note_about_default_uuids(argument, dest) -> "nodes.note":
# TODO(andreykurilin): add references to commands
return utils.note(
"The default value for the ``%(arg)s`` argument is taken from "
@@ -81,96 +61,97 @@ def compose_note_about_default_uuids(argument, dest):
"cmd": "``, ``".join(DEFAULT_UUIDS_CMD[dest])})
def compose_use_cmd_hint_msg(cmd):
def compose_use_cmd_hint_msg(cmd: str) -> "nodes.hint":
return utils.hint(
"You can set the default value by executing ``%(cmd)s <uuid>``"
" (ref__).\n\n __ #%(ref)s" % {"cmd": cmd,
"ref": cmd.replace(" ", "-")})
f"You can set the default value by executing ``{cmd} <uuid>``"
f" (ref__).\n\n __ #{cmd.replace(' ', '-')}"
)
def make_arguments_section(category_name, cmd_name, arguments, defaults):
def _first(value):
if isinstance(value, (list, tuple)):
return value[0] if value else None
return value
def _note_dest(cmd_name, param):
"""Return the default-uuid note key for a parameter, or None.
A parameter earns the note when it reads one of the ``RALLY_*`` env vars
that ``rally <thing> use`` populates. The ``use`` command sets these
defaults itself, so it is excluded.
"""
if cmd_name == "use":
return None
return _ENVVAR_DEST.get(_first(getattr(param, "envvar", None)))
def _display_names(param):
"""Return the flag(s)/name shown for a parameter."""
opts = list(getattr(param, "opts", None) or [])
if opts and opts[0].startswith("-"):
return opts
# A positional argument: show its metavar (e.g. ``UUID``) rather than the
# internal destination name.
metavar = getattr(param, "metavar", None)
return [metavar] if metavar else (opts or [param.name])
def _iter_params(command):
"""Yield the documentable parameters of a command (skip ``--help``)."""
for param in command.params:
opts = getattr(param, "opts", [])
if opts and opts[0] in ("--help", "-h", "--version"):
continue
if getattr(param, "name", None) == "help":
continue
yield param
def make_arguments_section(category_name, cmd_name, command):
elements = [utils.paragraph("**Command arguments**:")]
for args, kwargs in arguments:
# for future changes...
# :param args: a single command argument which can represented by
# several names(for example, --uuid and --task-id) in cli.
# :type args: tuple
# :param kwargs: description of argument. Have next format:
# {"dest": "action_kwarg_<name of keyword argument in code>",
# "help": "just a description of argument"
# "metavar": "[optional] metavar of argument. Example:"
# "Example: argument '--file'; metavar 'path' ",
# "type": "[optional] class object of argument's type",
# "required": "[optional] boolean value"}
# :type kwargs: dict
dest = kwargs.get("dest").replace("action_kwarg_", "")
for param in _iter_params(command):
names = _display_names(param)
flag = names[0]
description = []
if cmd_name != "use":
# lets add notes about specific default values and hint about
# "use" command with reference
if dest in ("deployment", "task"):
description.append(compose_note_about_default_uuids(
args[0], dest))
description.append(
compose_use_cmd_hint_msg("rally %s use" % dest))
elif dest == "verification":
description.append(compose_note_about_default_uuids(
args[0], dest))
description.append(
compose_use_cmd_hint_msg("rally verify use"))
note_dest = _note_dest(cmd_name, param)
if note_dest is not None:
description.append(
compose_note_about_default_uuids(flag, note_dest))
description.append(
compose_use_cmd_hint_msg(USE_CMD[note_dest]))
description.append(kwargs.get("help"))
description.append(getattr(param, "help", None))
action = kwargs.get("action")
if not action:
arg_type = kwargs.get("type")
if arg_type:
description.append("**Type**: %s" % arg_type.__name__)
if not getattr(param, "is_flag", False):
type_name = getattr(getattr(param, "type", None), "name", None)
if type_name:
description.append("**Type**: %s" % type_name)
skip_default = dest in ("deployment",
"task_id",
"verification")
if not skip_default and dest in defaults:
description.append("**Default**: %s" % defaults[dest])
metavar = kwargs.get("metavar")
default = getattr(param, "default", None)
if note_dest is None and default is not None:
description.append("**Default**: %s" % default)
ref = "%s_%s_%s" % (category_name, cmd_name, args[0].replace("-", ""))
if metavar:
args = ["%s %s" % (arg, metavar) for arg in args]
elements.extend(utils.make_definition(", ".join(args),
ref = "%s_%s_%s" % (category_name, cmd_name,
flag.replace("-", "").replace(" ", ""))
elements.extend(utils.make_definition(", ".join(names),
ref, description))
return elements
def get_defaults(func):
"""Return a map of argument:default_value for specified function."""
spec = inspect.getfullargspec(func)
if spec.defaults:
return dict(zip(spec.args[-len(spec.defaults):], spec.defaults))
return {}
def make_command_section(category_name, name, parser):
def make_command_section(category_name, name, command):
section = utils.subcategory("rally %s %s" % (category_name, name))
section.extend(utils.parse_text(parser["description"]))
if parser["parser"].arguments:
defaults = get_defaults(parser["parser"].defaults["action_fn"])
section.extend(make_arguments_section(
category_name, name, parser["parser"].arguments, defaults))
description = inspect.getdoc(command.callback) or command.help or ""
section.extend(utils.parse_text(description))
if any(True for _ in _iter_params(command)):
section.extend(make_arguments_section(category_name, name, command))
return section
def make_category_section(name, parser):
def make_category_section(name, group):
category_obj = utils.category("Category: %s" % name)
# NOTE(andreykurilin): we are re-using `_add_command_parsers` method from
# `rally.cli.cliutils`, but, since it was designed to print help message,
# generated description for categories contains specification for all
# sub-commands. We don't need information about sub-commands at this point,
# so let's skip "generated description" and take it directly from category
# class.
description = parser.defaults["command_object"].__doc__
description = group.help or ""
# TODO(andreykurilin): write a decorator which will mark cli-class as
# deprecated without changing its docstring.
if description.startswith("[Deprecated"):
@@ -180,9 +161,10 @@ def make_category_section(name, parser):
category_obj.append(utils.warning(msg))
category_obj.extend(utils.parse_text(description))
for command in sorted(parser.subparser.parsers.keys()):
subparser = parser.subparser.parsers[command]
category_obj.append(make_command_section(name, command, subparser))
commands = getattr(group, "commands", {})
for command in sorted(commands):
category_obj.append(
make_command_section(name, command, commands[command]))
return category_obj
@@ -191,17 +173,18 @@ class CLIReferenceDirective(rst.Directive):
option_spec = {"group": str}
def run(self):
parser = Parser()
categories = copy.copy(main.categories)
cli = typer.main.get_command(main.app)
groups = getattr(cli, "commands", {})
# only command groups (skip top-level leaf commands like ``version``)
categories = [c for c, g in groups.items()
if getattr(g, "commands", None)]
if "group" in self.options:
categories = {k: v for k, v in categories.items()
if k == self.options["group"]}
cliutils._add_command_parsers(categories, parser)
categories = [c for c in categories
if c == self.options["group"]]
content = []
for cg in sorted(categories.keys()):
content.append(make_category_section(
cg, parser.parsers[cg]["parser"]))
for cg in sorted(categories):
content.append(make_category_section(cg, groups[cg]))
return content
+47 -48
View File
@@ -24,61 +24,60 @@ _rally()
OPTS["db_revision"]=""
OPTS["db_show"]="--creds"
OPTS["db_upgrade"]=""
OPTS["deployment_check"]="--deployment"
OPTS["deployment_config"]="--deployment"
OPTS["deployment_check"]=""
OPTS["deployment_config"]=""
OPTS["deployment_create"]="--name --fromenv --filename --no-use"
OPTS["deployment_destroy"]="--deployment"
OPTS["deployment_destroy"]=""
OPTS["deployment_list"]=""
OPTS["deployment_recreate"]="--filename --deployment"
OPTS["deployment_show"]="--deployment"
OPTS["deployment_use"]="--deployment"
OPTS["env_check"]="--env --json --detailed"
OPTS["env_cleanup"]="--json --env"
OPTS["env_create"]="--name --description --extras --from-sysenv --spec --json --no-use"
OPTS["env_delete"]="--env --force"
OPTS["env_destroy"]="--env --skip-cleanup --json --detailed"
OPTS["env_info"]="--env --json"
OPTS["deployment_recreate"]="--filename"
OPTS["deployment_show"]=""
OPTS["deployment_use"]=""
OPTS["env_check"]="--json --detailed"
OPTS["env_cleanup"]="--json"
OPTS["env_create"]="--name --description --extras --spec --from-sysenv --json --no-use"
OPTS["env_delete"]="--force"
OPTS["env_destroy"]="--skip-cleanup --json --detailed"
OPTS["env_info"]="--json"
OPTS["env_list"]="--json"
OPTS["env_show"]="--env --json --only-spec"
OPTS["env_use"]="--env --json"
OPTS["plugin_list"]="--name --platform --plugin-base"
OPTS["plugin_show"]="--name --platform"
OPTS["task_abort"]="--uuid --soft"
OPTS["task_delete"]="--force --uuid"
OPTS["task_detailed"]="--uuid --iterations-data --filter-by"
OPTS["task_export"]="--uuid --type --to --deployment"
OPTS["env_show"]="--json --only-spec"
OPTS["env_use"]="--json"
OPTS["plugin_list"]="--platform --plugin-base"
OPTS["plugin_show"]="--platform"
OPTS["task_abort"]="--soft"
OPTS["task_delete"]="--force"
OPTS["task_detailed"]="--iterations-data --filter-by"
OPTS["task_export"]="--type --to --deployment"
OPTS["task_import"]="--file --deployment --tag"
OPTS["task_list"]="--deployment --all-deployments --status --tag --uuids-only"
OPTS["task_report"]="--out --open --html --html-static --json --uuid --deployment"
OPTS["task_restart"]="--deployment --uuid --scenario --tag --no-use --abort-on-sla-failure"
OPTS["task_results"]="--uuid"
OPTS["task_sla-check"]="--uuid --json"
OPTS["task_start"]="--deployment --task --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
OPTS["task_status"]="--uuid"
OPTS["task_trends"]="--out --open --tasks --html-static"
OPTS["task_use"]="--uuid"
OPTS["task_validate"]="--deployment --task --task-args --task-args-file"
OPTS["verify_add-verifier-ext"]="--id --source --version --extra-settings"
OPTS["verify_configure-verifier"]="--id --deployment-id --reconfigure --extend --override --show"
OPTS["verify_create-verifier"]="--name --type --platform --source --version --system-wide --extra-settings --no-use"
OPTS["verify_delete"]="--uuid"
OPTS["verify_delete-verifier"]="--id --deployment-id --force"
OPTS["verify_delete-verifier-ext"]="--id --name"
OPTS["verify_import"]="--id --deployment-id --file --run-args --no-use"
OPTS["task_report"]="--out --open --html --html-static --json --deployment"
OPTS["task_restart"]="--deployment --scenario --tag --no-use --abort-on-sla-failure"
OPTS["task_results"]=""
OPTS["task_sla-check"]="--json"
OPTS["task_start"]="--deployment --task-args --task-args-file --tag --no-use --abort-on-sla-failure"
OPTS["task_status"]=""
OPTS["task_trends"]="--out --open --html-static"
OPTS["task_use"]=""
OPTS["task_validate"]="--deployment --task-args --task-args-file"
OPTS["verify_add-verifier-ext"]="--source --extra-settings"
OPTS["verify_configure-verifier"]="--deployment-id --reconfigure --extend --override --show"
OPTS["verify_create-verifier"]="--name --type --platform --source --system-wide --extra-settings --no-use"
OPTS["verify_delete"]=""
OPTS["verify_delete-verifier"]="--deployment-id --force"
OPTS["verify_delete-verifier-ext"]="--name"
OPTS["verify_import"]="--deployment-id --file --run-args --no-use"
OPTS["verify_list"]="--id --deployment-id --tag --status"
OPTS["verify_list-plugins"]="--platform"
OPTS["verify_list-verifier-exts"]="--id"
OPTS["verify_list-verifier-tests"]="--id --pattern"
OPTS["verify_list-verifier-exts"]=""
OPTS["verify_list-verifier-tests"]="--pattern"
OPTS["verify_list-verifiers"]="--status"
OPTS["verify_report"]="--uuid --type --to --open"
OPTS["verify_rerun"]="--uuid --deployment-id --failed --tag --concurrency --detailed --no-use"
OPTS["verify_show"]="--uuid --sort-by --detailed"
OPTS["verify_show-verifier"]="--id"
OPTS["verify_start"]="--id --deployment-id --tag --pattern --concurrency --load-list --skip-list --xfail-list --detailed --no-use"
OPTS["verify_update-verifier"]="--id --update-venv --version --system-wide --no-system-wide"
OPTS["verify_use"]="--uuid"
OPTS["verify_use-verifier"]="--id"
OPTS["verify_report"]="--type --to --open"
OPTS["verify_rerun"]="--deployment-id --failed --tag --concurrency --detailed --no-use"
OPTS["verify_show"]="--sort-by --detailed"
OPTS["verify_show-verifier"]=""
OPTS["verify_start"]="--deployment-id --tag --pattern --concurrency --load-list --skip-list --xfail-list --detailed --no-use"
OPTS["verify_update-verifier"]="--update-venv --system-wide --no-system-wide"
OPTS["verify_use"]=""
OPTS["verify_use-verifier"]=""
for OPT in ${!OPTS[*]} ; do
CMD=${OPT%%_*}
CMDSUB=${OPT#*_}
@@ -106,4 +105,4 @@ _rally()
return 0
}
complete -o filenames -F _rally rally
complete -o filenames -F _rally rally
+8 -29
View File
@@ -48,7 +48,7 @@ dependencies = {file = ["requirements.txt"]}
[project.optional-dependencies]
mysql = [
"PyMySQL>=0.7.6" # MIT
"PyMySQL>=0.9.0" # MIT
]
postgres = [
"psycopg2>=2.5" # LGPL/ZPL
@@ -81,6 +81,11 @@ strict_equality = true
# declare optional result
no_warn_no_return = true
# oslo.log ships no type information (no py.typed marker).
[[tool.mypy.overrides]]
module = "oslo_log.*"
ignore_missing_imports = true
# FIXME(andreykurilin): all the following should be fixed
[[tool.mypy.overrides]]
@@ -89,40 +94,12 @@ disable_error_code = ["attr-defined", "no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.cliutils"
disable_error_code = ["arg-type", "assignment", "import-untyped", "index", "no-untyped-def", "var-annotated"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.db"
disable_error_code = ["no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.deployment"
disable_error_code = ["import-not-found", "no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.env"
disable_error_code = ["arg-type", "func-returns-value", "no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.plugin"
disable_error_code = ["no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.task"
disable_error_code = ["no-untyped-def", "var-annotated"]
[[tool.mypy.overrides]]
module = "rally.cli.commands.verify"
disable_error_code = ["assignment", "attr-defined", "method-assign", "no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.envutils"
disable_error_code = ["no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.main"
disable_error_code = ["no-untyped-def"]
[[tool.mypy.overrides]]
module = "rally.cli.task_results_loader"
disable_error_code = ["attr-defined", "no-untyped-def"]
@@ -307,6 +284,7 @@ select = [
"ICN003", # forbid `from <module> import ...` for modules imported whole
"PGH003", # `type: ignore` must name the specific error code
"PGH004", # `noqa` must name the specific rule code
"PLC0414", # band redundant aliases
"TID251", # forbid the APIs banned below
]
ignore = [
@@ -341,6 +319,7 @@ docstring-quotes = "double"
datetime = "dt"
typing = "t"
typing_extensions = "te"
"rally.common.logging" = "logging"
[tool.ruff.lint.flake8-import-conventions]
# Builtins are imported as modules: forbid `from <stdlib> import ...` so the
+7 -6
View File
@@ -1195,19 +1195,20 @@ class API(object):
CONFIG_SEARCH_PATHS = [sys.prefix + "/etc/rally", "~/.rally", "/etc/rally"]
CONFIG_FILE_NAME = "rally.conf"
def __init__(self, config_file=None, config_args=None,
plugin_paths=None, skip_db_check=False):
def __init__(
self,
config_file: str | None = None,
config_args: list[str] | None = None,
plugin_paths: list[str] | None = None,
skip_db_check: bool = False
):
"""Initialize Rally API instance
:param config_file: Path to rally configuration file. If None, default
path will be selected
:type config_file: str
:param config_args: Arguments for initialization current configuration
:type config_args: list
:param plugin_paths: Additional custom plugin locations
:type plugin_paths: list
:param skip_db_check: Allows to skip db revision check
:type skip_db_check: bool
"""
try:
+116
View File
@@ -0,0 +1,116 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Primary identifiers that accept both a positional and a ``--flag`` form.
Rally has always exposed a command's primary identifier both ways, e.g.
``rally task abort <uuid>`` and ``rally task abort --uuid <uuid>``. typer lets
a parameter be an Argument *or* an Option, not both, so
:class:`ArgumentOrKeyword` declares the positional form and :func:`install`
registers the ``--flag`` alias
directly in the parser, pointing at the same destination. Requiredness,
env-var defaults, unknown-option and extra-argument errors all stay native --
there is no hidden duplicate parameter and no post-hoc value merging.
"""
import inspect
import typing as t
import typer
import typer.core
from rally.cli import cliutils
if t.TYPE_CHECKING:
import typer._click.core
class ArgumentOrKeyword(typer.models.ArgumentInfo):
"""A required positional argument that is also accepted as ``--flag``.
Use it in place of :func:`typer.Argument` and pass the legacy flag
name(s)::
task_id: t.Annotated[
str,
ArgumentOrKeyword("--uuid", envvar=envutils.ENV_TASK,
help="UUID of task.")
]
typer builds an ordinary required Argument from it (the ``--flag`` is wired
up later by :func:`install`); the flag itself stays out of ``--help`` and
bash completion, so the positional form is the documented one.
"""
def __init__(
self,
*kw_decls: str,
help: str | None = None,
envvar: str | list[str] | None = None,
metavar: str | None = None
) -> None:
if metavar is None and kw_decls:
# ``--uuid`` -> ``UUID``, nicer in usage than the derived name.
metavar = kw_decls[0].lstrip("-").replace("-", "_").upper()
super().__init__(default=..., help=help, envvar=envvar,
metavar=metavar)
self.kw_decls = kw_decls
def _patch_param(
param: "typer.core.TyperOption",
kw_decls: t.Sequence[str]
) -> None:
"""Register ``kw_decls`` as an option feeding ``param``'s destination."""
original_add_to_parser = param.add_to_parser
# A list argument (``nargs == -1``) accepts repeated ``--flag`` values.
action = "append" if param.nargs == -1 else "store"
def add_to_parser(
parser: "typer._click.core._OptionParser",
ctx: "typer._click.core.Context"
) -> None:
original_add_to_parser(parser, ctx)
# Don't let the (now optional) positional overwrite a value that the
# ``--flag`` option already stored under the shared destination.
argument = parser._args[-1]
original_process = argument.process
def process(value: t.Any, state: t.Any) -> None:
if value in (None, ()) and param.name in state.opts:
return
original_process(value, state)
argument.process = process # type: ignore[method-assign]
parser.add_option(param, list(kw_decls), param.name,
action=action, nargs=1)
param.add_to_parser = add_to_parser # type: ignore[method-assign]
def install(command: typer.core.TyperGroup | typer.core.TyperCommand) -> None:
"""Wire up every :class:`ArgumentOrKeyword` in a built command tree."""
for _path, leaf, params in cliutils.iter_commands(command):
if leaf.callback is None:
continue
signature = inspect.signature(inspect.unwrap(leaf.callback))
marks = {}
for name, parameter in signature.parameters.items():
for meta in getattr(parameter.annotation, "__metadata__", ()):
if isinstance(meta, ArgumentOrKeyword):
marks[name] = meta.kw_decls
if not marks:
continue
for param in params:
if param.name in marks:
_patch_param(param, marks[param.name])
+100
View File
@@ -0,0 +1,100 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import itertools
_HEADER = r"""#!/bin/bash
# Standalone _filedir() alternative.
# This exempts from dependence of bash completion routines
function _rally_filedir()
{
test "${1}" \
&& COMPREPLY=( \
$(compgen -f -- "${cur}" | grep -E "${1}") \
$(compgen -o plusdirs -- "${cur}") ) \
|| COMPREPLY=( \
$(compgen -o plusdirs -f -- "${cur}") \
$(compgen -d -- "${cur}") )
}
_rally()
{
declare -A SUBCOMMANDS
declare -A OPTS
"""
_FOOTER = r""" for OPT in ${!OPTS[*]} ; do
CMD=${OPT%%_*}
CMDSUB=${OPT#*_}
SUBCOMMANDS[${CMD}]+="${CMDSUB} "
done
COMMANDS="${!SUBCOMMANDS[*]}"
COMPREPLY=()
local cur="${COMP_WORDS[COMP_CWORD]}"
local prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ $cur =~ ^(\.|\~|\/) ]] || [[ $prev =~ ^--out(|put-file)$ ]] ; then
_rally_filedir
elif [[ $prev =~ ^--(task|filename)$ ]] ; then
_rally_filedir "\.json|\.yaml|\.yml"
elif [ $COMP_CWORD == "1" ] ; then
COMPREPLY=($(compgen -W "$COMMANDS" -- ${cur}))
elif [ $COMP_CWORD == "2" ] ; then
COMPREPLY=($(compgen -W "${SUBCOMMANDS[${prev}]}" -- ${cur}))
else
COMMAND="${COMP_WORDS[1]}_${COMP_WORDS[2]}"
COMPREPLY=($(compgen -W "${OPTS[$COMMAND]}" -- ${cur}))
fi
return 0
}
complete -o filenames -F _rally rally
"""
def generate() -> str:
"""Return the bash completion script for the current CLI."""
import typer
from rally.cli import cliutils
from rally.cli import main
# ``typer.main.get_command`` builds the resolved command tree; we read the
# flags there rather than from ``registered_commands`` because typer stores
# a declared flag on the raw ``typer.Option`` ambiguously -- an Annotated
# positional flag lands in ``OptionInfo.default``, not ``param_decls`` --
# so only the built command exposes the correct ``--flags``.
command = typer.main.get_command(main.app)
lines = []
for path, _leaf, params in cliutils.iter_commands(command):
if len(path) != 2:
# top-level leaf command (e.g. ``version``) -- no OPTS entry
continue
category, name = path
opts = " ".join(
itertools.chain.from_iterable(
(
# only long ``--flags``; short aliases (``-n``) are valid
# but kept out of completion
name for name in p.opts
if (name.startswith("--")
and name not in ("--help", "--version"))
)
for p in params
))
lines.append(f' OPTS["{category}_{name}"]="{opts}"\n')
return _HEADER + "".join(sorted(lines)) + _FOOTER
+56 -483
View File
@@ -13,67 +13,72 @@
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import inspect
"""Shared helpers for the Rally CLI."""
import contextvars
import functools
import json
import os
import sys
import textwrap
import typing as t
import warnings
import jsonschema
import prettytable
import sqlalchemy.exc
import typer.core
from rally import api
from rally import exceptions
from rally.common import cfg
from rally.common import logging
from rally.common.plugin import info
from rally.utils import encodeutils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
if t.TYPE_CHECKING:
import typer._click.core
from rally.api import API
# Some CLI-specific constants
MARGIN = 3
_api: "contextvars.ContextVar[API]" = contextvars.ContextVar("rally_api")
class MissingArgs(Exception):
"""Supplied arguments are not sufficient for calling a function."""
def __init__(self, missing):
self.missing = missing
msg = "Missing arguments: %s" % ", ".join(missing)
super(MissingArgs, self).__init__(msg)
def set_api(api: "API") -> None:
"""Stash the per-invocation Rally API handle for the commands to read."""
_api.set(api)
def validate_args(fn, *args, **kwargs):
"""Check that the supplied args are sufficient for calling a function.
def get_api() -> "API":
"""Return the Rally API handle stashed by `set_api`."""
return _api.get()
>>> validate_args(lambda a: None)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): a
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
Traceback (most recent call last):
...
MissingArgs: Missing argument(s): b, d
:param fn: the function to check
:param args: the positional arguments supplied
:param kwargs: the keyword arguments supplied
def iter_commands(
command: "typer._click.core.Command",
) -> t.Iterator[
tuple[
tuple[str, ...],
typer.core.TyperCommand,
list[typer.core.TyperOption]
]
]:
"""Walk a built typer command tree and yield every leaf command.
:param command: a resolved typer command, e.g. the result of
`typer.main.get_command`. Groups are recursed into; for each leaf
command the generator yields ``(path, leaf, params)`` where:
* ``path`` is the tuple of command names from the root down to the
leaf (the root group itself contributes no name), so
``rally task status`` yields
``("task", "status")`` and a top-level ``rally version`` yields
``("version",)``;
* ``leaf`` is the `typer.core.TyperCommand` itself;
* ``params`` is its option/argument list
"""
required_args = [
p.name for p in inspect.signature(fn).parameters.values()
if p.default == inspect.Parameter.empty
and p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD]
missing_required_args = required_args[len(args):]
missing = [arg for arg in missing_required_args if arg not in kwargs]
if missing:
raise MissingArgs(missing)
if isinstance(command, typer.core.TyperGroup):
for name, sub in command.commands.items():
for path, leaf, params in iter_commands(sub):
yield (name, *path), leaf, params
else:
leaf = t.cast(typer.core.TyperCommand, command)
params = t.cast("list[typer.core.TyperOption]", leaf.params)
yield (), leaf, params
def print_list(objs, fields, formatters=None, sortby_index=0,
@@ -300,67 +305,24 @@ def make_table_header(table_label, table_width,
return "\n".join((border_line, label_line,))
def make_header(text, size=80, symbol="-"):
def make_header(text: str, size: int=80, symbol: str="-") -> str:
"""Unified way to make header message to CLI.
:param text: what text to write
:param size: Length of header decorative line
:param symbol: What symbol to use to create header
"""
header = symbol * size + "\n"
header += "%s\n" % text
header += symbol * size + "\n"
return header
return f"{symbol * size}\n{text}\n{symbol * size}\n"
def suppress_warnings(f):
f._suppress_warnings = True
return f
class CategoryParser(argparse.ArgumentParser):
"""Customized arguments parser
We need this one to override hardcoded behavior.
So, we want to print item's help instead of 'error: too few arguments'.
Also, we want not to print positional arguments in help message.
"""
def format_help(self):
formatter = self._get_formatter()
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
# positionals, optionals and user-defined groups
# INFO(oanufriev) _action_groups[0] contains positional arguments.
for action_group in self._action_groups[1:]:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
# epilog
formatter.add_text(self.epilog)
# determine help from format above
return formatter.format_help()
def error(self, message):
self.print_help(sys.stderr)
if message.startswith("argument") and message.endswith("is required"):
# NOTE(pirsriva) Argparse will currently raise an error
# message for only 1 missing argument at a time i.e. in the
# error message it WILL NOT LIST ALL the missing arguments
# at once INSTEAD only 1 missing argument at a time
missing_arg = message.split()[1]
print("Missing argument:\n%s" % missing_arg)
sys.exit(2)
"""Run the wrapped command with Python warnings silenced."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
return f(*args, **kwargs)
return wrapper
def pretty_float_formatter(field, ndigits=None):
@@ -379,392 +341,3 @@ def pretty_float_formatter(field, ndigits=None):
return value
return "n/a"
return _formatter
def args(*args, **kwargs):
def _decorator(func):
func.__dict__.setdefault("args", []).insert(0, (args, kwargs))
if "metavar" not in kwargs and "action" not in kwargs:
# NOTE(andreykurilin): argparse constructs awful metavars...
kwargs["metavar"] = "<%s>" % args[0].replace(
"--", "").replace("-", "_")
return func
return _decorator
def alias(command_name):
"""Allow cli to use alias command name instead of function name.
:param command_name: desired command name
"""
def decorator(func):
func.alias = command_name
return func
return decorator
def deprecated_args(*args, **kwargs):
def _decorator(func):
if "release" not in kwargs:
raise ValueError("'release' is required keyword argument of "
"'deprecated_args' decorator.")
release = kwargs.pop("release")
alternative = kwargs.pop("alternative", None)
help_msg = "[Deprecated since Rally %s] " % release
if alternative:
help_msg += "Use '%s' instead. " % alternative
if "help" in kwargs:
help_msg += kwargs["help"]
kwargs["help"] = help_msg
func.__dict__.setdefault("args", []).insert(0, (args, kwargs))
func.__dict__.setdefault("deprecated_args", {})
func.deprecated_args[args[0]] = (release, alternative)
return func
return _decorator
def help_group(uuid):
"""Label cli method with specific group.
Joining methods by groups allows to compose more user-friendly help
messages in CLI.
:param uuid: Name of group to find common methods. It will be used for
sorting groups in help message, so you can start uuid with
some number (i.e "1_launcher", "2_management") to put groups in proper
order. Note: default group had "0" uuid.
"""
def wrapper(func):
func.help_group = uuid
return func
return wrapper
def _methods_of(cls):
"""Get all callable methods of a class that don't start with underscore.
:returns: a list of tuples of the form (method_name, method)
"""
# The idea of unbound methods exists in Python 2 and was removed in
# Python 3, so "inspect.ismethod" is used here for Python 2 and
# "inspect.isfunction" for Python 3.
all_methods = inspect.getmembers(
cls, predicate=lambda x: inspect.ismethod(x) or inspect.isfunction(x))
methods = [m for m in all_methods if not m[0].startswith("_")]
help_groups = {}
for m in methods:
group = getattr(m[1], "help_group", "0")
help_groups.setdefault(group, []).append(m)
if len(help_groups) > 1:
# we should sort methods by groups
methods = []
for group in sorted(help_groups.items(), key=lambda x: x[0]):
if methods:
# None -> empty line between groups
methods.append((None, None))
methods.extend(group[1])
return methods
def _compose_category_description(category):
descr_pairs = _methods_of(category)
description = ""
doc = category.__doc__
if doc:
description = doc.strip()
if descr_pairs:
description += "\n\nCommands:\n"
sublen = lambda item: len(item[0]) if item[0] else 0
first_column_len = max(map(sublen, descr_pairs)) + MARGIN
for item in descr_pairs:
if item[0] is None:
description += "\n"
continue
name = getattr(item[1], "alias", item[0].replace("_", "-"))
if item[1].__doc__:
doc = info.parse_docstring(
item[1].__doc__)["short_description"]
else:
doc = ""
name += " " * (first_column_len - len(name))
description += " %s%s\n" % (name, doc)
return description
def _compose_action_description(action_fn):
description = ""
if action_fn.__doc__:
parsed_doc = info.parse_docstring(action_fn.__doc__)
short = parsed_doc.get("short_description")
long = parsed_doc.get("long_description")
description = "%s\n\n%s" % (short, long) if long else short
return description
def _print_version():
from rally.common import version
print("Rally version: %s" % version.__version__)
packages = version.plugins_versions()
if packages:
print("\nInstalled Plugins:")
print("\n".join("\t%s: %s" % p for p in sorted(packages.items())))
def _add_command_parsers(categories, subparsers):
# INFO(oanufriev) This monkey patching makes our custom parser class to be
# used instead of native. This affects all subparsers down from
# 'subparsers' parameter of this function (categories and actions).
subparsers._parser_class = CategoryParser
parser = subparsers.add_parser("bash-completion")
parser.add_argument("query_category", nargs="?")
for category in categories:
command_object = categories[category]()
descr = _compose_category_description(categories[category])
parser = subparsers.add_parser(
category, description=descr,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.set_defaults(command_object=command_object)
category_subparsers = parser.add_subparsers(dest="action")
for method_name, method in _methods_of(command_object):
if method is None:
continue
method_name = method_name.replace("_", "-")
descr = _compose_action_description(method)
parser = category_subparsers.add_parser(
getattr(method, "alias", method_name),
formatter_class=argparse.RawDescriptionHelpFormatter,
description=descr, help=descr)
action_kwargs = []
for args, kwargs in getattr(method, "args", []):
# FIXME(markmc): hack to assume dest is the arg name without
# the leading hyphens if no dest is supplied
kwargs.setdefault("dest", args[0][2:])
action_kwargs.append(kwargs["dest"])
kwargs["dest"] = "action_kwarg_" + kwargs["dest"]
parser.add_argument(*args, **kwargs)
parser.set_defaults(action_fn=method)
parser.set_defaults(action_kwargs=action_kwargs)
parser.add_argument("action_args", nargs="*")
def validate_deprecated_args(argv, fn):
if (len(argv) > 3
and (argv[2] == fn.__name__)
and getattr(fn, "deprecated_args", None)):
for item, details in fn.deprecated_args.items():
if item in argv[3:]:
msg = ("The argument `%s` is deprecated since Rally %s." %
(item, details[0]))
if details[1]:
msg += " Use `%s` instead." % details[1]
LOG.warning(msg)
def run(argv, categories):
if len(argv) > 1 and argv[1] in ["version", "--version"]:
_print_version()
return 0
parser = lambda subparsers: _add_command_parsers(categories, subparsers)
category_opt = cfg.SubCommandOpt("category",
title="Command categories",
help="Available categories",
handler=parser)
CONF.register_cli_opt(category_opt)
help_msg = ("Additional custom plugin locations. Multiple files or "
"directories may be specified. All plugins in the specified"
" directories and subdirectories will be imported. Plugins in"
" /opt/rally/plugins and ~/.rally/plugins will always be "
"imported.")
CONF.register_cli_opt(cfg.ListOpt("plugin-paths",
default=os.environ.get(
"RALLY_PLUGIN_PATHS"),
help=help_msg))
# NOTE(andreykurilin): this dirty hack is done to unblock the gates.
# Currently, we are using oslo.config for CLI purpose (don't do this!)
# and it makes the things too complicated.
# To discover which CLI method can be affected by warnings and which not
# (based on suppress_warnings decorator) we need to obtain a desired
# CLI method. It can be done only after initialization of oslo_config
# which is located in rally.api.API init method.
# Initialization of rally.api.API can produce a warning (for example,
# from pymysql), so suppressing of warnings later will not work in such
# case (it is what actually had happened now in our CI with the latest
# release of PyMySQL).
#
# https://bitbucket.org/zzzeek/sqlalchemy/issues/4120/mysql-5720-warns-on-tx_isolation
try:
import pymysql
warnings.filterwarnings("ignore", category=pymysql.Warning)
except ImportError:
pass
try:
rapi = api.API(config_args=argv[1:], skip_db_check=True)
except exceptions.RallyException as e:
print(e)
return 2
if CONF.category.name == "bash-completion":
print(_generate_bash_completion_script())
return 0
fn = CONF.category.action_fn
fn_args = [encodeutils.safe_decode(arg)
for arg in CONF.category.action_args]
# api instance always is the first argument
fn_args.insert(0, rapi)
fn_kwargs = {}
for k in CONF.category.action_kwargs:
v = getattr(CONF.category, "action_kwarg_" + k)
if v is None:
continue
if isinstance(v, str):
v = encodeutils.safe_decode(v)
fn_kwargs[k] = v
# call the action with the remaining arguments
# check arguments
try:
validate_args(fn, *fn_args, **fn_kwargs)
except MissingArgs as e:
# NOTE(mikal): this isn't the most helpful error message ever. It is
# long, and tells you a lot of things you probably don't want to know
# if you just got a single arg wrong.
print(fn.__doc__)
CONF.print_help()
print("Missing arguments:")
for missing in e.missing:
for arg in fn.args:
if arg[1].get("dest", "").endswith(missing):
print(" " + arg[0][0])
break
return 1
try:
validate_deprecated_args(argv, fn)
# skip db check for db and plugin commands
if CONF.category.name not in ("db", "plugin"):
rapi.check_db_revision()
if getattr(fn, "_suppress_warnings", False):
with warnings.catch_warnings():
warnings.simplefilter("ignore")
ret = fn(*fn_args, **fn_kwargs)
else:
ret = fn(*fn_args, **fn_kwargs)
return ret
except (IOError, TypeError, ValueError,
exceptions.RallyException, jsonschema.ValidationError) as e:
known_errors = (exceptions.InvalidTaskConfig, )
if logging.is_debug() and not isinstance(e, known_errors):
LOG.exception("Unexpected exception in CLI")
else:
print(e)
return getattr(e, "error_code", 1)
except sqlalchemy.exc.OperationalError as e:
if logging.is_debug():
LOG.exception("Something went wrong with database")
print(e)
print("Looks like Rally can't connect to its DB.")
print("Make sure that connection string in rally.conf is proper:")
print(CONF.database.connection)
return 1
except Exception:
print("Command failed, please check log for more info")
raise
def _generate_bash_completion_script():
from rally.cli import main
bash_data = """#!/bin/bash
# Standalone _filedir() alternative.
# This exempts from dependence of bash completion routines
function _rally_filedir()
{
test "${1}" \\
&& COMPREPLY=( \\
$(compgen -f -- "${cur}" | grep -E "${1}") \\
$(compgen -o plusdirs -- "${cur}") ) \\
|| COMPREPLY=( \\
$(compgen -o plusdirs -f -- "${cur}") \\
$(compgen -d -- "${cur}") )
}
_rally()
{
declare -A SUBCOMMANDS
declare -A OPTS
%(data)s
for OPT in ${!OPTS[*]} ; do
CMD=${OPT%%%%_*}
CMDSUB=${OPT#*_}
SUBCOMMANDS[${CMD}]+="${CMDSUB} "
done
COMMANDS="${!SUBCOMMANDS[*]}"
COMPREPLY=()
local cur="${COMP_WORDS[COMP_CWORD]}"
local prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ $cur =~ ^(\\.|\\~|\\/) ]] || [[ $prev =~ ^--out(|put-file)$ ]] ; then
_rally_filedir
elif [[ $prev =~ ^--(task|filename)$ ]] ; then
_rally_filedir "\\.json|\\.yaml|\\.yml"
elif [ $COMP_CWORD == "1" ] ; then
COMPREPLY=($(compgen -W "$COMMANDS" -- ${cur}))
elif [ $COMP_CWORD == "2" ] ; then
COMPREPLY=($(compgen -W "${SUBCOMMANDS[${prev}]}" -- ${cur}))
else
COMMAND="${COMP_WORDS[1]}_${COMP_WORDS[2]}"
COMPREPLY=($(compgen -W "${OPTS[$COMMAND]}" -- ${cur}))
fi
return 0
}
complete -o filenames -F _rally rally
"""
completion = []
for category, cmds in main.categories.items():
for name, command in _methods_of(cmds):
if name is None:
continue
command_name = getattr(command, "alias", name.replace("_", "-"))
args_list = []
for arg in getattr(command, "args", []):
if getattr(command, "deprecated_args", []):
if arg[0][0] not in command.deprecated_args:
args_list.append(arg[0][0])
else:
args_list.append(arg[0][0])
args = " ".join(args_list)
completion.append(""" OPTS["{cat}_{cmd}"]="{args}"\n""".format(
cat=category, cmd=command_name, args=args))
return bash_data % {"data": "".join(sorted(completion))}
+76 -54
View File
@@ -1,6 +1,3 @@
# Copyright 2013: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@@ -14,71 +11,96 @@
# under the License.
import re
import typing as t
import typer
from rally.cli import cliutils
from rally.cli import envutils
from rally.common import cfg
from rally.common import db
class DBCommands(object):
"""CLI commands for DB management."""
db_app = typer.Typer(
name="db", no_args_is_help=False,
help="Set of commands that allow you to manage Rally's database.")
def recreate(self, api):
"""Drop and create Rally database.
This will delete all existing data.
"""
print("Recreating database: ", end="")
self.show(api, True)
db.schema.schema_cleanup()
print("Database deleted successfully")
def _print_connection(show_creds: bool) -> None:
if show_creds:
print(cfg.CONF.database.connection)
else:
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
@db_app.command()
def recreate() -> None:
"""Drop and create Rally database.
This will delete all existing data.
"""
print("Recreating database: ", end="")
_print_connection(True)
db.schema.schema_cleanup()
print("Database deleted successfully")
db.schema.schema_create()
print("Database created successfully")
envutils.clear_env()
@db_app.command()
def create() -> None:
"""Create Rally database."""
print("Creating database: ", end="")
_print_connection(True)
db.schema.schema_create()
print("Database created successfully")
@db_app.command()
def ensure() -> None:
"""Create Rally database if it doesn't exist."""
print("Ensuring database exists: ", end="")
_print_connection(True)
if not db.schema.schema_revision():
db.schema.schema_create()
print("Database created successfully")
envutils.clear_env()
else:
print("Database already exists, nothing to do")
def create(self, api):
"""Create Rally database."""
print("Creating database: ", end="")
self.show(api, True)
db.schema.schema_create()
print("Database created successfully")
def ensure(self, api):
"""Creates Rally database if it doesn't exists."""
print("Ensuring database exists: ", end="")
self.show(api, True)
@db_app.command()
def upgrade() -> None:
"""Upgrade Rally database to the latest state."""
print("Upgrading database: ", end="")
_print_connection(True)
if not db.schema.schema_revision():
db.schema.schema_create()
print("Database created successfully")
else:
print("Database already exists, nothing to do")
start_revision = db.schema.schema_revision()
db.schema.schema_upgrade()
current_revision = db.schema.schema_revision()
if start_revision != current_revision:
print("Database schema upgraded successfully "
"from {start} to {end} revision."
.format(start=start_revision, end=current_revision))
else:
print("Database is already up to date")
def upgrade(self, api):
"""Upgrade Rally database to the latest state."""
print("Upgrading database: ", end="")
self.show(api, True)
start_revision = db.schema.schema_revision()
db.schema.schema_upgrade()
current_revision = db.schema.schema_revision()
if start_revision != current_revision:
print("Database schema upgraded successfully "
"from {start} to {end} revision."
.format(start=start_revision, end=current_revision))
else:
print("Database is already up to date")
@db_app.command()
def revision() -> None:
"""Print current Rally database revision UUID."""
print(db.schema.schema_revision())
def revision(self, api):
"""Print current Rally database revision UUID."""
print(db.schema.schema_revision())
@cliutils.args("--creds", action="store_true", dest="show_creds",
help="Do not hide credentials from connection string")
def show(self, api, show_creds=False):
"""Show the connection string."""
if not show_creds:
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
else:
print(cfg.CONF.database.connection)
@db_app.command()
def show(
creds: t.Annotated[
bool,
typer.Option(
"--creds",
help="Do not hide credentials from connection string"
)
] = False,
) -> None:
"""Show the connection string."""
_print_connection(creds)
+359 -280
View File
@@ -18,11 +18,15 @@
import json
import os
import sys
import typing as t
import jsonschema
import typer
from rally import exceptions
from rally import plugins
from rally.api import API
from rally.cli import argutils
from rally.cli import cliutils
from rally.cli import envutils
from rally.cli import yamlutils as yaml
@@ -31,309 +35,384 @@ from rally.common import utils
from rally.env import env_mgr
class DeploymentCommands(object):
"""Set of commands that allow you to manage deployments."""
deployment_app = typer.Typer(
name="deployment", no_args_is_help=False,
help="Set of commands that allow you to manage deployments.")
@cliutils.args("--name", type=str, required=True,
help="Name of the deployment.")
@cliutils.args("--fromenv", action="store_true",
help="Read environment variables instead of config file.")
@cliutils.args("--filename", type=str, required=False, metavar="<path>",
help="Path to the configuration file of the deployment.")
@cliutils.args("--no-use", action="store_false", dest="do_use",
help="Don't set new deployment as default for"
" future operations.")
@plugins.ensure_plugins_are_loaded
def create(self, api, name, fromenv=False, filename=None, do_use=False):
"""Create new deployment.
This command will create a new deployment record in rally
database. In the case of ExistingCloud deployment engine, it
will use the cloud represented in the configuration. If the
cloud doesn't exist, Rally can deploy a new one for you with
Devstack or Fuel. Different deployment engines exist for these
cases (see `rally plugin list --plugin-base Engine` for
more details).
def _list_deployments(api: API,
deployment_list: list | None = None) -> None:
headers = ["uuid", "created_at", "name", "status", "active"]
current_deployment = envutils.get_global("RALLY_DEPLOYMENT")
deployment_list = deployment_list or api.deployment.list()
If you use the ExistingCloud deployment engine, you can pass
the deployment config by environment variables with ``--fromenv``:
table_rows = []
if deployment_list:
for dep in deployment_list:
r = [str(dep[column]) for column in headers[:-1]]
r.append("" if dep["uuid"] != current_deployment else "*")
table_rows.append(utils.Struct(**dict(zip(headers, r))))
cliutils.print_list(table_rows, headers,
sortby_index=headers.index("created_at"))
else:
print("There are no deployments. To create a new deployment, use:"
"\nrally deployment create")
OS_USERNAME
OS_PASSWORD
OS_AUTH_URL
OS_TENANT_NAME or OS_PROJECT_NAME
OS_ENDPOINT_TYPE or OS_INTERFACE
OS_ENDPOINT
OS_REGION_NAME
OS_CACERT
OS_INSECURE
OS_IDENTITY_API_VERSION
All other deployment engines need more complex configuration
data, so it should be stored in a configuration file.
def _update_openrc_deployment_file(deployment: str, credential: dict) -> None:
openrc_path = os.path.expanduser("~/.rally/openrc-%s" % deployment)
with open(openrc_path, "w+") as env_file:
env_file.write("export OS_AUTH_URL='%(auth_url)s'\n"
"export OS_USERNAME='%(username)s'\n"
"export OS_PASSWORD='%(password)s'\n"
"export OS_TENANT_NAME='%(tenant_name)s'\n"
"export OS_PROJECT_NAME='%(tenant_name)s'\n"
% credential)
if credential.get("region_name"):
env_file.write("export OS_REGION_NAME='%s'\n" %
credential["region_name"])
if credential.get("endpoint_type"):
env_file.write("export OS_ENDPOINT_TYPE='%sURL'\n" %
credential["endpoint_type"])
env_file.write("export OS_INTERFACE='%s'\n" %
credential["endpoint_type"])
if credential.get("endpoint"):
env_file.write("export OS_ENDPOINT='%s'\n" %
credential["endpoint"])
if credential.get("https_cacert"):
env_file.write("export OS_CACERT='%s'\n" %
credential["https_cacert"])
if credential.get("project_domain_name"):
env_file.write("export OS_IDENTITY_API_VERSION=3\n"
"export OS_USER_DOMAIN_NAME='%s'\n"
"export OS_PROJECT_DOMAIN_NAME='%s'\n" %
(credential["user_domain_name"],
credential["project_domain_name"]))
expanded_path = os.path.expanduser("~/.rally/openrc")
if os.path.exists(expanded_path):
os.remove(expanded_path)
os.symlink(openrc_path, expanded_path)
You can use physical servers, LXC containers, KVM virtual
machines or virtual machines in OpenStack for deploying the
cloud. Except physical servers, Rally can create cluster nodes
for you. Interaction with virtualization software, OpenStack
cloud or physical servers is provided by server providers.
"""
if fromenv:
result = env_mgr.EnvManager.create_spec_from_sys_environ()
config = result["spec"]
if "existing@openstack" in config:
# NOTE(andreykurilin): if we are here it means that
# rally-openstack package is installed
import rally_openstack
if rally_openstack.__version_tuple__ <= (1, 4, 0):
if ("https_key" in config["existing@openstack"]
and config["existing@openstack"]["https_key"]):
print("WARNING: OS_KEY is ignored due to old version "
"of rally-openstack package.")
# NOTE(andreykurilin): To support
# rally-openstack<=1.4.0 we need to remove
# https_key, since OpenStackCredentials object
# doesn't support it.
# Latest rally-openstack fixed this issue with
# https://github.com/openstack/rally-openstack/commit/c7483386e6b59474c83e3ecd0c7ee0e77ff50c02
config["existing@openstack"].pop("https_key")
def _use(api: API, deployment: t.Any) -> int | None:
# TODO(astudenov): make this method platform independent
try:
if not isinstance(deployment, dict):
deployment = api.deployment.get(deployment=deployment)
except exceptions.DBRecordNotFound:
print("Deployment %s is not found." % deployment)
return 1
print("Using deployment: %s" % deployment["uuid"])
envutils.update_globals_file(envutils.ENV_DEPLOYMENT, deployment["uuid"])
envutils.update_globals_file(envutils.ENV_ENV, deployment["uuid"])
if "openstack" in deployment["credentials"]:
creds = deployment["credentials"]["openstack"][0]
_update_openrc_deployment_file(
deployment["uuid"], creds["admin"] or creds["users"][0])
print("~/.rally/openrc was updated\n\nHINTS:\n"
"\n* To use standard OpenStack clients, set up your env by "
"running:\n\tsource ~/.rally/openrc\n"
" OpenStack clients are now configured, e.g run:\n\t"
"openstack image list")
return None
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def create(
name: t.Annotated[
str,
typer.Option(
help="Name of the deployment."
)
],
fromenv: t.Annotated[
bool,
typer.Option(
help="Read environment variables instead of config file."
)
] = False,
filename: t.Annotated[
str | None,
typer.Option(
help="Path to the configuration file of the deployment."
)
] = None,
no_use: t.Annotated[
bool,
typer.Option(
"--no-use",
help="Don't set new deployment as default for future operations."
)
] = False,
) -> None:
"""Create new deployment.
This command will create a new deployment record in rally
database. In the case of ExistingCloud deployment engine, it
will use the cloud represented in the configuration. If the
cloud doesn't exist, Rally can deploy a new one for you with
Devstack or Fuel. Different deployment engines exist for these
cases (see `rally plugin list --plugin-base Engine` for
more details).
If you use the ExistingCloud deployment engine, you can pass
the deployment config by environment variables with ``--fromenv``:
OS_USERNAME
OS_PASSWORD
OS_AUTH_URL
OS_TENANT_NAME or OS_PROJECT_NAME
OS_ENDPOINT_TYPE or OS_INTERFACE
OS_ENDPOINT
OS_REGION_NAME
OS_CACERT
OS_INSECURE
OS_IDENTITY_API_VERSION
All other deployment engines need more complex configuration
data, so it should be stored in a configuration file.
You can use physical servers, LXC containers, KVM virtual
machines or virtual machines in OpenStack for deploying the
cloud. Except physical servers, Rally can create cluster nodes
for you. Interaction with virtualization software, OpenStack
cloud or physical servers is provided by server providers.
"""
api = cliutils.get_api()
if fromenv:
result = env_mgr.EnvManager.create_spec_from_sys_environ()
config = result["spec"]
if "existing@openstack" in config:
# NOTE(andreykurilin): if we are here it means that
# rally-openstack package is installed
import rally_openstack # type: ignore[import-not-found]
if rally_openstack.__version_tuple__ <= (1, 4, 0):
if ("https_key" in config["existing@openstack"]
and config["existing@openstack"]["https_key"]):
print("WARNING: OS_KEY is ignored due to old version "
"of rally-openstack package.")
config["existing@openstack"].pop("https_key")
else:
if not filename:
config = {}
else:
if not filename:
config = {}
else:
with open(os.path.expanduser(filename), "rb") as deploy_file:
config = yaml.safe_load(deploy_file.read())
try:
deployment = api.deployment.create(config=config, name=name)
except jsonschema.ValidationError:
print("Config schema validation error: %s." % sys.exc_info()[1])
return 1
except exceptions.DBRecordExists:
print("Error: %s" % sys.exc_info()[1])
return 1
self.list(api, deployment_list=[deployment])
if do_use:
self.use(api, deployment)
@cliutils.args("--filename", type=str, required=False, metavar="<path>",
help="Path to the configuration file of the deployment.")
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the deployment.")
@envutils.with_default_deployment()
@plugins.ensure_plugins_are_loaded
def recreate(self, api, deployment=None, filename=None):
"""Destroy and create an existing deployment.
Unlike 'deployment destroy', the deployment database record
will not be deleted, so the deployment UUID stays the same.
"""
config = None
if filename:
with open(filename, "rb") as deploy_file:
with open(os.path.expanduser(filename), "rb") as deploy_file:
config = yaml.safe_load(deploy_file.read())
api.deployment.recreate(deployment=deployment, config=config)
try:
deployment = api.deployment.create(config=config, name=name)
except jsonschema.ValidationError:
print("Config schema validation error: %s." % sys.exc_info()[1])
raise typer.Exit(code=1)
except exceptions.DBRecordExists:
print("Error: %s" % sys.exc_info()[1])
raise typer.Exit(code=1)
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the deployment.")
@envutils.with_default_deployment()
@plugins.ensure_plugins_are_loaded
def destroy(self, api, deployment=None):
"""Destroy existing deployment.
_list_deployments(api, deployment_list=[deployment])
if not no_use:
_use(api, deployment)
This will delete all containers, virtual machines, OpenStack
instances or Fuel clusters created during Rally deployment
creation. Also it will remove the deployment record from the
Rally database.
"""
api.deployment.destroy(deployment=deployment)
def list(self, api, deployment_list=None):
"""List existing deployments."""
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def recreate(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of the deployment.",
envvar=envutils.ENV_ENV
)
],
filename: t.Annotated[
str | None,
typer.Option(
help="Path to the configuration file of the deployment."
)
] = None,
) -> None:
"""Destroy and create an existing deployment.
headers = ["uuid", "created_at", "name", "status", "active"]
current_deployment = envutils.get_global("RALLY_DEPLOYMENT")
deployment_list = deployment_list or api.deployment.list()
Unlike 'deployment destroy', the deployment database record
will not be deleted, so the deployment UUID stays the same.
"""
api = cliutils.get_api()
config = None
if filename:
with open(filename, "rb") as deploy_file:
config = yaml.safe_load(deploy_file.read())
table_rows = []
if deployment_list:
for t in deployment_list:
r = [str(t[column]) for column in headers[:-1]]
r.append("" if t["uuid"] != current_deployment else "*")
table_rows.append(utils.Struct(**dict(zip(headers, r))))
cliutils.print_list(table_rows, headers,
sortby_index=headers.index("created_at"))
api.deployment.recreate(deployment=deployment, config=config)
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def destroy(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of the deployment.",
envvar=envutils.ENV_ENV
)
],
) -> None:
"""Destroy existing deployment.
This will delete all containers, virtual machines, OpenStack
instances or Fuel clusters created during Rally deployment
creation. Also it will remove the deployment record from the
Rally database.
"""
api = cliutils.get_api()
api.deployment.destroy(deployment=deployment)
@deployment_app.command(name="list")
@plugins.ensure_plugins_are_loaded
def list_() -> None:
"""List existing deployments."""
_list_deployments(cliutils.get_api())
@deployment_app.command()
@cliutils.suppress_warnings
def config(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of the deployment.",
envvar=envutils.ENV_ENV
)
],
) -> None:
"""Display configuration of the deployment.
Output is the configuration of the deployment in a
pretty-printed JSON format.
"""
deploy = cliutils.get_api().deployment.get(deployment=deployment)
result = deploy["config"]
print(json.dumps(result, sort_keys=True, indent=4))
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def show(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of the deployment.",
envvar=envutils.ENV_ENV
)
],
) -> None:
"""Show the credentials of the deployment."""
# TODO(astudenov): make this method platform independent
headers = ["auth_url", "username", "password", "tenant_name",
"region_name", "endpoint_type"]
table_rows = []
deployment = cliutils.get_api().deployment.get(deployment=deployment)
creds = deployment["credentials"]["openstack"][0]
users = creds["users"]
admin = creds["admin"]
credentials = users + [admin] if admin else users
for ep in credentials:
data = ["***" if m == "password" else ep.get(m, "")
for m in headers]
table_rows.append(utils.Struct(**dict(zip(headers, data))))
cliutils.print_list(table_rows, headers)
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def check(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of the deployment.",
envvar=envutils.ENV_ENV
)
],
) -> None:
"""Check all credentials and list all available services."""
def is_field_there(lst: list, field: str) -> bool:
return bool([item for item in lst if field in item])
def print_error(user_type: str, error: dict) -> None:
print("Error while checking %s credentials:" % user_type)
if logging.is_debug():
print(error["trace"])
else:
print("There are no deployments. To create a new deployment, use:"
"\nrally deployment create")
print("\t%s: %s" % (error["etype"], error["msg"]))
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the deployment.")
@envutils.with_default_deployment()
@cliutils.suppress_warnings
def config(self, api, deployment=None):
"""Display configuration of the deployment.
exit_code = 0
Output is the configuration of the deployment in a
pretty-printed JSON format.
"""
deploy = api.deployment.get(deployment=deployment)
result = deploy["config"]
print(json.dumps(result, sort_keys=True, indent=4))
info = cliutils.get_api().deployment.check(deployment=deployment)
for platform in info:
for i, creds in enumerate(info[platform]):
failed = False
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the deployment.")
@envutils.with_default_deployment()
@plugins.ensure_plugins_are_loaded
def show(self, api, deployment=None):
"""Show the credentials of the deployment."""
# TODO(astudenov): make this method platform independent
n = "" if len(info[platform]) == 1 else " #%s" % (i + 1)
header = "Platform %s%s:" % (platform, n)
print(cliutils.make_header(header))
if "admin_error" in creds:
print_error("admin", creds["admin_error"])
failed = True
if "user_error" in creds:
print_error("users", creds["user_error"])
failed = True
headers = ["auth_url", "username", "password", "tenant_name",
"region_name", "endpoint_type"]
table_rows = []
deployment = api.deployment.get(deployment=deployment)
creds = deployment["credentials"]["openstack"][0]
users = creds["users"]
admin = creds["admin"]
credentials = users + [admin] if admin else users
for ep in credentials:
data = ["***" if m == "password" else ep.get(m, "")
for m in headers]
table_rows.append(utils.Struct(**dict(zip(headers, data))))
cliutils.print_list(table_rows, headers)
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the deployment.")
@envutils.with_default_deployment()
@plugins.ensure_plugins_are_loaded
def check(self, api, deployment=None):
"""Check all credentials and list all available services."""
def is_field_there(lst, field):
return bool([item for item in lst if field in item])
def print_error(user_type, error):
print("Error while checking %s credentials:" % user_type)
if logging.is_debug():
print(error["trace"])
else:
print("\t%s: %s" % (error["etype"], error["msg"]))
exit_code = 0
info = api.deployment.check(deployment=deployment)
for platform in info:
for i, creds in enumerate(info[platform]):
failed = False
n = "" if len(info[platform]) == 1 else " #%s" % (i + 1)
header = "Platform %s%s:" % (platform, n)
print(cliutils.make_header(header))
if "admin_error" in creds:
print_error("admin", creds["admin_error"])
failed = True
if "user_error" in creds:
print_error("users", creds["user_error"])
failed = True
if not failed:
print("Available services:")
formatters = {
"Service": lambda x: x.get("name"),
"Service Type": lambda x: x.get("type"),
"Status": lambda x: x.get("status", "Available")}
if (is_field_there(creds["services"], "type")
and is_field_there(creds["services"], "name")):
headers = ["Service", "Service Type", "Status"]
else:
headers = ["Service", "Status"]
if is_field_there(creds["services"], "version"):
headers.append("Version")
if is_field_there(creds["services"], "description"):
headers.append("Description")
cliutils.print_list(creds["services"], headers,
normalize_field_names=True,
formatters=formatters)
if not failed:
print("Available services:")
formatters = {
"Service": lambda x: x.get("name"),
"Service Type": lambda x: x.get("type"),
"Status": lambda x: x.get("status", "Available")}
if (is_field_there(creds["services"], "type")
and is_field_there(creds["services"], "name")):
headers = ["Service", "Service Type", "Status"]
else:
exit_code = 1
print("\n")
headers = ["Service", "Status"]
return exit_code
if is_field_there(creds["services"], "version"):
headers.append("Version")
def _update_openrc_deployment_file(self, deployment, credential):
openrc_path = os.path.expanduser("~/.rally/openrc-%s" % deployment)
with open(openrc_path, "w+") as env_file:
env_file.write("export OS_AUTH_URL='%(auth_url)s'\n"
"export OS_USERNAME='%(username)s'\n"
"export OS_PASSWORD='%(password)s'\n"
"export OS_TENANT_NAME='%(tenant_name)s'\n"
"export OS_PROJECT_NAME='%(tenant_name)s'\n"
% credential)
if credential.get("region_name"):
env_file.write("export OS_REGION_NAME='%s'\n" %
credential["region_name"])
if credential.get("endpoint_type"):
env_file.write("export OS_ENDPOINT_TYPE='%sURL'\n" %
credential["endpoint_type"])
env_file.write("export OS_INTERFACE='%s'\n" %
credential["endpoint_type"])
if credential.get("endpoint"):
env_file.write("export OS_ENDPOINT='%s'\n" %
credential["endpoint"])
if credential.get("https_cacert"):
env_file.write("export OS_CACERT='%s'\n" %
credential["https_cacert"])
if credential.get("project_domain_name"):
env_file.write("export OS_IDENTITY_API_VERSION=3\n"
"export OS_USER_DOMAIN_NAME='%s'\n"
"export OS_PROJECT_DOMAIN_NAME='%s'\n" %
(credential["user_domain_name"],
credential["project_domain_name"]))
expanded_path = os.path.expanduser("~/.rally/openrc")
if os.path.exists(expanded_path):
os.remove(expanded_path)
os.symlink(openrc_path, expanded_path)
if is_field_there(creds["services"], "description"):
headers.append("Description")
@cliutils.args("--deployment", dest="deployment", type=str,
metavar="<uuid>", required=False,
help="UUID or name of a deployment.")
@plugins.ensure_plugins_are_loaded
def use(self, api, deployment):
"""Set active deployment."""
# TODO(astudenov): make this method platform independent
try:
if not isinstance(deployment, dict):
deployment = api.deployment.get(deployment=deployment)
except exceptions.DBRecordNotFound:
print("Deployment %s is not found." % deployment)
return 1
print("Using deployment: %s" % deployment["uuid"])
cliutils.print_list(creds["services"], headers,
normalize_field_names=True,
formatters=formatters)
else:
exit_code = 1
print("\n")
envutils.update_globals_file(envutils.ENV_DEPLOYMENT,
deployment["uuid"])
envutils.update_globals_file(envutils.ENV_ENV,
deployment["uuid"])
if exit_code:
raise typer.Exit(code=exit_code)
if "openstack" in deployment["credentials"]:
creds = deployment["credentials"]["openstack"][0]
self._update_openrc_deployment_file(
deployment["uuid"], creds["admin"] or creds["users"][0])
print("~/.rally/openrc was updated\n\nHINTS:\n"
"\n* To use standard OpenStack clients, set up your env by "
"running:\n\tsource ~/.rally/openrc\n"
" OpenStack clients are now configured, e.g run:\n\t"
"openstack image list")
@deployment_app.command()
@plugins.ensure_plugins_are_loaded
def use(
deployment: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--deployment",
help="UUID or name of a deployment."
)
],
) -> None:
"""Set active deployment."""
rc = _use(cliutils.get_api(), deployment)
if rc:
raise typer.Exit(code=rc)
+446 -291
View File
@@ -1,5 +1,3 @@
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@@ -15,323 +13,480 @@
import json
import os
import traceback
import typing as t
import prettytable
import typer
from rally import exceptions
from rally.cli import argutils
from rally.cli import cliutils
from rally.cli import envutils
from rally.cli import yamlutils as yaml
from rally.env import env_mgr
env_app = typer.Typer(
name="env", no_args_is_help=False,
help="Set of commands that allow you to manage envs.")
YES = u":-)"
NO = u":-("
MSG_NO_ENVS = ("There are no environments. To create a new environment, "
"use command bellow to create one:\nrally env create")
def _print(msg, silent=False):
def _print(msg: object, silent: bool = False) -> None:
if not silent:
print(msg)
# TODO(boris-42): Wrap all methods to catch EnvManager Exceptions
class EnvCommands(object):
"""Set of commands that allow you to manage envs."""
@cliutils.args("--name", "-n", type=str, required=True,
help="Name of the env.")
@cliutils.args("--description", "-d", type=str, required=False,
help="Env description")
@cliutils.args("--extras", "-e", type=str, required=False,
help="JSON or YAML dict with custom non validate info.")
@cliutils.args("--from-sysenv", action="store_true", dest="from_sysenv",
help="Iterate over all available platforms and check system"
" environment for credentials.")
@cliutils.args("--spec", "-s", type=str, required=False,
metavar="<path>", help="Path to env spec.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.args("--no-use", action="store_false", dest="do_use",
help="Don't set new env as default for future operations.")
def create(self, api, name, description=None, extras=None,
spec=None, from_sysenv=False, to_json=False, do_use=True):
"""Create new environment."""
if spec is not None and from_sysenv:
print("Arguments '--spec' and '--from-sysenv' cannot be used "
"together, use only one of them.")
return 1
spec = spec or {}
if spec:
with open(os.path.expanduser(spec), "rb") as f:
spec = yaml.safe_load(f.read())
if extras:
extras = yaml.safe_load(extras)
if from_sysenv:
result = env_mgr.EnvManager.create_spec_from_sys_environ()
spec = result["spec"]
_print("Your system environment includes specifications of "
"%s platform(s)." % len(spec), to_json)
_print("Discovery information:", to_json)
for p_name, p_result in result["discovery_details"].items():
_print("\t - %s : %s." % (p_name, p_result["message"]),
to_json)
if "traceback" in p_result:
_print("".join(p_result["traceback"]), to_json)
try:
env = env_mgr.EnvManager.create(
name, spec, description=description, extras=extras)
except exceptions.ManagerInvalidSpec as e:
_print("Env spec has wrong format:", to_json)
_print(json.dumps(e.kwargs["spec"], indent=2), to_json)
for err in e.kwargs["errors"]:
_print(err, to_json)
return 1
except Exception:
_print("Something went wrong during env creation:", to_json)
_print(traceback.print_exc(), to_json)
return 1
if do_use:
self._use(env.uuid, to_json)
self._show(env.data, to_json=to_json, only_spec=False)
return 0
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@envutils.with_default_env()
def cleanup(self, api, env=None, to_json=False):
"""Perform disaster cleanup for specified environment.
Cases when Rally can leave undeleted resources after performing
workload:
- Rally execution was interrupted and cleanup was not performed
- The environment or a particular platform became unreachable which
fail Rally execution of cleanup
"""
env = env_mgr.EnvManager.get(env)
_print("Cleaning up resources for %s" % env, to_json)
result = env.cleanup()
if to_json:
print(json.dumps(result, indent=2))
return int(any([p["errors"] for p in result.values()]))
print("Cleaning is finished. See the results bellow.")
return_code = 0
for platform in sorted(result):
cleanup_info = result[platform]
print("\nInformation for %s platform." % platform)
print("=" * 80)
print("Status: %s" % cleanup_info["message"])
for key in ("discovered", "deleted", "failed"):
print("Total %s: %s" % (key, cleanup_info[key]))
if cleanup_info["errors"]:
return_code = 1
errors = "\t- ".join(e["message"]
for e in cleanup_info["errors"])
print("Errors:\n\t- %s" % errors)
return return_code
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@cliutils.args("--skip-cleanup", action="store_true", dest="skip_cleanup",
help="Do not perform platforms cleanup before destroy.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.args("--detailed", action="store_true", dest="detailed",
help="Show detailed information.")
@envutils.with_default_env()
def destroy(self, api, env=None, skip_cleanup=False, to_json=False,
detailed=False):
"""Destroy existing environment."""
env = env_mgr.EnvManager.get(env)
_print("Destroying %s" % env, to_json)
result = env.destroy(skip_cleanup)
return_code = int(result["destroy_info"]["skipped"])
if result["destroy_info"]["skipped"]:
_print("%s Failed to destroy env %s: %s"
% (NO, env, result["destroy_info"]["message"]), to_json)
else:
_print("%s Successfully destroyed env %s" % (YES, env), to_json)
if detailed or to_json:
print(json.dumps(result, indent=2))
return return_code
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@cliutils.args("--force", action="store_true", dest="force",
help="Delete DB records even if env is not destroyed.")
@envutils.with_default_env()
def delete(self, api, env=None, force=False):
"""Deletes all records related to the environment from db."""
env_mgr.EnvManager.get(env).delete(force=force)
# TODO(boris-42): clear env variables if default one is deleted
MSG_NO_ENVS = ("There are no environments. To create a new environment, "
"use command bellow to create one:\nrally env create")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.suppress_warnings
def list(self, api, to_json=False):
"""List existing environments."""
envs = env_mgr.EnvManager.list()
if to_json:
print(json.dumps([env.cached_data for env in envs], indent=2))
elif not envs:
print(self.MSG_NO_ENVS)
else:
cur_env = envutils.get_global(envutils.ENV_ENV)
table = prettytable.PrettyTable()
fields = ["uuid", "name", "status", "created_at", "description"]
table.field_names = fields + ["default"]
for env in envs:
row = [env.cached_data[f] for f in fields]
row.append(cur_env == env.cached_data["uuid"] and "*" or "")
table.add_row(row)
table.sortby = "created_at"
table.reversesort = True
table.align = "l"
print(table.get_string())
def _show(self, env_data, to_json, only_spec):
if only_spec:
print(json.dumps(env_data["spec"], indent=2))
elif to_json:
print(json.dumps(env_data, indent=2))
else:
table = prettytable.PrettyTable()
table.header = False
for k in ["uuid", "name", "status",
"created_at", "updated_at", "description"]:
table.add_row([k, env_data[k]])
table.add_row(["extras", json.dumps(env_data["extras"], indent=2)])
for p, data in env_data["platforms"].items():
table.add_row(["platform: %s" % p,
json.dumps(data["platform_data"], indent=2)])
table.align = "l"
print(table.get_string())
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.args("--only-spec", action="store_true", dest="only_spec",
help="Print only a spec for the environment.")
@cliutils.suppress_warnings
@envutils.with_default_env()
def show(self, api, env=None, to_json=False, only_spec=False):
"""Show base information about the environment record."""
env_data = env_mgr.EnvManager.get(env).data
self._show(env_data, to_json=to_json, only_spec=only_spec)
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@envutils.with_default_env()
def info(self, api, env=None, to_json=False):
"""Retrieve and show environment information."""
env = env_mgr.EnvManager.get(env)
env_info = env.get_info()
return_code = int(any(v.get("error") for v in env_info.values()))
if to_json:
print(json.dumps(env_info, indent=2))
return return_code
def _show(env_data: dict, to_json: bool, only_spec: bool) -> None:
if only_spec:
print(json.dumps(env_data["spec"], indent=2))
elif to_json:
print(json.dumps(env_data, indent=2))
else:
table = prettytable.PrettyTable()
table.field_names = ["platform", "info", "error"]
for platform, data in env_info.items():
table.add_row([
platform, json.dumps(data["info"], indent=2),
data.get("error") or ""
])
table.header = False
for k in ["uuid", "name", "status",
"created_at", "updated_at", "description"]:
table.add_row([k, env_data[k]])
table.add_row(["extras", json.dumps(env_data["extras"], indent=2)])
for p, data in env_data["platforms"].items():
table.add_row(["platform: %s" % p,
json.dumps(data["platform_data"], indent=2)])
table.align = "l"
print(env)
print(table.get_string())
return return_code
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of the env.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
@cliutils.args("--detailed", action="store_true", dest="detailed",
help="Show detailed information.")
@envutils.with_default_env()
def check(self, api, env=None, to_json=False, detailed=False):
"""Check availability of all platforms in environment."""
env = env_mgr.EnvManager.get(env)
data = env.check_health()
available = all(x["available"] for x in data.values())
if to_json:
print(json.dumps(data, indent=2))
return not available
def _use(env_uuid: str, to_json: bool) -> None:
_print("Using environment: %s" % env_uuid, to_json)
envutils.update_globals_file(envutils.ENV_ENV, env_uuid)
def _format_raw(plugin_name, el):
return [
el["available"] and YES or NO,
plugin_name.split("@")[1], el["message"], plugin_name
]
@env_app.command()
def create(
name: t.Annotated[
str,
typer.Option(
"--name", "-n",
help="Name of the env."
)
],
description: t.Annotated[
str | None,
typer.Option(
"--description", "-d",
help="Env description"
)
] = None,
extras: t.Annotated[
str | None,
typer.Option(
"--extras", "-e",
help="JSON or YAML dict with custom non validate info."
)
] = None,
spec: t.Annotated[
str | None,
typer.Option(
"--spec", "-s",
help="Path to env spec."
)
] = None,
from_sysenv: t.Annotated[
bool,
typer.Option(
"--from-sysenv",
help="Iterate over all available platforms and check system "
"environment for credentials."
)
] = False,
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
no_use: t.Annotated[
bool,
typer.Option(
"--no-use",
help="Don't set new env as default for future operations."
)
] = False,
) -> None:
"""Create new environment."""
if spec is not None and from_sysenv:
print("Arguments '--spec' and '--from-sysenv' cannot be used "
"together, use only one of them.")
raise typer.Exit(code=1)
spec_obj: t.Any = spec or {}
if spec:
with open(os.path.expanduser(spec), "rb") as f:
spec_obj = yaml.safe_load(f.read())
extras_obj = yaml.safe_load(extras) if extras else None
if from_sysenv:
result = env_mgr.EnvManager.create_spec_from_sys_environ()
spec_obj = result["spec"]
_print("Your system environment includes specifications of "
"%s platform(s)." % len(spec_obj), to_json)
_print("Discovery information:", to_json)
for p_name, p_result in result["discovery_details"].items():
_print("\t - %s : %s." % (p_name, p_result["message"]), to_json)
if "traceback" in p_result:
_print("".join(p_result["traceback"]), to_json)
try:
env = env_mgr.EnvManager.create(
name, spec_obj, description=description, extras=extras_obj)
except exceptions.ManagerInvalidSpec as e:
_print("Env spec has wrong format:", to_json)
_print(json.dumps(e.kwargs["spec"], indent=2), to_json)
for err in e.kwargs["errors"]:
_print(err, to_json)
raise typer.Exit(code=1)
except Exception:
_print("Something went wrong during env creation:", to_json)
_print(traceback.format_exc(), to_json)
raise typer.Exit(code=1)
if not no_use:
_use(env.uuid, to_json)
_show(env.data, to_json=to_json, only_spec=False)
@env_app.command()
def cleanup(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
) -> None:
"""Perform disaster cleanup for specified environment.
Cases when Rally can leave undeleted resources after performing
workload:
- Rally execution was interrupted and cleanup was not performed
- The environment or a particular platform became unreachable which
fail Rally execution of cleanup
"""
env_obj = env_mgr.EnvManager.get(env)
_print("Cleaning up resources for %s" % env_obj, to_json)
result = env_obj.cleanup()
if to_json:
print(json.dumps(result, indent=2))
if any(p["errors"] for p in result.values()):
raise typer.Exit(code=1)
return
print("Cleaning is finished. See the results bellow.")
return_code = 0
for platform in sorted(result):
cleanup_info = result[platform]
print("\nInformation for %s platform." % platform)
print("=" * 80)
print("Status: %s" % cleanup_info["message"])
for key in ("discovered", "deleted", "failed"):
print("Total %s: %s" % (key, cleanup_info[key]))
if cleanup_info["errors"]:
return_code = 1
errors = "\t- ".join(e["message"]
for e in cleanup_info["errors"])
print("Errors:\n\t- %s" % errors)
if return_code:
raise typer.Exit(code=1)
@env_app.command()
def destroy(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
skip_cleanup: t.Annotated[
bool,
typer.Option(
"--skip-cleanup",
help="Do not perform platforms cleanup before destroy."
)
] = False,
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
detailed: t.Annotated[
bool,
typer.Option(
"--detailed",
help="Show detailed information."
)
] = False,
) -> None:
"""Destroy existing environment."""
env_obj = env_mgr.EnvManager.get(env)
_print("Destroying %s" % env_obj, to_json)
result = env_obj.destroy(skip_cleanup)
return_code = int(result["destroy_info"]["skipped"])
if result["destroy_info"]["skipped"]:
_print("%s Failed to destroy env %s: %s"
% (NO, env_obj, result["destroy_info"]["message"]), to_json)
else:
_print("%s Successfully destroyed env %s" % (YES, env_obj), to_json)
if detailed or to_json:
print(json.dumps(result, indent=2))
if return_code:
raise typer.Exit(code=return_code)
@env_app.command()
def delete(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
force: t.Annotated[
bool,
typer.Option(
"--force",
help="Delete DB records even if env is not destroyed."
)
] = False,
) -> None:
"""Delete all records related to the environment from db."""
env_mgr.EnvManager.get(env).delete(force=force)
@env_app.command(name="list")
@cliutils.suppress_warnings
def list_(
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
) -> None:
"""List existing environments."""
envs = env_mgr.EnvManager.list()
if to_json:
print(json.dumps([env.cached_data for env in envs], indent=2))
elif not envs:
print(MSG_NO_ENVS)
else:
cur_env = envutils.get_global(envutils.ENV_ENV)
table = prettytable.PrettyTable()
if detailed:
table.field_names = ["Available", "Platform", "Message", "Plugin"]
for plugin_name, r in data.items():
table.add_row(_format_raw(plugin_name, r))
else:
table.field_names = ["Available", "Platform", "Message"]
for plugin_name, r in data.items():
table.add_row(_format_raw(plugin_name, r)[:3])
fields = ["uuid", "name", "status", "created_at", "description"]
table.field_names = fields + ["default"]
for env in envs:
row = [env.cached_data[f] for f in fields]
row.append(cur_env == env.cached_data["uuid"] and "*" or "")
table.add_row(row)
table.sortby = "created_at"
table.reversesort = True
table.align = "l"
table.align["available"] = "c"
table.sortby = "Platform"
print("%s %s" % (env, available and YES or NO))
print(table.get_string())
if not available and detailed:
for name, p_data in data.items():
if p_data["available"]:
continue
print("-" * 4)
print("Plugin %s raised exception:" % name)
print("".join(p_data["traceback"]))
return not available
@cliutils.args("--env", dest="env", type=str,
metavar="<uuid>", required=False,
help="UUID or name of a env.")
@cliutils.args("--json", action="store_true", dest="to_json",
help="Format output as JSON.")
def use(self, api, env, to_json=False):
"""Set default environment."""
try:
env = env_mgr.EnvManager.get(env)
except exceptions.DBRecordNotFound:
_print("Can't use non existing environment %s." % env, to_json)
return 1
self._use(env.uuid, to_json)
@env_app.command()
@cliutils.suppress_warnings
def show(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
only_spec: t.Annotated[
bool,
typer.Option(
"--only-spec",
help="Print only a spec for the environment."
)
] = False,
) -> None:
"""Show base information about the environment record."""
env_data = env_mgr.EnvManager.get(env).data
_show(env_data, to_json=to_json, only_spec=only_spec)
def _use(self, env_uuid, to_json):
_print("Using environment: %s" % env_uuid, to_json)
envutils.update_globals_file(envutils.ENV_ENV, env_uuid)
@env_app.command()
def info(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
) -> None:
"""Retrieve and show environment information."""
env_obj = env_mgr.EnvManager.get(env)
env_info = env_obj.get_info()
return_code = int(any(v.get("error") for v in env_info.values()))
if to_json:
print(json.dumps(env_info, indent=2))
if return_code:
raise typer.Exit(code=return_code)
return
table = prettytable.PrettyTable()
table.field_names = ["platform", "info", "error"]
for platform, data in env_info.items():
table.add_row([
platform, json.dumps(data["info"], indent=2),
data.get("error") or ""
])
table.align = "l"
print(env_obj)
print(table.get_string())
if return_code:
raise typer.Exit(code=return_code)
@env_app.command()
def check(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
envvar=envutils.ENV_ENV,
help="UUID or name of the env."
)
],
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
detailed: t.Annotated[
bool,
typer.Option(
"--detailed",
help="Show detailed information."
)
] = False,
) -> None:
"""Check availability of all platforms in environment."""
env_obj = env_mgr.EnvManager.get(env)
data = env_obj.check_health()
available = all(x["available"] for x in data.values())
if to_json:
print(json.dumps(data, indent=2))
if not available:
raise typer.Exit(code=1)
return
def _format_raw(plugin_name: str, el: dict) -> list:
return [
el["available"] and YES or NO,
plugin_name.split("@")[1], el["message"], plugin_name
]
table = prettytable.PrettyTable()
if detailed:
table.field_names = ["Available", "Platform", "Message", "Plugin"]
for plugin_name, r in data.items():
table.add_row(_format_raw(plugin_name, r))
else:
table.field_names = ["Available", "Platform", "Message"]
for plugin_name, r in data.items():
table.add_row(_format_raw(plugin_name, r)[:3])
table.align = "l"
table.align["available"] = "c"
table.sortby = "Platform"
print("%s %s" % (env_obj, available and YES or NO))
print(table.get_string())
if not available and detailed:
for name, p_data in data.items():
if p_data["available"]:
continue
print("-" * 4)
print("Plugin %s raised exception:" % name)
print("".join(p_data["traceback"]))
if not available:
raise typer.Exit(code=1)
@env_app.command()
def use(
env: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--env",
help="UUID or name of the env."
)
],
to_json: t.Annotated[
bool,
typer.Option(
"--json",
help="Format output as JSON."
)
] = False,
) -> None:
"""Set default environment."""
try:
env_obj = env_mgr.EnvManager.get(env)
except exceptions.DBRecordNotFound:
_print("Can't use non existing environment %s." % env, to_json)
raise typer.Exit(code=1)
_use(env_obj.uuid, to_json)
+109 -84
View File
@@ -13,100 +13,125 @@
# License for the specific language governing permissions and limitations
# under the License.
import typing as t
import typer
from rally import exceptions
from rally import plugins
from rally.cli import argutils
from rally.cli import cliutils
from rally.common import utils
from rally.common.plugin import plugin
class PluginCommands(object):
"""Set of commands that allow you to manage Rally plugins."""
plugin_app = typer.Typer(
name="plugin", no_args_is_help=False,
help="Set of commands that allow you to manage Rally plugins.")
@staticmethod
def _print_plugins_list(plugin_list):
formatters = {
"Name": lambda p: p.get_name(),
"Platform": lambda p: p.get_platform(),
"Title": lambda p: p.get_info()["title"],
"Plugin base": lambda p: p._get_base().__name__
}
cliutils.print_list(plugin_list, formatters=formatters,
normalize_field_names=True,
fields=["Plugin base", "Name", "Platform",
"Title"])
def _print_plugins_list(plugin_list: list) -> None:
formatters = {
"Name": lambda p: p.get_name(),
"Platform": lambda p: p.get_platform(),
"Title": lambda p: p.get_info()["title"],
"Plugin base": lambda p: p._get_base().__name__
}
@cliutils.args("--name", dest="name", type=str,
help="Plugin name.")
@cliutils.args("--platform", dest="platform", type=str,
help="Plugin platform.")
@plugins.ensure_plugins_are_loaded
def show(self, api, name, platform=None):
"""Show detailed information about a Rally plugin."""
cliutils.print_list(plugin_list, formatters=formatters,
normalize_field_names=True,
fields=["Plugin base", "Name", "Platform", "Title"])
@plugin_app.command()
@plugins.ensure_plugins_are_loaded
def show(
name: t.Annotated[
str,
argutils.ArgumentOrKeyword(
"--name",
help="Plugin name."
)
],
platform: t.Annotated[
str | None,
typer.Option(
help="Plugin platform."
)
] = None,
) -> None:
"""Show detailed information about a Rally plugin."""
name_lw = name.lower()
all_plugins = plugin.Plugin.get_all(platform=platform)
found = [p for p in all_plugins if name_lw in p.get_name().lower()]
exact_match = [p for p in found if name_lw == p.get_name().lower()]
if not found:
if platform:
print("Plugin %(name)s@%(platform)s not found"
% {"name": name, "platform": platform})
else:
print("Plugin %s not found at any platform" % name)
raise typer.Exit(code=exceptions.PluginNotFound.error_code)
elif len(found) == 1 or exact_match:
plugin_ = found[0] if len(found) == 1 else exact_match[0]
plugin_info = plugin_.get_info()
print(cliutils.make_header(plugin_info["title"]))
print("NAME\n\t%s" % plugin_info["name"])
print("PLATFORM\n\t%s" % plugin_info["platform"])
print("MODULE\n\t%s" % plugin_info["module"])
if plugin_info["description"]:
print("DESCRIPTION\n\t", end="")
print("\n\t".join(plugin_info["description"].split("\n")))
if plugin_info["parameters"]:
print("PARAMETERS")
rows = [utils.Struct(name=p["name"], description=p["doc"])
for p in plugin_info["parameters"]]
cliutils.print_list(rows, fields=["name", "description"],
sortby_index=None)
else:
print("Multiple plugins found:")
_print_plugins_list(found)
raise typer.Exit(code=exceptions.MultiplePluginsFound.error_code)
@plugin_app.command(name="list")
@plugins.ensure_plugins_are_loaded
def list_(
name: t.Annotated[
str | None,
typer.Argument(
help="List only plugins that match the given name."
)
] = None,
platform: t.Annotated[
str | None,
typer.Option(
help="List only plugins that are in the specified platform."
)
] = None,
base_cls: t.Annotated[
str | None,
typer.Option(
"--plugin-base",
help="Plugin base class."
)
] = None,
) -> None:
"""List all Rally plugins that match name and platform."""
all_plugins = plugin.Plugin.get_all(platform=platform)
matched = all_plugins
if name:
name_lw = name.lower()
all_plugins = plugin.Plugin.get_all(platform=platform)
found = [p for p in all_plugins if name_lw in p.get_name().lower()]
exact_match = [p for p in found if name_lw == p.get_name().lower()]
matched = [p for p in all_plugins if name_lw in p.get_name().lower()]
if not found:
if platform:
print(
"Plugin %(name)s@%(platform)s not found"
% {"name": name, "platform": platform}
)
else:
print("Plugin %s not found at any platform" % name)
return exceptions.PluginNotFound.error_code
if base_cls:
matched = [p for p in matched if p._get_base().__name__ == base_cls]
elif len(found) == 1 or exact_match:
plugin_ = found[0] if len(found) == 1 else exact_match[0]
plugin_info = plugin_.get_info()
print(cliutils.make_header(plugin_info["title"]))
print("NAME\n\t%s" % plugin_info["name"])
print("PLATFORM\n\t%s" % plugin_info["platform"])
print("MODULE\n\t%s" % plugin_info["module"])
if plugin_info["description"]:
print("DESCRIPTION\n\t", end="")
print("\n\t".join(plugin_info["description"].split("\n")))
if plugin_info["parameters"]:
print("PARAMETERS")
rows = [utils.Struct(name=p["name"],
description=p["doc"])
for p in plugin_info["parameters"]]
cliutils.print_list(rows, fields=["name", "description"],
sortby_index=None)
else:
print("Multiple plugins found:")
self._print_plugins_list(found)
return exceptions.MultiplePluginsFound.error_code
@cliutils.args(
"--name", dest="name", type=str,
help="List only plugins that match the given name.")
@cliutils.args(
"--platform", dest="platform", type=str,
help="List only plugins that are in the specified platform.")
@cliutils.args(
"--plugin-base", dest="base_cls", type=str,
help="Plugin base class.")
@plugins.ensure_plugins_are_loaded
def list(self, api, name=None, platform=None, base_cls=None):
"""List all Rally plugins that match name and platform."""
all_plugins = plugin.Plugin.get_all(platform=platform)
matched = all_plugins
if name:
name_lw = name.lower()
matched = [p for p in all_plugins
if name_lw in p.get_name().lower()]
if base_cls:
matched = [p for p in matched
if p._get_base().__name__ == base_cls]
if not all_plugins:
print("Platform %s not found" % platform)
elif not matched:
print("Plugin %s not found" % name)
else:
self._print_plugins_list(matched)
if not all_plugins:
print("Platform %s not found" % platform)
elif not matched:
print("Plugin %s not found" % name)
else:
_print_plugins_list(matched)
+997 -721
View File
File diff suppressed because it is too large Load Diff
+1089 -766
View File
File diff suppressed because it is too large Load Diff
+15 -70
View File
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
import inspect
import os
from rally import exceptions
@@ -26,9 +24,7 @@ ENV_DEPLOYMENT = "RALLY_DEPLOYMENT"
ENV_TASK = "RALLY_TASK"
ENV_VERIFIER = "RALLY_VERIFIER"
ENV_VERIFICATION = "RALLY_VERIFICATION"
ENVVARS = [ENV_ENV, ENV_DEPLOYMENT, ENV_TASK, ENV_VERIFIER, ENV_VERIFICATION]
MSG_MISSING_ARG = "Missing argument: --%(arg_name)s"
ENVVARS = (ENV_ENV, ENV_DEPLOYMENT, ENV_TASK, ENV_VERIFIER, ENV_VERIFICATION)
def _read_env_file(path, except_env=None):
@@ -45,8 +41,7 @@ def _read_env_file(path, except_env=None):
with open(path, "r") as env_file:
content = env_file.readlines()
for line in content:
if except_env is None or not line.startswith("%s=" %
except_env):
if except_env is None or not line.startswith(f"{except_env}="):
output.append(line)
return output
@@ -96,7 +91,7 @@ def update_globals_file(key, value):
if not os.path.exists(dir):
os.makedirs(dir)
expanded_path = os.path.join(dir, "globals")
_update_env_file(expanded_path, key, "%s\n" % value)
_update_env_file(expanded_path, key, f"{value}\n")
def clear_global(global_key):
@@ -117,71 +112,21 @@ def get_global(global_key, do_raise=False):
_load_env_file(os.path.expanduser(PATH_GLOBALS))
value = os.environ.get(global_key)
if not value and do_raise:
raise exceptions.InvalidArgumentsException("%s env is missing"
% global_key)
raise exceptions.InvalidArgumentsException(
f"{global_key} env is missing"
)
return value
def default_from_global(arg_name, env_name,
cli_arg_name,
message=MSG_MISSING_ARG):
def wrapper(func):
def load_globals():
"""Load persisted Rally globals (``~/.rally/globals``) into os.environ."""
@functools.wraps(func)
def inner(*args, **kwargs):
params = list(inspect.signature(func).parameters)
id_arg_index = params.index(arg_name)
for line in _read_env_file(os.path.expanduser(PATH_GLOBALS)):
key, _, value = line.partition("=")
os.environ.setdefault(key, value.rstrip())
args = list(args)
if ((len(args) <= id_arg_index or args[id_arg_index] is None)
and arg_name not in kwargs):
kwargs[arg_name] = get_global(env_name)
if not kwargs[arg_name]:
print(message % {"arg_name": cli_arg_name})
return 1
return func(*args, **kwargs)
return inner
return wrapper
def with_default_env():
# NOTE(boris-42): This allows smooth transition from deployment to env
# set ENV_ENV from ENV_DEPLOYMENT if ENV is not presented
# This should be removed with rally env command
if not get_global(ENV_ENV):
deployment = get_global(ENV_DEPLOYMENT)
if deployment:
os.environ[ENV_ENV] = deployment
return default_from_global(
"env", ENV_ENV, "env",
message="There is no default env. To set it use command:\n"
"\trally env use <env_uuid>|<env_name>\n"
"or pass uuid or name to your command using --%(arg_name)s")
def with_default_deployment(cli_arg_name="uuid"):
# NOTE(boris-42): This allows smooth transition from deployment to env
# set ENV_ENV from ENV_DEPLOYMENT and use ENV_ENV
# This should be removed with rally env command
if not get_global(ENV_ENV):
deployment = get_global(ENV_DEPLOYMENT)
if deployment:
os.environ[ENV_ENV] = deployment
return default_from_global(
"deployment", ENV_ENV, cli_arg_name,
message="There is no default deployment.\n"
"\tPlease use command:\n"
"\trally deployment use <deployment_uuid>|<deployment_name>\n"
"or pass uuid of deployment to the --%(arg_name)s "
"argument of this command")
def with_default_verifier_id(cli_arg_name="id"):
return default_from_global("verifier_id", ENV_VERIFIER, cli_arg_name)
with_default_task_id = default_from_global("task_id", ENV_TASK, "uuid")
with_default_verification_uuid = default_from_global("verification_uuid",
ENV_VERIFICATION, "uuid")
# set ENV_ENV from ENV_DEPLOYMENT and use ENV_ENV
# This should be removed with rally env command
if not os.environ.get(ENV_ENV) and os.environ.get(ENV_DEPLOYMENT):
os.environ[ENV_ENV] = os.environ[ENV_DEPLOYMENT]
+210 -18
View File
@@ -15,30 +15,222 @@
"""CLI interface for Rally."""
import inspect
import re
import sys
import typing as t
import jsonschema
import sqlalchemy.exc
import typer
from rally import api as rally_api
from rally import exceptions
from rally.cli import argutils
from rally.cli import cliutils
from rally.cli.commands import db
from rally.cli.commands import deployment
from rally.cli.commands import env
from rally.cli.commands import plugin
from rally.cli.commands import task
from rally.cli.commands import verify
from rally.cli import envutils
from rally.cli.commands.db import db_app
from rally.cli.commands.deployment import deployment_app
from rally.cli.commands.env import env_app
from rally.cli.commands.plugin import plugin_app
from rally.cli.commands.task import task_app
from rally.cli.commands.verify import verify_app
from rally.common import cfg
from rally.common import logging
categories = {
"db": db.DBCommands,
"env": env.EnvCommands,
"deployment": deployment.DeploymentCommands,
"plugin": plugin.PluginCommands,
"task": task.TaskCommands,
"verify": verify.VerifyCommands
}
LOG = logging.getLogger(__name__)
def main():
return cliutils.run(sys.argv, categories)
if t.TYPE_CHECKING:
import typer.core
if __name__ == "__main__":
sys.exit(main())
# ``no_args_is_help=False`` -> a bare ``rally`` / ``rally task`` errors with
# "Missing command." (exit 2) instead of printing help.
app = typer.Typer(name="rally", help="Rally command-line interface.",
no_args_is_help=False, add_completion=True)
app.add_typer(db_app, name="db")
app.add_typer(deployment_app, name="deployment")
app.add_typer(env_app, name="env")
app.add_typer(plugin_app, name="plugin")
app.add_typer(task_app, name="task")
app.add_typer(verify_app, name="verify")
@app.command(name="version")
def print_version() -> None:
"""Print the Rally version."""
from rally.common import version
lines = ["Rally version: %s" % version.__version__]
packages = version.plugins_versions()
if packages:
lines.append("\nInstalled Plugins:")
lines.extend("\t%s: %s" % p for p in sorted(packages.items()))
print("\n".join(lines))
def _version_callback(value: bool) -> None:
if value:
print_version()
raise typer.Exit()
def _expand_signature(
build_params: t.Callable[[], list[inspect.Parameter]]
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
"""Replace a function's ``**kwargs`` with dynamically-generated parameters.
typer reads the callback signature via :func:`inspect.signature`, so the
fixed options can be written as an ordinary signature while the variable
``**kwargs`` tail is swapped for whatever ``build_params`` yields (here the
``oslo.log`` CLI options generated from oslo's own ``Opt`` objects, so
``--log-file`` etc. stay in sync). The generated options arrive as keyword
arguments -- i.e. in the real ``**kwargs`` at call time.
"""
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
signature = inspect.signature(func)
fixed = [p for p in signature.parameters.values()
if p.kind is not inspect.Parameter.VAR_KEYWORD]
func.__signature__ = signature.replace( # type: ignore[attr-defined]
parameters=fixed + list(build_params()))
return func
return decorator
@app.callback()
@_expand_signature(logging.build_cli_params)
def bootstrap(
ctx: typer.Context,
config_file: t.Annotated[
list[str] | None,
typer.Option(
help="Path to a config file. Repeatable; later files take "
"precedence."
)
] = None,
config_dir: t.Annotated[
list[str] | None,
typer.Option(
help="Path to a config directory. Repeatable."
)
] = None,
plugin_paths: t.Annotated[
list[str] | None,
typer.Option(
envvar="RALLY_PLUGIN_PATHS",
help="Additional custom plugin locations."
)
] = None,
version: t.Annotated[
bool,
typer.Option(
"--version",
callback=_version_callback,
is_eager=True,
help="Print the Rally version and exit."
)
] = False,
**kwargs: t.Any,
) -> None:
"""Build the API and expose it to the sub-commands."""
if any(opt in sys.argv[1:] for opt in ctx.help_option_names):
return
envutils.load_globals()
config_args: list[str] = []
for path in config_file or []:
config_args += ["--config-file", path]
for path in config_dir or []:
config_args += ["--config-dir", path]
config_args += logging.to_oslo_argv(kwargs)
paths = None
if plugin_paths:
paths = [p for item in plugin_paths
for p in item.split(",") if p]
# do not run database check on commands that does not need that
skip_db_check = ctx.invoked_subcommand in ("db", "plugin", "version")
try:
api = rally_api.API(config_args=config_args, plugin_paths=paths,
skip_db_check=skip_db_check)
except exceptions.RallyException as e:
print(e)
raise typer.Exit(code=2)
cliutils.set_api(api)
def _eat_all(param: "typer.core.TyperOption") -> None:
"""Make one multi-value option consume space-separated values.
typer options are repeated-flag by default (``--tag a --tag b``); Rally
historically accepted the space-separated ``--tag a b c`` (argparse
``nargs="+"``). There is no per-parameter hook for this, so we override
the option's parser to grab the following tokens up to the next flag.
"""
original_add_to_parser = param.add_to_parser
def add_to_parser(parser: t.Any, ctx: t.Any) -> None:
original_add_to_parser(parser, ctx)
for opt in param.opts:
handler = parser._long_opt.get(opt) or parser._short_opt.get(opt)
if handler is None:
continue
previous_process = handler.process
def process(value: t.Any, state: t.Any,
_prev: t.Any = previous_process) -> None:
values = [value]
while state.rargs and not state.rargs[0].startswith("-"):
values.append(state.rargs.pop(0))
for item in values:
_prev(item, state)
handler.process = process
break
param.add_to_parser = add_to_parser # type: ignore[method-assign]
def _install_multivalue(
command: "typer.core.TyperGroup | typer.core.TyperCommand"
) -> None:
"""Apply :func:`_eat_all` to every multi-value option in the tree."""
for _path, _leaf, params in cliutils.iter_commands(command):
for param in params:
if param.multiple and param.opts and param.opts[0].startswith("-"):
_eat_all(param)
def main() -> None:
cli: "typer.core.TyperGroup" = (
typer.main.get_command(app) # type: ignore[assignment]
)
_install_multivalue(cli)
argutils.install(cli)
try:
cli()
except (OSError, TypeError, ValueError, exceptions.RallyException,
jsonschema.ValidationError) as e:
if (logging.is_debug()
and not isinstance(e, exceptions.InvalidTaskConfig)):
LOG.exception("Unexpected exception in CLI")
else:
print(e)
sys.exit(getattr(e, "error_code", 1))
except sqlalchemy.exc.OperationalError as e:
if logging.is_debug():
LOG.exception("Something went wrong with the database")
print(e)
print("Looks like Rally can't connect to its DB.")
print("Make sure the connection string in rally.conf is proper:")
print(re.sub("//[^@]*@", "//**:**@", cfg.CONF.database.connection))
sys.exit(1)
except Exception:
print("Command failed, please check log for more info")
raise
if __name__ == "__main__": # pragma: no cover
main()
+84 -2
View File
@@ -13,10 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import dataclasses
import functools
import traceback
import typing as t
import warnings
from oslo_log import _options as log_options
from oslo_log import handlers
from oslo_log import log as oslogging
@@ -173,7 +176,7 @@ class CatcherHandler(log.handlers.BufferingHandler):
self.buffer.append(record)
class LogCatcher(object):
class LogCatcher:
"""Context manager that catches log messages.
User can make an assertion on their content or fetch them all.
@@ -340,5 +343,84 @@ def log_deprecated_module(target, new_module, release):
)
def is_debug():
def is_debug() -> bool:
return CONF.debug or CONF.rally_debug
_CLI_OPTS = [
*DEBUG_OPTS, # --rally-debug
*log_options.common_cli_opts, # --debug/-d
*log_options.logging_cli_opts, # --log-file, --log-dir, ...
]
_CLI_TYPES: dict[type, t.Any] = {
cfg.BoolOpt: bool,
cfg.StrOpt: str | None,
cfg.IntOpt: int | None,
cfg.ListOpt: list | None,
cfg.MultiStrOpt: list | None,
}
@dataclasses.dataclass(frozen=True)
class _FieldInfo:
oslo_name: str
is_bool: bool
is_list: bool
_CLI_FIELDS: dict[str, _FieldInfo] = {}
def build_cli_params() -> list:
"""Return the oslo.log CLI options as ``inspect.Parameter`` objects.
The rally CLI callback forges these onto its ``__signature__`` before typer
reads it, so ``--log-file``/``--log-dir``/... are generated from oslo's own
``Opt`` objects (kept in sync with whatever oslo.log offers) instead of
hand-declared. Also populates the metadata consumed by
`to_oslo_argv`. ``typer`` is imported lazily so importing this
module (which happens almost everywhere) stays cheap.
"""
import inspect
import typer
params = []
for opt in _CLI_OPTS:
typ = _CLI_TYPES.get(type(opt), str | None)
# Only booleans carry a default; every other option defaults to
# ``None`` and lets oslo apply its own default when it re-parses argv.
default = opt.default if isinstance(opt, cfg.BoolOpt) else None
help_text = (opt.help or "").strip().split("\n")[0]
option = typer.Option("--%s" % opt.name, help=help_text)
params.append(
inspect.Parameter(
opt.dest,
inspect.Parameter.KEYWORD_ONLY,
default=default,
annotation=t.Annotated[typ, option]
)
)
_CLI_FIELDS[opt.dest] = _FieldInfo(
oslo_name=opt.name,
is_bool=isinstance(opt, cfg.BoolOpt),
is_list=isinstance(opt, (cfg.ListOpt, cfg.MultiStrOpt)))
return params
def to_oslo_argv(values: t.Mapping) -> list[str]:
"""Rebuild the oslo.config ``argv`` slice from the parsed logging options.
``values`` maps each option's ``dest`` to the value typer parsed for it.
"""
argv: list[str] = []
for name, info in _CLI_FIELDS.items():
value = values.get(name)
if info.is_bool:
if value:
argv.append("--%s" % info.oslo_name)
elif value is not None:
for item in (value if info.is_list else [value]):
argv += ["--%s" % info.oslo_name, str(item)]
return argv
+1
View File
@@ -18,4 +18,5 @@ PyYAML # MIT
python-subunit # Apache-2.0 or BSD
requests!=2.20.0,!=2.24.0 # Apache-2.0
SQLAlchemy>=2 # MIT
typer>=0.26.0 # MIT
virtualenv!=16.3.0 # MIT
+4 -4
View File
@@ -22,14 +22,14 @@ from rally.utils import encodeutils
class CLITestCase(unittest.TestCase):
def test_rally_cli(self):
# ``rally`` with no sub-command prints the top-level help.
try:
subprocess.check_output(["rally"], stderr=subprocess.STDOUT)
output = encodeutils.safe_decode(
subprocess.check_output(["rally"], stderr=subprocess.STDOUT))
except subprocess.CalledProcessError as e:
output = encodeutils.safe_decode(e.output)
else:
self.fail("It should ve non-zero exit code.")
self.assertIn("the following arguments are required: category", output)
self.assertIn("Usage: rally", output)
def test_version_cli(self):
output = encodeutils.safe_decode(
+19 -19
View File
@@ -16,57 +16,57 @@
from unittest import mock
from rally.cli.commands import db
from tests.unit import fakes
from tests.unit import test
class DBCommandsTestCase(test.TestCase):
def setUp(self):
super(DBCommandsTestCase, self).setUp()
self.db_commands = db.DBCommands()
self.fake_api = fakes.FakeAPI()
@mock.patch("rally.cli.commands.db._print_connection")
@mock.patch("rally.cli.commands.db.envutils")
@mock.patch("rally.cli.commands.db.db.schema")
def test_recreate(self, mock_db_schema, mock_envutils):
self.db_commands.recreate(self.fake_api)
def test_recreate(self, mock_db_schema, mock_envutils,
mock__print_connection):
db.recreate()
db_calls = [mock.call.schema_cleanup(),
mock.call.schema_create()]
self.assertEqual(db_calls, mock_db_schema.mock_calls)
envutils_calls = [mock.call.clear_env()]
self.assertEqual(envutils_calls, mock_envutils.mock_calls)
@mock.patch("rally.cli.commands.db._print_connection")
@mock.patch("rally.cli.commands.db.db.schema")
def test_create(self, mock_db_schema):
self.db_commands.create(self.fake_api)
def test_create(self, mock_db_schema, mock__print_connection):
db.create()
calls = [mock.call.schema_create()]
self.assertEqual(calls, mock_db_schema.mock_calls)
@mock.patch("rally.cli.commands.db._print_connection")
@mock.patch("rally.cli.commands.db.db.schema")
def test_ensure_create(self, mock_db_schema):
def test_ensure_create(self, mock_db_schema, mock__print_connection):
mock_db_schema.schema_revision.return_value = None
self.db_commands.ensure(self.fake_api)
db.ensure()
calls = [mock.call.schema_revision(),
mock.call.schema_create()]
self.assertEqual(calls, mock_db_schema.mock_calls)
@mock.patch("rally.cli.commands.db._print_connection")
@mock.patch("rally.cli.commands.db.db.schema")
def test_ensure_exists(self, mock_db_schema):
def test_ensure_exists(self, mock_db_schema, mock__print_connection):
mock_db_schema.schema_revision.return_value = "revision"
self.db_commands.ensure(self.fake_api)
db.ensure()
calls = [mock.call.schema_revision()]
self.assertEqual(calls, mock_db_schema.mock_calls)
@mock.patch("rally.cli.commands.db._print_connection")
@mock.patch("rally.cli.commands.db.db.schema")
def test_upgrade(self, mock_db_schema):
self.db_commands.upgrade(self.fake_api)
def test_upgrade(self, mock_db_schema, mock__print_connection):
db.upgrade()
calls = [mock.call.schema_upgrade()]
mock_db_schema.assert_has_calls(calls)
@mock.patch("rally.cli.commands.db.db.schema")
def test_revision(self, mock_db_schema):
self.db_commands.revision(self.fake_api)
db.revision()
calls = [mock.call.schema_revision()]
mock_db_schema.assert_has_calls(calls)
@@ -74,8 +74,8 @@ class DBCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.db.cfg.CONF.database")
def test_show(self, mock_conf_database, mock_print):
mock_conf_database.connection = "http://aaa:bbb@testing.com:888"
self.db_commands.show(self.fake_api)
db.show()
mock_print.assert_called_once_with("http://**:**@testing.com:888")
mock_print.reset_mock()
self.db_commands.show(self.fake_api, show_creds=True)
db.show(creds=True)
mock_print.assert_called_once_with("http://aaa:bbb@testing.com:888")
+38 -62
View File
@@ -30,31 +30,31 @@ from tests.unit import test
class DeploymentCommandsTestCase(test.TestCase):
def setUp(self):
super(DeploymentCommandsTestCase, self).setUp()
self.deployment = deployment.DeploymentCommands()
self.fake_api = fakes.FakeAPI()
cliutils.set_api(self.fake_api)
@mock.patch.dict(os.environ, {"RALLY_DEPLOYMENT": "my_deployment_id"})
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
@mock.patch("rally.cli.commands.deployment._list_deployments")
@mock.patch("rally.cli.commands.deployment.open",
side_effect=mock.mock_open(read_data="{\"some\": \"json\"}"),
create=True)
def test_create(self, mock_open, mock_deployment_commands_list):
self.deployment.create(self.fake_api, "fake_deploy", False,
"path_to_config.json")
def test_create(self, mock_open, mock__list_deployments):
deployment.create(name="fake_deploy",
filename="path_to_config.json")
self.fake_api.deployment.create.assert_called_once_with(
config={"some": "json"}, name="fake_deploy")
@mock.patch.dict(os.environ, {"RALLY_DEPLOYMENT": "my_deployment_id"})
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
def test_create_empty(self, mock_deployment_commands_list):
self.deployment.create(self.fake_api, "fake_deploy")
@mock.patch("rally.cli.commands.deployment._list_deployments")
def test_create_empty(self, mock__list_deployments):
deployment.create(name="fake_deploy")
self.fake_api.deployment.create.assert_called_once_with(
config={}, name="fake_deploy")
@mock.patch("rally.env.env_mgr.EnvManager.create_spec_from_sys_environ",
return_value={"spec": {"auth_url": "http://fake"}})
def test_create_fromenv(self, mock_create_spec_from_sys_environ):
self.deployment.create(self.fake_api, "from_env", True)
deployment.create(name="from_env", fromenv=True)
self.fake_api.deployment.create.assert_called_once_with(
config={"auth_url": "http://fake"},
name="from_env"
@@ -76,7 +76,7 @@ class DeploymentCommandsTestCase(test.TestCase):
with mock.patch.dict("sys.modules",
{"rally_openstack": mock_rally_os}):
self.deployment.create(self.fake_api, "from_env", True)
deployment.create(name="from_env", fromenv=True)
self.fake_api.deployment.create.assert_called_once_with(
config={"existing@openstack": {"another_key": "another"}},
name="from_env"
@@ -84,33 +84,33 @@ class DeploymentCommandsTestCase(test.TestCase):
self.fake_api.deployment.create.reset_mock()
mock_rally_os.__version_tuple__ = (1, 5, 0)
self.deployment.create(self.fake_api, "from_env", True)
deployment.create(name="from_env", fromenv=True)
self.fake_api.deployment.create.assert_called_once_with(
config={"existing@openstack": {"another_key": "another",
"https_key": "some key"}},
name="from_env"
)
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.list")
@mock.patch("rally.cli.commands.deployment.DeploymentCommands.use")
@mock.patch("rally.cli.commands.deployment._list_deployments")
@mock.patch("rally.cli.commands.deployment._use")
@mock.patch("rally.cli.commands.deployment.open",
side_effect=mock.mock_open(read_data="{\"uuid\": \"uuid\"}"),
create=True)
def test_create_and_use(self, mock_open, mock_deployment_commands_use,
mock_deployment_commands_list):
def test_create_and_use(self, mock_open, mock__use,
mock__list_deployments):
self.fake_api.deployment.create.return_value = dict(uuid="uuid")
self.deployment.create(self.fake_api, "fake_deploy", False,
"path_to_config.json", True)
deployment.create(name="fake_deploy",
filename="path_to_config.json")
self.fake_api.deployment.create.assert_called_once_with(
config={"uuid": "uuid"}, name="fake_deploy")
mock_deployment_commands_list.assert_called_once_with(
mock__list_deployments.assert_called_once_with(
self.fake_api, deployment_list=[{"uuid": "uuid"}])
mock_deployment_commands_use.assert_called_once_with(
mock__use.assert_called_once_with(
self.fake_api, self.fake_api.deployment.create.return_value)
def test_recreate(self):
deployment_id = "43924f8b-9371-4152-af9f-4cf02b4eced4"
self.deployment.recreate(self.fake_api, deployment_id)
deployment.recreate(deployment=deployment_id)
self.fake_api.deployment.recreate.assert_called_once_with(
deployment=deployment_id, config=None)
@@ -119,29 +119,16 @@ class DeploymentCommandsTestCase(test.TestCase):
create=True)
def test_recreate_config(self, mock_open):
deployment_id = "43924f8b-9371-4152-af9f-4cf02b4eced4"
self.deployment.recreate(self.fake_api, deployment_id,
filename="my.json")
deployment.recreate(deployment=deployment_id, filename="my.json")
self.fake_api.deployment.recreate.assert_called_once_with(
deployment=deployment_id, config={"some": "json"})
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
def test_recreate_no_deployment_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.deployment.recreate, None)
def test_destroy(self):
deployment_id = "53fd0273-60ce-42e5-a759-36f1a683103e"
self.deployment.destroy(self.fake_api, deployment_id)
deployment.destroy(deployment=deployment_id)
self.fake_api.deployment.destroy.assert_called_once_with(
deployment=deployment_id)
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
def test_destroy_no_deployment_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.deployment.destroy, self.fake_api, None)
@mock.patch("rally.cli.commands.deployment.cliutils.print_list")
@mock.patch("rally.cli.commands.deployment.utils.Struct")
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
@@ -157,7 +144,7 @@ class DeploymentCommandsTestCase(test.TestCase):
"active": "False"}]
self.fake_api.deployment.list.return_value = fake_deployment_list
self.deployment.list(self.fake_api)
deployment.list_()
fake_deployment = fake_deployment_list[0]
fake_deployment["active"] = ""
@@ -181,7 +168,7 @@ class DeploymentCommandsTestCase(test.TestCase):
"status": "deploy->finished",
"active": "True"}]
self.fake_api.deployment.list.return_value = fake_deployment_list
self.deployment.list(self.fake_api)
deployment.list_()
fake_deployment = fake_deployment_list[0]
fake_deployment["active"] = "*"
@@ -197,18 +184,12 @@ class DeploymentCommandsTestCase(test.TestCase):
deployment_id = "fa4a423e-f15d-4d83-971a-89574f892999"
value = {"config": "config"}
self.fake_api.deployment.get.return_value = value
self.deployment.config(self.fake_api, deployment_id)
deployment.config(deployment=deployment_id)
mock_json_dumps.assert_called_once_with(value["config"],
sort_keys=True, indent=4)
self.fake_api.deployment.get.assert_called_once_with(
deployment=deployment_id)
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
def test_config_no_deployment_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.deployment.config, self.fake_api, None)
@mock.patch("rally.cli.commands.deployment.cliutils.print_list")
@mock.patch("rally.cli.commands.deployment.utils.Struct")
def test_show(self, mock_struct, mock_print_list):
@@ -220,11 +201,11 @@ class DeploymentCommandsTestCase(test.TestCase):
"region_name": "r",
"endpoint_type": consts.EndpointType.INTERNAL},
"users": []}
deployment = self.fake_api.deployment.get
deployment.return_value = {"credentials": {"openstack": [
dep_get = self.fake_api.deployment.get
dep_get.return_value = {"credentials": {"openstack": [
{"admin": value["admin"],
"users": []}]}}
self.deployment.show(self.fake_api, deployment_id)
deployment.show(deployment=deployment_id)
self.fake_api.deployment.get.assert_called_once_with(
deployment=deployment_id)
@@ -234,12 +215,6 @@ class DeploymentCommandsTestCase(test.TestCase):
mock_struct.assert_called_once_with(**dict(zip(headers, fake_data)))
mock_print_list.assert_called_once_with([mock_struct()], headers)
@mock.patch("rally.cli.commands.deployment.envutils.get_global")
def test_deploy_no_deployment_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.deployment.show, None)
@mock.patch("os.remove")
@mock.patch("os.symlink")
@mock.patch("os.path.exists", return_value=True)
@@ -260,7 +235,7 @@ class DeploymentCommandsTestCase(test.TestCase):
with mock.patch("rally.cli.commands.deployment.open", mock.mock_open(),
create=True) as mock_file:
self.deployment.use(self.fake_api, deployment_id)
deployment.use(deployment_id)
self.assertEqual(3, mock_path_exists.call_count)
mock__update_env_file.assert_has_calls([
mock.call(os.path.expanduser("~/.rally/globals"),
@@ -306,7 +281,7 @@ class DeploymentCommandsTestCase(test.TestCase):
with mock.patch("rally.cli.commands.deployment.open", mock.mock_open(),
create=True) as mock_file:
self.deployment.use(self.fake_api, deployment_id)
deployment.use(deployment_id)
self.assertEqual(3, mock_path_exists.call_count)
mock__update_env_file.assert_has_calls([
mock.call(os.path.expanduser("~/.rally/globals"),
@@ -332,7 +307,7 @@ class DeploymentCommandsTestCase(test.TestCase):
mock_remove.assert_called_once_with(os.path.expanduser(
"~/.rally/openrc"))
@mock.patch("rally.cli.commands.deployment.DeploymentCommands."
@mock.patch("rally.cli.commands.deployment."
"_update_openrc_deployment_file")
@mock.patch("rally.cli.envutils.update_globals_file")
def test_use_by_name(self, mock_update_globals_file,
@@ -342,7 +317,7 @@ class DeploymentCommandsTestCase(test.TestCase):
"credentials": {"openstack": [fake_credentials]}}
self.fake_api.deployment.list.return_value = [fake_deployment]
self.fake_api.deployment.get.return_value = fake_deployment
status = self.deployment.use(self.fake_api, deployment="fake_name")
status = deployment.use("fake_name")
self.assertIsNone(status)
self.fake_api.deployment.get.assert_called_once_with(
deployment="fake_name")
@@ -358,7 +333,8 @@ class DeploymentCommandsTestCase(test.TestCase):
exc = exceptions.DBRecordNotFound(criteria="uuid: %s" % deployment_id,
table="deployments")
self.fake_api.deployment.get.side_effect = exc
self.assertEqual(1, self.deployment.use(self.fake_api, deployment_id))
with self.assertExitCode(1):
deployment.use(deployment_id)
@mock.patch("rally.cli.commands.deployment.logging.is_debug",
return_value=False)
@@ -393,8 +369,8 @@ class DeploymentCommandsTestCase(test.TestCase):
with mock.patch.object(deployment.cliutils, "print_list",
new=print_list):
self.assertEqual(
1, self.deployment.check(self.fake_api, deployment_uuid))
with self.assertExitCode(1):
deployment.check(deployment=deployment_uuid)
self.assertEqual(
"-----------------------------------------------------------------"
@@ -446,8 +422,8 @@ class DeploymentCommandsTestCase(test.TestCase):
"KeystoneError: connection refused"}}]
}
self.assertEqual(
1, self.deployment.check(self.fake_api, deployment_uuid))
with self.assertExitCode(1):
deployment.check(deployment=deployment_uuid)
self.assertEqual(
"-----------------------------------------------------------------"
+22 -22
View File
@@ -12,33 +12,33 @@
# License for the specific language governing permissions and limitations
# under the License.
import inspect
from rally.cli import main
from rally.cli.commands import db
from rally.cli.commands import deployment
from rally.cli.commands import env
from rally.cli.commands import plugin
from rally.cli.commands import task
from rally.cli.commands import verify
from rally.common.plugin import info
from tests.unit import test
APPS = [db.db_app, deployment.deployment_app, env.env_app, plugin.plugin_app,
task.task_app, verify.verify_app]
class DocstringsTestCase(test.TestCase):
@staticmethod
def _get_all_category_methods(category):
all_methods = inspect.getmembers(
category,
predicate=lambda x: inspect.ismethod(x) or inspect.isfunction(x))
return [m for m in all_methods if not m[0].startswith("_")]
def test_params(self):
for category in main.categories.values():
all_methods = self._get_all_category_methods(category)
for name, method in all_methods:
m_info = info.parse_docstring(method.__doc__)
for app in APPS:
for command in app.registered_commands:
func = command.callback
if func is None or func.__doc__ is None:
continue
m_info = info.parse_docstring(func.__doc__)
if m_info["params"]:
print(m_info)
self.fail("The description of parameters for CLI methods "
"should be transmitted as a 'help' argument of "
"`rally.cli.cliutils.arg` decorator. You should "
"remove descriptions from docstring of "
"`%s.%s.%s`" % (category.__module__,
category.__class__,
name))
self.fail("The description of parameters for CLI commands "
"should be passed as the 'help' argument of a "
"`typer.Option`/`typer.Argument`. You should "
"remove the parameter descriptions from the "
"docstring of `%s:%s`" % (func.__module__,
func.__qualname__))
+61 -62
View File
@@ -27,14 +27,6 @@ from tests.unit import test
class EnvCommandsTestCase(test.TestCase):
def setUp(self):
super(EnvCommandsTestCase, self).setUp()
self.env = env.EnvCommands()
# TODO(boris-42): api argument is not used by EvnCommands
# it's going to be removed when we remove rally.api
# from other commands
self.api = None
@staticmethod
def gen_env_data(uid=None, name=None, description=None,
status=env_mgr.STATUS.INIT, spec=None, extras=None):
@@ -56,30 +48,30 @@ class EnvCommandsTestCase(test.TestCase):
env._print("Test42", silent=False)
mock_print.assert_called_once_with("Test42")
env._print("Test43")
mock_print.assert_has_calls([mock.call("Test42"), mock.call("Test43")])
mock_print.assert_has_calls([mock.call("Test42"),
mock.call("Test43")])
@mock.patch("rally.env.env_mgr.EnvManager.create")
@mock.patch("rally.cli.commands.env.EnvCommands._show")
def test_create_emtpy_use(self, mock_env_commands__show,
@mock.patch("rally.cli.commands.env._show")
def test_create_emtpy_use(self, mock__show,
mock_env_manager_create):
self.assertEqual(
0, self.env.create(self.api, "test_name", "test_description"))
env.create(name="test_name", description="test_description")
mock_env_manager_create.assert_called_once_with(
"test_name", {}, description="test_description", extras=None)
mock_env_commands__show.assert_called_once_with(
mock__show.assert_called_once_with(
mock_env_manager_create.return_value.data,
to_json=False, only_spec=False)
@mock.patch("rally.env.env_mgr.EnvManager.create")
@mock.patch("rally.cli.commands.env.open", create=True)
@mock.patch("rally.cli.commands.env.print")
def test_create_spec_and_extra_no_use_to_json(self, mock_print, mock_open,
mock_env_manager_create):
def test_create_spec_and_extra_no_use_to_json(
self, mock_print, mock_open, mock_env_manager_create):
mock_open.side_effect = mock.mock_open(read_data="{\"a\": 1}")
mock_env_manager_create.return_value.data = {"test": "test"}
self.assertEqual(
0, self.env.create(self.api, "n", "d", extras="{\"extra\": 123}",
spec="spec.yml", to_json=True, do_use=False))
env.create(name="n", description="d",
extras="{\"extra\": 123}", spec="spec.yml",
to_json=True, no_use=True)
mock_env_manager_create.assert_called_once_with(
"n", {"a": 1}, description="d", extras={"extra": 123})
@@ -90,15 +82,15 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.env.open", create=True)
def test_create_invalid_spec(self, mock_open, mock_print):
mock_open.side_effect = mock.mock_open(read_data="[]")
self.assertEqual(
1, self.env.create(self.api, "n", "d", spec="spec.yml"))
with self.assertExitCode(1):
env.create(name="n", description="d", spec="spec.yml")
mock_print.assert_has_calls([
mock.call("Env spec has wrong format:"),
mock.call("[]"),
mock.call(mock.ANY)
])
@mock.patch("rally.cli.commands.env.EnvCommands._show")
@mock.patch("rally.cli.commands.env._show")
@mock.patch("rally.env.env_mgr.EnvManager.create_spec_from_sys_environ")
@mock.patch("rally.env.env_mgr.EnvManager.create")
@mock.patch("rally.cli.commands.env.open", create=True)
@@ -106,7 +98,7 @@ class EnvCommandsTestCase(test.TestCase):
def test_create_from_sys_env(
self, mock_print, mock_open, mock_env_manager_create,
mock_env_manager_create_spec_from_sys_environ,
mock_env_commands__show):
mock__show):
result = {
"spec": {"foo": mock.Mock()},
"discovery_details": collections.OrderedDict([
@@ -117,9 +109,8 @@ class EnvCommandsTestCase(test.TestCase):
}
mock_env_manager_create_spec_from_sys_environ.return_value = result
self.assertEqual(
0, self.env.create(self.api, "n", "d", spec=None,
from_sysenv=True, do_use=False))
env.create(name="n", description="d", spec=None,
from_sysenv=True, no_use=True)
self.assertEqual(
[
# check that the number of listed platforms is right
@@ -138,15 +129,16 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.env.print")
def test_create_with_incompatible_arguments(self, mock_print):
self.assertEqual(
1, self.env.create(self.api, "n", "d", spec="asd",
from_sysenv=True))
with self.assertExitCode(1):
env.create(name="n", description="d", spec="asd",
from_sysenv=True)
@mock.patch("rally.env.env_mgr.EnvManager.create")
@mock.patch("rally.cli.commands.env.print")
def test_create_exception(self, mock_print, mock_env_manager_create):
mock_env_manager_create.side_effect = Exception
self.assertEqual(1, self.env.create(self.api, "n", "d"))
with self.assertExitCode(1):
env.create(name="n", description="d")
mock_print.assert_has_calls([
mock.call("Something went wrong during env creation:"),
mock.call(mock.ANY)
@@ -176,7 +168,8 @@ class EnvCommandsTestCase(test.TestCase):
]
}
}
self.assertEqual(1, self.env.cleanup(self.api, env_))
with self.assertExitCode(1):
env.cleanup(env=env_)
mock_env_manager_get.assert_called_once_with(env_)
env_inst.cleanup.assert_called_once_with()
@@ -228,7 +221,8 @@ class EnvCommandsTestCase(test.TestCase):
]
}
}
self.assertEqual(1, self.env.cleanup(self.api, env_, to_json=True))
with self.assertExitCode(1):
env.cleanup(env=env_, to_json=True)
mock_print.assert_called_once_with(
json.dumps(env_inst.cleanup.return_value, indent=2))
@@ -243,7 +237,8 @@ class EnvCommandsTestCase(test.TestCase):
"message": "42"
}
}
self.assertEqual(1, self.env.destroy(self.api, env_))
with self.assertExitCode(1):
env.destroy(env_)
mock_env_manager_get.assert_called_once_with(env_)
env_inst.destroy.assert_called_once_with(False)
mock_print.assert_has_calls([
@@ -266,9 +261,7 @@ class EnvCommandsTestCase(test.TestCase):
"message": "42"
}
}
self.assertEqual(
0,
self.env.destroy(self.api, env_, skip_cleanup=True, to_json=True))
env.destroy(env_, skip_cleanup=True, to_json=True)
env_inst.destroy.assert_called_once_with(True)
mock_print.assert_called_once_with(
json.dumps(env_inst.destroy.return_value, indent=2))
@@ -276,7 +269,7 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.env.env_mgr.EnvManager.get")
def test_delete(self, mock_env_manager_get):
env_ = mock.Mock()
self.env.delete(self.api, env_)
env.delete(env_)
mock_env_manager_get.assert_called_once_with(env_)
mock_env_manager_get.return_value.delete.assert_called_once_with(
force=False)
@@ -284,7 +277,7 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.env.env_mgr.EnvManager.get")
def test_delete_force(self, mock_env_manager_get):
env_ = mock.Mock()
self.env.delete(self.api, env_, force=True)
env.delete(env_, force=True)
mock_env_manager_get.assert_called_once_with(env_)
mock_env_manager_get.return_value.delete.assert_called_once_with(
force=True)
@@ -293,11 +286,11 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.env.print")
def test_list_empty(self, mock_print, mock_env_manager_list):
mock_env_manager_list.return_value = []
self.env.list(self.api, to_json=True)
env.list_(to_json=True)
mock_print.assert_called_once_with("[]")
mock_print.reset_mock()
self.env.list(self.api, to_json=False)
mock_print.assert_called_once_with(self.env.MSG_NO_ENVS)
env.list_(to_json=False)
mock_print.assert_called_once_with(env.MSG_NO_ENVS)
@mock.patch("rally.env.env_mgr.EnvManager.list")
@mock.patch("rally.cli.commands.env.print")
@@ -306,7 +299,7 @@ class EnvCommandsTestCase(test.TestCase):
env_b = env_mgr.EnvManager(self.gen_env_data())
mock_env_manager_list.return_value = [env_a, env_b]
self.env.list(self.api, to_json=True)
env.list_(to_json=True)
mock_env_manager_list.assert_called_once_with()
mock_print.assert_called_once_with(
json.dumps([env_a.cached_data, env_b.cached_data], indent=2))
@@ -314,7 +307,7 @@ class EnvCommandsTestCase(test.TestCase):
for m in [mock_env_manager_list, mock_print]:
m.reset_mock()
self.env.list(self.api)
env.list_()
mock_env_manager_list.assert_called_once_with()
mock_print.assert_called_once_with(mock.ANY)
@@ -325,7 +318,7 @@ class EnvCommandsTestCase(test.TestCase):
name="my best env",
description="description")
env_data["platforms"] = {}
self.env._show(env_data, False, False)
env._show(env_data, False, False)
mock_print.assert_called_once_with(
"+-------------+--------------------------------------+\n"
"| uuid | a77004a6-7fe5-4b75-a278-009c3c5f6b20 |\n"
@@ -339,25 +332,25 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.env.print")
def test__show_to_json(self, mock_print):
self.env._show("data", to_json=True, only_spec=False)
env._show("data", to_json=True, only_spec=False)
mock_print.assert_called_once_with("\"data\"")
@mock.patch("rally.cli.commands.env.print")
def test__show_only_spec(self, mock_print):
self.env._show({"spec": "data"}, to_json=False, only_spec=True)
env._show({"spec": "data"}, to_json=False, only_spec=True)
mock_print.assert_called_once_with("\"data\"")
@mock.patch("rally.env.env_mgr.EnvManager.get")
@mock.patch("rally.cli.commands.env.EnvCommands._show")
@mock.patch("rally.cli.commands.env._show")
def test_show(self, mock__show, mock_env_manager_get):
env_ = mock.Mock()
self.env.show(self.api, env_)
env.show(env=env_)
mock_env_manager_get.assert_called_once_with(env_)
mock__show.assert_called_once_with(
mock_env_manager_get.return_value.data, to_json=False,
only_spec=False)
mock__show.reset_mock()
self.env.show(self.api, env_, to_json=True)
env.show(env=env_, to_json=True)
mock__show.assert_called_once_with(
mock_env_manager_get.return_value.data, to_json=True,
only_spec=False)
@@ -368,7 +361,7 @@ class EnvCommandsTestCase(test.TestCase):
mock_env_manager_get.return_value.get_info.return_value = {
"p1": {"info": {"a": True}}}
self.assertEqual(0, self.env.info(self.api, "any", to_json=True))
env.info(env="any", to_json=True)
mock_env_manager_get.assert_called_once_with("any")
mock_print.assert_called_once_with(
json.dumps(mock_env_manager_get.return_value.get_info.return_value,
@@ -378,7 +371,8 @@ class EnvCommandsTestCase(test.TestCase):
"p1": {"info": {"a": False}},
"p2": {"info": {}, "error": "some error"}
}
self.assertEqual(1, self.env.info(self.api, "any", to_json=True))
with self.assertExitCode(1):
env.info(env="any", to_json=True)
@mock.patch("rally.env.env_mgr.EnvManager.get")
@mock.patch("rally.cli.commands.env.print")
@@ -387,7 +381,8 @@ class EnvCommandsTestCase(test.TestCase):
"p1@pl1": {"info": {"a": False}},
"p2@pl2": {"info": {}, "error": "some error"}
}
self.assertEqual(1, self.env.info(self.api, "any"))
with self.assertExitCode(1):
env.info(env="any")
mock_print.assert_has_calls([
mock.call(mock_env_manager_get.return_value),
mock.call(
@@ -409,7 +404,8 @@ class EnvCommandsTestCase(test.TestCase):
"p1@p1": {"available": True, "message": "OK!"},
"p2@p2": {"available": False, "message": "BAD !"}
}
self.assertEqual(1, self.env.check(self.api, "env_42"))
with self.assertExitCode(1):
env.check(env="env_42")
mock_env_manager_get.assert_called_once_with("env_42")
mock_print.assert_has_calls([
@@ -432,7 +428,8 @@ class EnvCommandsTestCase(test.TestCase):
"p2@p2": {"available": False, "message": "BAD !",
"traceback": "Filaneme\n Codeline\nError"}
}
self.assertEqual(1, self.env.check(self.api, "env_42", detailed=True))
with self.assertExitCode(1):
env.check(env="env_42", detailed=True)
mock_env_manager_get.assert_called_once_with("env_42")
print(mock_print.call_args_list)
@@ -457,7 +454,7 @@ class EnvCommandsTestCase(test.TestCase):
mock_env_manager_get.return_value.check_health.return_value = {
"p1": {"available": True}}
self.assertEqual(0, self.env.check(self.api, "some_env", to_json=True))
env.check(env="some_env", to_json=True)
mock_env_manager_get.assert_called_once_with("some_env")
mock_print.assert_called_once_with(
json.dumps(
@@ -467,7 +464,8 @@ class EnvCommandsTestCase(test.TestCase):
mock_env_manager_get.return_value.check_health.return_value = {
"p1": {"available": False}}
self.assertEqual(1, self.env.check(self.api, "some_env", to_json=True))
with self.assertExitCode(1):
env.check(env="some_env", to_json=True)
@mock.patch("rally.env.env_mgr.EnvManager.get")
@mock.patch("rally.cli.commands.env._print")
@@ -475,19 +473,20 @@ class EnvCommandsTestCase(test.TestCase):
mock_env_manager_get.side_effect = exceptions.DBRecordNotFound(
criteria="", table="")
env_ = str(uuid.uuid4())
self.assertEqual(1, self.env.use(self.api, env_))
with self.assertExitCode(1):
env.use(env_)
mock_env_manager_get.assert_called_once_with(env_)
mock__print.assert_called_once_with(
"Can't use non existing environment %s." % env_, False)
@mock.patch("rally.env.env_mgr.EnvManager.get")
@mock.patch("rally.cli.commands.env.EnvCommands._use")
@mock.patch("rally.cli.commands.env._use")
def test_use(self, mock__use, mock_env_manager_get):
mock_env_manager_get.side_effect = [
mock.Mock(uuid="aa"), mock.Mock(uuid="bb")
]
self.assertIsNone(self.env.use(self.api, "aa"))
self.assertIsNone(self.env.use(self.api, "bb", to_json=True))
self.assertIsNone(env.use("aa"))
self.assertIsNone(env.use("bb", to_json=True))
mock_env_manager_get.assert_has_calls(
[mock.call("aa"), mock.call("bb")])
@@ -497,11 +496,11 @@ class EnvCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.env.envutils.update_globals_file")
@mock.patch("rally.cli.commands.env.print")
def test__use(self, mock_print, mock_update_globals_file):
self.env._use("aa", True)
env._use("aa", True)
self.assertFalse(mock_print.called)
mock_update_globals_file.assert_called_once_with("RALLY_ENV", "aa")
mock_update_globals_file.reset_mock()
self.env._use("bb", False)
env._use("bb", False)
mock_print.assert_called_once_with("Using environment: bb")
mock_update_globals_file.assert_called_once_with("RALLY_ENV", "bb")
+24 -18
View File
@@ -18,6 +18,7 @@ from unittest import mock
import ddt
from rally import exceptions
from rally.cli import cliutils
from rally.cli.commands import plugin as plugin_cmd
from rally.common import utils
@@ -30,7 +31,6 @@ class PluginCommandsTestCase(test.TestCase):
def setUp(self):
super(PluginCommandsTestCase, self).setUp()
self.plugin_cmd = plugin_cmd.PluginCommands()
@plugin.configure("p1", "p1_ns")
class Plugin1(plugin.Plugin):
@@ -75,7 +75,7 @@ class PluginCommandsTestCase(test.TestCase):
with mock.patch.object(plugin_cmd.cliutils, "print_list",
new=print_list):
plugin_cmd.PluginCommands._print_plugins_list(
plugin_cmd._print_plugins_list(
[self.Plugin1, self.Plugin2])
self.assertEqual(
@@ -88,7 +88,7 @@ class PluginCommandsTestCase(test.TestCase):
def test_show(self):
with utils.StdOutCapture() as out:
plugin_cmd.PluginCommands().show(None, "p1", "p1_ns")
plugin_cmd.show("p1", platform="p1_ns")
output = out.getvalue()
self.assertIn("NAME\n\tp1", output)
@@ -112,20 +112,26 @@ class PluginCommandsTestCase(test.TestCase):
@ddt.unpack
def test_show_not_found(self, name, platform, text):
with utils.StdOutCapture() as out:
plugin_cmd.PluginCommands().show(None, name, platform)
with self.assertExitCode(exceptions.PluginNotFound.error_code):
plugin_cmd.show(name, platform=platform)
self.assertEqual(out.getvalue(), text)
@mock.patch("rally.cli.commands.plugin.PluginCommands._print_plugins_list")
def test_show_many(self, mock_plugin_commands__print_plugins_list):
@mock.patch("rally.cli.commands.plugin._print_plugins_list")
def test_show_many(self, mock__print_plugins_list):
with utils.StdOutCapture() as out:
with mock.patch("rally.cli.commands.plugin.plugin.Plugin."
"get_all") as mock_plugin_get_all:
mock_plugin_get_all.return_value = [self.Plugin2, self.Plugin3]
plugin_cmd.PluginCommands().show(None, "p", "p2_ns")
self.assertEqual("Multiple plugins found:\n", out.getvalue())
mock_plugin_get_all.assert_called_once_with(platform="p2_ns")
mock_plugin_get_all.return_value = [self.Plugin2,
self.Plugin3]
with self.assertExitCode(
exceptions.MultiplePluginsFound.error_code):
plugin_cmd.show("p", platform="p2_ns")
self.assertEqual("Multiple plugins found:\n",
out.getvalue())
mock_plugin_get_all.assert_called_once_with(
platform="p2_ns")
mock_plugin_commands__print_plugins_list.assert_called_once_with([
mock__print_plugins_list.assert_called_once_with([
self.Plugin2, self.Plugin3])
@ddt.data(
@@ -144,17 +150,17 @@ class PluginCommandsTestCase(test.TestCase):
def test_list_not_found(self, name, platform, text):
with utils.StdOutCapture() as out:
plugin_cmd.PluginCommands().list(None, name, platform)
plugin_cmd.list_(name, platform=platform)
self.assertEqual(text, out.getvalue())
@mock.patch("rally.cli.commands.plugin.PluginCommands._print_plugins_list")
def test_list(self, mock_plugin_commands__print_plugins_list):
@mock.patch("rally.cli.commands.plugin._print_plugins_list")
def test_list(self, mock__print_plugins_list):
plugin_cmd.PluginCommands().list(None, None, "p1_ns")
plugin_cmd.PluginCommands().list(None, "p1", "p1_ns")
plugin_cmd.PluginCommands().list(None, "p2", None)
plugin_cmd.list_(None, platform="p1_ns")
plugin_cmd.list_("p1", platform="p1_ns")
plugin_cmd.list_("p2")
mock_plugin_commands__print_plugins_list.assert_has_calls([
mock__print_plugins_list.assert_has_calls([
mock.call([self.Plugin1]),
mock.call([self.Plugin1]),
mock.call([self.Plugin2])
+139 -167
View File
@@ -38,8 +38,8 @@ class TaskCommandsTestCase(test.TestCase):
def setUp(self):
super(TaskCommandsTestCase, self).setUp()
self.task = task.TaskCommands()
self.fake_api = fakes.FakeAPI()
cliutils.set_api(self.fake_api)
with mock.patch("rally.api.API.check_db_revision"):
self.real_api = api.API()
@@ -53,14 +53,14 @@ class TaskCommandsTestCase(test.TestCase):
mock.mock_open(read_data=input_task).return_value,
mock.mock_open(read_data="{'test': 1}").return_value
]
task_conf = self.task._load_and_validate_task(
task_conf = task._load_and_validate_task(
self.real_api, "in_task", args_file="in_args_path")
self.assertEqual({"ab": 1}, task_conf)
mock_open.side_effect = [
mock.mock_open(read_data=input_task).return_value
]
task_conf = self.task._load_and_validate_task(
task_conf = task._load_and_validate_task(
self.real_api, "in_task", raw_args=input_args)
self.assertEqual({"ab": 2}, task_conf)
@@ -68,7 +68,7 @@ class TaskCommandsTestCase(test.TestCase):
mock.mock_open(read_data=input_task).return_value,
mock.mock_open(read_data="{'test': 1}").return_value
]
task_conf = self.task._load_and_validate_task(
task_conf = task._load_and_validate_task(
self.real_api, "in_task", raw_args=input_args,
args_file="any_file")
self.assertEqual({"ab": 2}, task_conf)
@@ -77,7 +77,7 @@ class TaskCommandsTestCase(test.TestCase):
mock.mock_open(read_data=input_task).return_value,
mock.mock_open(read_data="{'test': 1}").return_value
]
task_conf = self.task._load_and_validate_task(
task_conf = task._load_and_validate_task(
self.real_api, "in_task", raw_args="test=2",
args_file="any_file")
self.assertEqual({"ab": 2}, task_conf)
@@ -96,7 +96,7 @@ class TaskCommandsTestCase(test.TestCase):
mock_open.side_effect = open_return_value
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task,
task._load_and_validate_task,
self.fake_api, task_file="in_task",
args_file="in_args_path")
self.assertEqual("Invalid --task-args-file passed:\n\n\t Error "
@@ -109,7 +109,7 @@ class TaskCommandsTestCase(test.TestCase):
task_file = __file__
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task, self.real_api,
task._load_and_validate_task, self.real_api,
task_file, raw_args="{'test': {}")
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
"YAML or JSON. Details:\n\nfoo", e.format_message())
@@ -118,7 +118,7 @@ class TaskCommandsTestCase(test.TestCase):
# the case #2
mock_safe_load.reset_mock()
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task, self.real_api,
task._load_and_validate_task, self.real_api,
task_file, raw_args="[]")
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
"YAML or JSON. Details:\n\nfoo", e.format_message())
@@ -127,7 +127,7 @@ class TaskCommandsTestCase(test.TestCase):
# the case #3
mock_safe_load.reset_mock()
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task, self.real_api,
task._load_and_validate_task, self.real_api,
task_file, raw_args="foo")
self.assertEqual("Invalid --task-args passed:\n\n\t Value has to be "
"YAML or JSON. Details:\n\nfoo", e.format_message())
@@ -139,11 +139,11 @@ class TaskCommandsTestCase(test.TestCase):
mock.mock_open(read_data="{'test': {{t}}}").return_value
]
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task, self.real_api,
task._load_and_validate_task, self.real_api,
"in_task")
self.assertEqual("Invalid --task passed:\n\n\t Failed to render task "
"template.\n\nPlease specify template task argument: "
"t", e.format_message())
self.assertEqual("Invalid task file passed:\n\n\t Failed to render "
"task template.\n\nPlease specify template task "
"argument: t", e.format_message())
@mock.patch("rally.cli.commands.task.yaml")
@mock.patch("rally.cli.commands.task.open", create=True)
@@ -154,9 +154,9 @@ class TaskCommandsTestCase(test.TestCase):
mock_yaml.safe_load.side_effect = Exception("ERROR!!!PANIC!!!")
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task, self.fake_api,
task._load_and_validate_task, self.fake_api,
"in_task")
self.assertEqual("Invalid --task passed:\n\n\t Wrong format of "
self.assertEqual("Invalid task file passed:\n\n\t Wrong format of "
"rendered input task. It should be YAML or JSON. "
"Details:\n\nERROR!!!PANIC!!!", e.format_message())
@@ -166,8 +166,8 @@ class TaskCommandsTestCase(test.TestCase):
"samples/tasks/scenarios/dummy/dummy.json")
input_task = "{%% include \"%s\" %%}" % os.path.basename(
other_template_path)
expect = self.task._load_and_validate_task(self.real_api,
other_template_path)
expect = task._load_and_validate_task(self.real_api,
other_template_path)
with mock.patch("rally.cli.commands.task.open",
create=True) as mock_open:
@@ -176,8 +176,8 @@ class TaskCommandsTestCase(test.TestCase):
]
input_task_file = os.path.join(
os.path.dirname(other_template_path), "input_task.json")
actual = self.task._load_and_validate_task(self.real_api,
input_task_file)
actual = task._load_and_validate_task(self.real_api,
input_task_file)
self.assertEqual(expect, actual)
@mock.patch("rally.cli.commands.task.open", create=True)
@@ -185,20 +185,20 @@ class TaskCommandsTestCase(test.TestCase):
mock_open.side_effect = IOError
e = self.assertRaises(task.FailedToLoadTask,
self.task._load_and_validate_task,
task._load_and_validate_task,
api=self.fake_api, task_file="some_task",
raw_args="task_args", args_file="task_args_file")
self.assertEqual(
"Invalid --task passed:\n\n\t Error reading some_task: ",
"Invalid task file passed:\n\n\t Error reading some_task: ",
e.format_message())
@mock.patch("rally.cli.commands.task.version")
@mock.patch("rally.cli.commands.task.TaskCommands.use")
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
@mock.patch("rally.cli.commands.task._use")
@mock.patch("rally.cli.commands.task._detailed")
@mock.patch("rally.cli.commands.task._load_and_validate_task",
return_value={"some": "json"})
def test_start(self, mock__load_and_validate_task, mock__detailed,
mock_use, mock_version):
mock__use, mock_version):
deployment_id = "e0617de9-77d1-4875-9b49-9d5789e29f20"
task_path = "path_to_config.json"
fake_task = fakes.FakeTask(uuid="some_new_uuid", tags=["tag"])
@@ -207,9 +207,8 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.validate.return_value = fakes.FakeTask(
some="json", uuid="some_uuid", temporary=True)
val = self.task.start(self.fake_api, task_path,
deployment_id, do_use=True)
self.assertEqual(2, val)
with self.assertExitCode(2):
task.start(task_path, deployment=deployment_id)
mock_version.version_string.assert_called_once_with()
self.fake_api.task.create.assert_called_once_with(
deployment=deployment_id, tags=None)
@@ -220,16 +219,14 @@ class TaskCommandsTestCase(test.TestCase):
abort_on_sla_failure=False)
mock__load_and_validate_task.assert_called_once_with(
self.fake_api, task_path, args_file=None, raw_args=None)
mock_use.assert_called_once_with(self.fake_api, "some_new_uuid")
mock__use.assert_called_once_with(self.fake_api, "some_new_uuid")
mock__detailed.assert_called_once_with(self.fake_api,
task_id=fake_task["uuid"])
mock__detailed.return_value = 0
val1 = self.task.start(self.fake_api, task_path,
deployment_id, do_use=True)
self.assertEqual(0, val1)
task.start(task_path, deployment=deployment_id)
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
@mock.patch("rally.cli.commands.task._detailed")
@mock.patch("rally.cli.commands.task._load_and_validate_task",
return_value="some_config")
def test_start_on_unfinished_deployment(self, mock__load_and_validate_task,
mock__detailed):
@@ -244,13 +241,12 @@ class TaskCommandsTestCase(test.TestCase):
uuid=deployment_id,
status=consts.DeployStatus.DEPLOY_INIT)
self.fake_api.task.create.side_effect = exc
self.assertEqual(1, self.task.start(self.fake_api, task_path,
deployment="any",
tags=["some_tag"]))
with self.assertExitCode(1):
task.start(task_path, deployment="any", tags=["some_tag"])
self.assertFalse(mock__detailed.called)
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
@mock.patch("rally.cli.commands.task._detailed")
@mock.patch("rally.cli.commands.task._load_and_validate_task",
return_value="some_config")
def test_start_with_task_args(self, mock__load_and_validate_task,
mock__detailed):
@@ -259,13 +255,14 @@ class TaskCommandsTestCase(test.TestCase):
uuid="new_uuid", tags=["some_tag"])
self.fake_api.task.validate.return_value = fakes.FakeTask(
uuid="some_id")
mock__detailed.return_value = 0
task_path = "path_to_config.json"
task_args = "task_args"
task_args_file = "task_args_file"
self.task.start(self.fake_api, task_path, deployment="any",
task_args=task_args, task_args_file=task_args_file,
tags=["some_tag"])
task.start(task_path, deployment="any",
task_args=task_args, task_args_file=task_args_file,
tags=["some_tag"])
mock__load_and_validate_task.assert_called_once_with(
self.fake_api, task_path, raw_args=task_args,
@@ -282,14 +279,8 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.create.assert_called_once_with(
deployment="any", tags=["some_tag"])
@mock.patch("rally.cli.commands.task.envutils.get_global")
def test_start_no_deployment_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.task.start, "path_to_config.json", None)
@mock.patch("rally.cli.commands.task.TaskCommands._detailed")
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
@mock.patch("rally.cli.commands.task._detailed")
@mock.patch("rally.cli.commands.task._load_and_validate_task")
def test_start_invalid_task(self, mock__load_and_validate_task,
mock__detailed):
task_obj = fakes.FakeTask(temporary=False, tag="tag", uuid="uuid")
@@ -299,8 +290,8 @@ class TaskCommandsTestCase(test.TestCase):
mock__load_and_validate_task.side_effect = exc
self.assertRaises(exceptions.InvalidTaskException,
self.task.start, self.fake_api, "task_path",
"deployment", tags=["tag"])
task.start, "task_path",
deployment="deployment", tags=["tag"])
self.assertFalse(self.fake_api.task.create.called)
self.assertFalse(self.fake_api.task.start.called)
@@ -311,8 +302,8 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.start.side_effect = KeyError()
self.assertRaises(KeyError,
self.task.start, self.fake_api, "task_path",
"deployment", tags=["tag"])
task.start, "task_path",
deployment="deployment", tags=["tag"])
self.fake_api.task.create.assert_called_once_with(
deployment="deployment", tags=["tag"])
@@ -324,7 +315,7 @@ class TaskCommandsTestCase(test.TestCase):
self.assertFalse(mock__detailed.called)
@mock.patch("rally.cli.commands.task.TaskCommands._start_task")
@mock.patch("rally.cli.commands.task._start_task")
@ddt.data({"scenario": None},
{"scenario": "scenario_name"},
{"scenario": "none_name"})
@@ -350,17 +341,13 @@ class TaskCommandsTestCase(test.TestCase):
]
}
if scenario == "none_name":
self.assertEqual(
1,
self.task.restart(self.fake_api, "deployment_uuid",
"task_uuid", scenarios=scenario)
)
with self.assertExitCode(1):
task.restart(deployment="deployment_uuid",
task_id="task_uuid", scenarios=scenario)
else:
self.assertEqual(
mock__start_task.return_value,
self.task.restart(self.fake_api, "deployment_uuid",
"task_uuid", scenarios=scenario)
)
mock__start_task.return_value = 0
task.restart(deployment="deployment_uuid",
task_id="task_uuid", scenarios=scenario)
self.fake_api.task.get.assert_called_once_with(task_id="task_uuid",
detailed=True)
@@ -378,38 +365,24 @@ class TaskCommandsTestCase(test.TestCase):
"msg": ""
}
}
self.assertEqual(
1,
self.task.restart(self.fake_api, "deployment_uuid", "task_uuid")
)
with self.assertExitCode(1):
task.restart(deployment="deployment_uuid", task_id="task_uuid")
self.fake_api.task.get.assert_called_once_with(task_id="task_uuid",
detailed=True)
def test_abort(self):
test_uuid = "17860c43-2274-498d-8669-448eff7b073f"
self.task.abort(self.fake_api, test_uuid)
task.abort(test_uuid)
self.fake_api.task.abort.assert_called_once_with(
task_uuid=test_uuid, soft=False, wait=True)
@mock.patch("rally.cli.commands.task.envutils.get_global")
def test_abort_no_task_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.task.abort, self.fake_api, None)
def test_status(self):
test_uuid = "a3e7cefb-bec2-4802-89f6-410cc31f71af"
value = {"task_id": "task", "status": "status"}
self.fake_api.task.get.return_value = value
self.task.status(self.fake_api, test_uuid)
task.status(task_id=test_uuid)
self.fake_api.task.get.assert_called_once_with(task_id=test_uuid)
@mock.patch("rally.cli.commands.task.envutils.get_global")
def test_status_no_task_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.task.status, None)
@ddt.data({"iterations_data": False, "has_output": True,
"filters": None},
{"iterations_data": True, "has_output": False,
@@ -549,9 +522,8 @@ class TaskCommandsTestCase(test.TestCase):
detailed_value["subtasks"][0]["workloads"][0]["output"] = {
"additive": [], "complete": []}
self.fake_api.task.get.return_value = detailed_value
self.task.detailed(self.fake_api, test_uuid,
iterations_data=iterations_data,
filters=filters)
task.detailed(task_id=test_uuid, iterations_data=iterations_data,
filters=filters)
self.fake_api.task.get.assert_called_once_with(
task_id=test_uuid, detailed=True)
@@ -574,7 +546,7 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.get.return_value = value
mock_logging.is_debug.return_value = debug
self.task.detailed(self.fake_api, test_uuid)
task.detailed(task_id=test_uuid)
if debug:
expected_calls = [
mock.call("Task test_task_id: crashed"),
@@ -599,23 +571,17 @@ class TaskCommandsTestCase(test.TestCase):
"results": []
}
self.fake_api.task.get.return_value = value
self.task.detailed(self.fake_api, test_uuid)
task.detailed(task_id=test_uuid)
expected_calls = [mock.call("Task test_task_id: init"),
mock.call("\nThe task test_task_id marked as "
"'init'. Results available when it "
"is 'finished'.")]
mock_stdout.write.assert_has_calls(expected_calls, any_order=True)
@mock.patch("rally.cli.commands.task.envutils.get_global")
def test_detailed_no_task_id(self, mock_get_global):
mock_get_global.side_effect = exceptions.InvalidArgumentsException
self.assertRaises(exceptions.InvalidArgumentsException,
self.task.detailed, None)
def test_detailed_wrong_id(self):
test_uuid = "eb290c30-38d8-4c8f-bbcc-fc8f74b004ae"
self.fake_api.task.get.side_effect = None
self.task.detailed(self.fake_api, test_uuid)
task.detailed(task_id=test_uuid)
self.fake_api.task.get.assert_called_once_with(
task_id=test_uuid, detailed=True)
@@ -699,7 +665,7 @@ class TaskCommandsTestCase(test.TestCase):
self.fake_api.task.get.return_value = task_obj
self.assertIsNone(self.task.results(self.fake_api, task_id))
self.assertIsNone(task.results(task_id=task_id))
self.assertEqual(1, mock_json_dumps.call_count)
self.assertEqual(1, len(mock_json_dumps.call_args[0]))
self.assertEqual(list(result), mock_json_dumps.call_args[0][0])
@@ -721,7 +687,8 @@ class TaskCommandsTestCase(test.TestCase):
task_obj["uuid"] = task_id
self.fake_api.task.get.return_value = task_obj
self.assertEqual(1, self.task.results(self.fake_api, task_id))
with self.assertExitCode(1):
task.results(task_id=task_id)
self.fake_api.task.get.assert_called_once_with(
task_id=task_id, detailed=True)
@@ -733,33 +700,46 @@ class TaskCommandsTestCase(test.TestCase):
consts.TaskStatus.ABORTED)))
mock_stdout.write.assert_has_calls([mock.call(expected_out)])
def test_trends(self):
self.task.export = mock.MagicMock()
self.task.trends(self.fake_api,
tasks=["uuid"],
out="output.html")
self.task.export.assert_called_once_with(
@mock.patch("rally.cli.commands.task._export")
def test_trends(self, mock__export):
task.trends(["uuid"], out="output.html")
mock__export.assert_called_once_with(
self.fake_api, tasks=["uuid"], output_type="trends-html",
output_dest="output.html", open_it=False)
def test_trends_no_tasks_given(self):
ret = self.task.trends(self.fake_api, tasks=[],
out="output.html", out_format="html")
self.assertEqual(1, ret)
@mock.patch("rally.cli.commands.task._export")
def test_trends_html_static(self, mock__export):
task.trends(["uuid"], out="output.html", html_static=True)
mock__export.assert_called_once_with(
self.fake_api, tasks=["uuid"],
output_type="trends-html-static",
output_dest="output.html", open_it=False)
def test_report(self):
self.task.export = mock.MagicMock()
self.task.report(self.fake_api, tasks="uuid",
out="out", open_it=False, out_format="junit-xml")
self.task.export.assert_called_once_with(
self.fake_api, tasks="uuid", output_type="junit-xml",
output_dest="out", open_it=False, deployment=None
)
def test_trends_no_tasks_given(self):
with self.assertExitCode(1):
task.trends([], out="output.html")
@mock.patch("rally.cli.commands.task._export")
def test_report(self, mock__export):
task.report("uuid", out="out")
mock__export.assert_called_once_with(
self.fake_api, tasks="uuid", output_type="html",
output_dest="out", open_it=False, deployment=None)
mock__export.reset_mock()
task.report("uuid", out="out", to_json=True)
mock__export.assert_called_once_with(
self.fake_api, tasks="uuid", output_type="json",
output_dest="out", open_it=False, deployment=None)
mock__export.reset_mock()
task.report("uuid", out="out", html_static=True)
mock__export.assert_called_once_with(
self.fake_api, tasks="uuid", output_type="html-static",
output_dest="out", open_it=False, deployment=None)
@mock.patch("rally.cli.commands.task.cliutils.print_list")
@mock.patch("rally.cli.commands.task.envutils.get_global",
return_value="123456789")
def test_list(self, mock_get_global, mock_print_list):
def test_list(self, mock_print_list):
self.fake_api.task.list.return_value = [
{"uuid": "a",
"created_at": "2007-01-01T00:00:01",
@@ -767,9 +747,9 @@ class TaskCommandsTestCase(test.TestCase):
"status": consts.TaskStatus.RUNNING,
"tags": ["d"],
"deployment_name": "some_name"}]
self.task.list(self.fake_api, status="running")
task.list_(deployment="123456789", status="running")
self.fake_api.task.list.assert_called_once_with(
deployment=mock_get_global.return_value,
deployment="123456789",
status=consts.TaskStatus.RUNNING)
headers = ["UUID", "Deployment name", "Created at", "Load duration",
@@ -781,9 +761,7 @@ class TaskCommandsTestCase(test.TestCase):
sortby_index=headers.index("Created at"),
formatters=mock.ANY)
@mock.patch("rally.cli.commands.task.envutils.get_global",
return_value="123456789")
def test_list_uuids_only(self, mock_get_global):
def test_list_uuids_only(self):
self.fake_api.task.list.return_value = [
{"uuid": "a",
"created_at": "2007-01-01T00:00:01",
@@ -793,31 +771,31 @@ class TaskCommandsTestCase(test.TestCase):
"deployment_name": "some_name"}]
out = io.StringIO()
with mock.patch.object(sys, "stdout", new=out):
self.task.list(self.fake_api, status="running", uuids_only=True)
task.list_(deployment="123456789", status="running",
uuids_only=True)
self.assertEqual("a\n", out.getvalue())
self.fake_api.task.list.assert_called_once_with(
deployment=mock_get_global.return_value,
deployment="123456789",
status=consts.TaskStatus.RUNNING)
def test_list_wrong_status(self):
self.assertEqual(1, self.task.list(self.fake_api, deployment="fake",
status="wrong non existing status"))
with self.assertExitCode(1):
task.list_(deployment="fake",
status="wrong non existing status")
def test_list_no_results(self):
self.fake_api.task.list.return_value = []
self.assertIsNone(self.task.list(self.fake_api, deployment="fake",
all_deployments=True))
self.assertIsNone(task.list_(deployment="fake",
all_deployments=True))
self.fake_api.task.list.assert_called_once_with()
self.fake_api.task.list.reset_mock()
self.assertIsNone(self.task.list(self.fake_api, deployment="d",
status=consts.TaskStatus.RUNNING))
self.assertIsNone(task.list_(deployment="d",
status=consts.TaskStatus.RUNNING))
self.fake_api.task.list.assert_called_once_with(
deployment="d", status=consts.TaskStatus.RUNNING)
@mock.patch("rally.cli.commands.task.envutils.get_global",
return_value="123456789")
def test_list_output(self, mock_get_global):
def test_list_output(self):
self.fake_api.task.list.return_value = [
{"uuid": "UUID-1",
"created_at": "2007-01-01T00:00:01",
@@ -844,7 +822,7 @@ class TaskCommandsTestCase(test.TestCase):
with mock.patch.object(task.cliutils, "print_list",
new=print_list):
self.task.list(self.fake_api, status="running")
task.list_(deployment="123456789", status="running")
self.assertEqual(1, len(print_list_calls))
@@ -866,7 +844,7 @@ class TaskCommandsTestCase(test.TestCase):
def test_delete(self):
task_uuid = "8dcb9c5e-d60b-4022-8975-b5987c7833f7"
force = False
self.task.delete(self.fake_api, task_uuid, force=force)
task.delete(task_id=task_uuid, force=force)
self.fake_api.task.delete.assert_called_once_with(
task_uuid=task_uuid, force=force)
@@ -876,7 +854,7 @@ class TaskCommandsTestCase(test.TestCase):
"6a3cb11c-ac75-41e7-8ae7-935732bfb48f",
"018af931-0e5a-40d5-9d6f-b13f4a3a09fc"]
force = False
self.task.delete(self.fake_api, task_uuids, force=force)
task.delete(task_id=task_uuids, force=force)
self.assertTrue(
self.fake_api.task.delete.call_count == len(task_uuids))
expected_calls = [mock.call(task_uuid=task_uuid,
@@ -892,17 +870,15 @@ class TaskCommandsTestCase(test.TestCase):
"pos": 0, "success": False, "detail": "Max foo, actually bar"}]
self.fake_api.task.get.return_value = task_obj
result = self.task.sla_check(self.fake_api, task_id="fake_task_id")
self.assertEqual(1, result)
with self.assertExitCode(1):
task.sla_check(task_id="fake_task_id")
self.fake_api.task.get.assert_called_with(
task_id="fake_task_id", detailed=True)
task_obj["subtasks"][0]["workloads"][0]["sla_results"]["sla"][0][
"success"] = True
result = self.task.sla_check(self.fake_api, task_id="fake_task_id",
tojson=True)
self.assertEqual(0, result)
task.sla_check(task_id="fake_task_id", tojson=True)
@mock.patch("rally.cli.commands.task.os.path.isfile", return_value=True)
@mock.patch("rally.cli.commands.task.open",
@@ -911,12 +887,12 @@ class TaskCommandsTestCase(test.TestCase):
def test_validate(self, mock_open, mock_os_path_isfile):
self.fake_api.task.render_template = self.real_api.task.render_template
self.task.validate(self.fake_api, "path_to_config.json", "fake_id")
task.validate("path_to_config.json", deployment="fake_id")
self.fake_api.task.validate.assert_called_once_with(
deployment="fake_id", config={"some": "json"})
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task",
@mock.patch("rally.cli.commands.task._load_and_validate_task",
side_effect=task.FailedToLoadTask)
def test_validate_failed_to_load_task(self, mock__load_and_validate_task):
args = "args"
@@ -924,21 +900,21 @@ class TaskCommandsTestCase(test.TestCase):
mock__load_and_validate_task.side_effect = KeyError("foo")
self.assertRaises(KeyError, self.task.validate, self.real_api,
"path_to_task", "fake_deployment_id",
self.assertRaises(KeyError, task.validate, "path_to_task",
deployment="fake_deployment_id",
task_args=args, task_args_file=args_file)
self.assertFalse(self.fake_api.task.validate.called)
mock__load_and_validate_task.assert_called_once_with(
self.real_api, "path_to_task", raw_args=args, args_file=args_file)
self.fake_api, "path_to_task", raw_args=args, args_file=args_file)
@mock.patch("rally.cli.commands.task.TaskCommands._load_and_validate_task")
@mock.patch("rally.cli.commands.task._load_and_validate_task")
def test_validate_invalid(self, mock__load_and_validate_task):
exc = exceptions.InvalidTaskException("foo")
self.fake_api.task.validate.side_effect = exc
self.assertRaises(exceptions.InvalidTaskException,
self.task.validate, self.fake_api, "path_to_task",
"deployment")
task.validate, "path_to_task",
deployment="deployment")
self.fake_api.task.validate.assert_called_once_with(
deployment="deployment",
config=mock__load_and_validate_task.return_value)
@@ -946,7 +922,7 @@ class TaskCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.envutils._rewrite_env_file")
def test_use(self, mock__rewrite_env_file):
task_id = "80422553-5774-44bd-98ac-38bd8c7a0feb"
self.task.use(self.fake_api, task_id)
task.use(task_id)
mock__rewrite_env_file.assert_called_once_with(
os.path.expanduser("~/.rally/globals"),
["RALLY_TASK=%s\n" % task_id])
@@ -956,8 +932,8 @@ class TaskCommandsTestCase(test.TestCase):
exc = exceptions.DBRecordNotFound(criteria="uuid: %s" % task_id,
table="tasks")
self.fake_api.task.get.side_effect = exc
self.assertRaises(exceptions.DBRecordNotFound, self.task.use,
self.fake_api, task_id)
self.assertRaises(exceptions.DBRecordNotFound, task.use,
task_id)
@mock.patch("rally.cli.task_results_loader.load")
@mock.patch("rally.cli.commands.task.os.path")
@@ -977,9 +953,9 @@ class TaskCommandsTestCase(test.TestCase):
mock_open.side_effect = mock_fd
mock_load.return_value = [{"task": "task_1"}, {"task": "task2"}]
self.task.export(self.fake_api, tasks=["uuid", "file"],
output_type="json", output_dest="output_dest",
open_it=True)
task._export(self.fake_api, tasks=["uuid", "file"],
output_type="json", output_dest="output_dest",
open_it=True)
self.fake_api.task.export.assert_called_once_with(
tasks=["uuid"] + mock_load.return_value,
@@ -992,7 +968,7 @@ class TaskCommandsTestCase(test.TestCase):
# print
self.fake_api.task.export.reset_mock()
self.fake_api.task.export.return_value = {"print": "content"}
self.task.export(self.fake_api, tasks="uuid", output_type="json")
task._export(self.fake_api, tasks="uuid", output_type="json")
self.fake_api.task.export.assert_called_once_with(
tasks=["uuid"],
output_type="json", output_dest=None
@@ -1090,7 +1066,7 @@ class TaskCommandsTestCase(test.TestCase):
"validation_result": json.dumps([error_type, error_message,
error_traceback])
}
self.task.detailed(self.fake_api, test_uuid)
task.detailed(task_id=test_uuid)
self.fake_api.task.get.assert_called_once_with(
task_id=test_uuid, detailed=True)
mock_stdout.write.assert_has_calls([
@@ -1104,9 +1080,8 @@ class TaskCommandsTestCase(test.TestCase):
mock_os_path.expanduser = lambda path: path
mock_load.return_value = ["results"]
self.task.import_results(self.fake_api,
"deployment_uuid",
"task_file", tags=["tag"])
task.import_results("task_file", deployment="deployment_uuid",
tags=["tag"])
mock_load.assert_called_once_with("task_file")
self.fake_api.task.import_results.assert_called_once_with(
@@ -1115,9 +1090,6 @@ class TaskCommandsTestCase(test.TestCase):
# not exist
mock_os_path.exists.return_value = False
self.assertEqual(
1,
self.task.import_results(self.fake_api,
"deployment_uuid",
"task_file", ["tag"])
)
with self.assertExitCode(1):
task.import_results("task_file", deployment="deployment_uuid",
tags=["tag"])
+127 -112
View File
@@ -33,8 +33,8 @@ class VerifyCommandsTestCase(test.TestCase):
def setUp(self):
super(VerifyCommandsTestCase, self).setUp()
self.verify = verify.VerifyCommands()
self.fake_api = fakes.FakeAPI()
cliutils.set_api(self.fake_api)
self.deployment_name = "Some Deploy"
self.deployment_uuid = "some-deploy-uuid"
@@ -125,7 +125,7 @@ class VerifyCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.verify.logging.is_debug",
return_value=True)
def test_list_plugins(self, mock_is_debug, mock_print_list):
self.verify.list_plugins(self.fake_api, platform="some")
verify.list_plugins(platform="some")
self.fake_api.verifier.list_plugins.assert_called_once_with(
platform="some")
@@ -134,9 +134,9 @@ class VerifyCommandsTestCase(test.TestCase):
self.fake_api.verifier.create.return_value = self.verifier_uuid
self.fake_api.verifier.get.return_value = self.verifier_data
self.verify.create_verifier(self.fake_api, "a", vtype="b",
platform="c", source="d", version="e",
system_wide=True, extra={})
verify.create_verifier(name="a", vtype="b", platform="c",
source="d", version="e", system_wide=True,
extra={})
self.fake_api.verifier.create.assert_called_once_with(
name="a", vtype="b", platform="c", source="d", version="e",
system_wide=True, extra_settings={})
@@ -149,7 +149,7 @@ class VerifyCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
def test_use_verifier(self, mock_update_globals_file):
self.fake_api.verifier.get.return_value = self.verifier_data
self.verify.use_verifier(self.fake_api, self.verifier_uuid)
verify.use_verifier(verifier_id=self.verifier_uuid)
self.fake_api.verifier.get.assert_called_once_with(
verifier_id=self.verifier_uuid)
mock_update_globals_file.assert_called_once_with(
@@ -158,10 +158,10 @@ class VerifyCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
def test_list_verifiers_empty_verifiers(self, mock_print_list):
self.fake_api.verifier.list.return_value = []
self.verify.list_verifiers(self.fake_api)
verify.list_verifiers()
self.verify.list_verifiers(self.fake_api, "foo")
self.verify.list_verifiers(self.fake_api)
verify.list_verifiers(status="foo")
verify.list_verifiers()
self.fake_api.verifier.list.assert_has_calls(
[mock.call(status=None), mock.call(status="foo")])
@@ -175,7 +175,7 @@ class VerifyCommandsTestCase(test.TestCase):
"Active"]
additional_keys = ["normalize_field_names", "sortby_index",
"formatters"]
self.verify.list_verifiers(self.fake_api)
verify.list_verifiers()
# astarove: should be replaced on mock_print_list.assert_called_once()
self.assertEqual(1, mock_print_list.call_count)
self.assertEqual(([self.verifier_data], additional_fields),
@@ -183,10 +183,11 @@ class VerifyCommandsTestCase(test.TestCase):
self.assertEqual(additional_keys.sort(),
list(mock_print_list.call_args[1].keys()).sort())
@mock.patch("rally.cli.commands.verify._base_dir",
return_value="./verifiers/")
@mock.patch("rally.cli.commands.verify.envutils.get_global")
def test_show_verifier(self, mock_get_global):
def test_show_verifier(self, mock_get_global, mock__base_dir):
self.fake_api.verifier.get.return_value = self.verifier_data
self.verify._base_dir = mock.Mock(return_value="./verifiers/")
# It is a hard task to mock default value of function argument, so we
# need to apply this workaround
@@ -200,7 +201,7 @@ class VerifyCommandsTestCase(test.TestCase):
with mock.patch.object(verify.cliutils, "print_dict",
new=print_dict):
self.verify.show_verifier(self.fake_api, self.verifier_uuid)
verify.show_verifier(self.verifier_uuid)
self.assertEqual(1, len(print_dict_calls))
@@ -230,54 +231,54 @@ class VerifyCommandsTestCase(test.TestCase):
verifier_id=self.verifier_uuid)
def test_delete_verifier(self):
self.verify.delete_verifier(self.fake_api, "v_id", "d_id", force=True)
verify.delete_verifier(verifier_id="v_id", deployment="d_id",
force=True)
self.fake_api.verifier.delete.assert_called_once_with(
verifier_id="v_id", deployment_id="d_id", force=True)
def test_update_verifier(self):
self.verify.update_verifier(self.fake_api, self.verifier_uuid)
with self.assertExitCode(1):
verify.update_verifier(verifier_id=self.verifier_uuid)
self.assertFalse(self.fake_api.verifier.update.called)
self.verify.update_verifier(self.fake_api, self.verification_uuid,
update_venv=True,
system_wide=True)
with self.assertExitCode(1):
verify.update_verifier(verifier_id=self.verification_uuid,
update_venv=True, system_wide=True)
self.assertFalse(self.fake_api.verifier.update.called)
self.verify.update_verifier(self.fake_api, self.verification_uuid,
system_wide=True,
no_system_wide=True)
with self.assertExitCode(1):
verify.update_verifier(verifier_id=self.verification_uuid,
system_wide=True, no_system_wide=True)
self.assertFalse(self.fake_api.verifier.update.called)
self.verify.update_verifier(self.fake_api, self.verification_uuid,
version="a",
system_wide=True)
verify.update_verifier(verifier_id=self.verification_uuid,
version="a", system_wide=True)
self.fake_api.verifier.update.assert_called_once_with(
verifier_id=self.verification_uuid, system_wide=True,
version="a", update_venv=None)
version="a", update_venv=False)
@mock.patch("rally.cli.commands.verify.open", create=True)
@mock.patch("rally.cli.commands.verify.os.path.exists")
def test_configure_verifier(self, mock_exists, mock_open):
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
self.deployment_uuid,
new_configuration="/p/a/t/h",
reconfigure=True,
show=True)
with self.assertExitCode(1):
verify.configure_verifier(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
new_configuration="/p/a/t/h",
reconfigure=True, show=True)
self.assertFalse(self.fake_api.verifier.configure.called)
mock_exists.return_value = False
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
self.deployment_uuid,
new_configuration="/p/a/t/h",
show=True)
with self.assertExitCode(1):
verify.configure_verifier(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
new_configuration="/p/a/t/h", show=True)
self.assertFalse(self.fake_api.verifier.override_configuration.called)
mock_exists.return_value = True
mock_open.return_value = mock.mock_open(read_data="data").return_value
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
self.deployment_uuid,
new_configuration="/p/a/t/h",
show=True)
verify.configure_verifier(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
new_configuration="/p/a/t/h", show=True)
mock_open.assert_called_once_with("/p/a/t/h")
self.fake_api.verifier.override_configuration(self.verifier_uuid,
self.deployment_uuid,
@@ -286,37 +287,39 @@ class VerifyCommandsTestCase(test.TestCase):
tf = tempfile.NamedTemporaryFile()
with open(tf.name, "w") as f:
f.write("[DEFAULT]\nopt = val\n[foo]\nopt = val")
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
self.deployment_uuid,
extra_options=tf.name)
verify.configure_verifier(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
extra_options=tf.name)
expected_options = {"foo": {"opt": "val"},
"DEFAULT": {"opt": "val"}}
self.fake_api.verifier.configure.assert_called_once_with(
verifier=self.verifier_uuid, deployment_id=self.deployment_uuid,
extra_options=expected_options, reconfigure=False)
self.verify.configure_verifier(self.fake_api, self.verifier_uuid,
self.deployment_uuid,
extra_options="{foo: {opt: val}, "
"DEFAULT: {opt: val}}")
verify.configure_verifier(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
extra_options="{foo: {opt: val}, "
"DEFAULT: {opt: val}}")
self.fake_api.verifier.configure.assert_called_with(
verifier=self.verifier_uuid, deployment_id=self.deployment_uuid,
extra_options=expected_options, reconfigure=False)
def test_list_verifier_tests(self):
self.fake_api.verifier.list_tests.return_value = ["test_1", "test_2"]
self.verify.list_verifier_tests(self.fake_api, self.verifier_uuid, "p")
verify.list_verifier_tests(verifier_id=self.verifier_uuid,
pattern="p")
self.fake_api.verifier.list_tests.return_value = []
self.verify.list_verifier_tests(self.fake_api, self.verifier_uuid, "p")
verify.list_verifier_tests(verifier_id=self.verifier_uuid,
pattern="p")
self.fake_api.verifier.list_tests.assert_has_calls(
[mock.call(verifier_id=self.verifier_uuid, pattern="p"),
mock.call(verifier_id=self.verifier_uuid, pattern="p")])
def test_add_verifier_ext(self):
self.verify.add_verifier_ext(self.fake_api, self.verifier_uuid,
"a", "b", "c")
verify.add_verifier_ext(verifier_id=self.verifier_uuid, source="a",
version="b", extra="c")
self.fake_api.verifier.add_extension.assert_called_once_with(
verifier_id=self.verifier_uuid,
source="a", version="b", extra_settings="c")
@@ -327,9 +330,9 @@ class VerifyCommandsTestCase(test.TestCase):
def test_list_verifier_exts_empty_list(self,
mock_is_debug, mock_print_list):
self.fake_api.verifier.list_extensions.return_value = []
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.fake_api.verifier.list_extensions.assert_has_calls(
[mock.call(verifier_id=self.verifier_uuid),
@@ -342,9 +345,9 @@ class VerifyCommandsTestCase(test.TestCase):
ver_exts = self.fake_api.verifier.list_extensions
ver_exts.return_value = [mock.MagicMock()]
fields = ["Name", "Entry point"]
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.fake_api.verifier.list_extensions.assert_has_calls(
[mock.call(verifier_id=self.verifier_uuid),
@@ -362,9 +365,9 @@ class VerifyCommandsTestCase(test.TestCase):
ver_exts = self.fake_api.verifier.list_extensions
ver_exts.return_value = [mock.MagicMock()]
fields = ["Name", "Entry point", "Location"]
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.verify.list_verifier_exts(self.fake_api, self.verifier_uuid)
verify.list_verifier_exts(verifier_id=self.verifier_uuid)
self.fake_api.verifier.list_extensions.assert_has_calls(
[mock.call(verifier_id=self.verifier_uuid),
@@ -375,17 +378,18 @@ class VerifyCommandsTestCase(test.TestCase):
normalize_field_names=True)
def test_delete_verifier_ext(self):
self.verify.delete_verifier_ext(self.fake_api, self.verifier_uuid,
"ext_name")
verify.delete_verifier_ext(verifier_id=self.verifier_uuid,
name="ext_name")
self.fake_api.verifier.delete_extension.assert_called_once_with(
verifier_id=self.verifier_uuid, name="ext_name")
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
@mock.patch("rally.cli.commands.verify.os.path.exists")
def test_start(self, mock_exists, mock_update_globals_file):
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, pattern="pattern",
load_list="load-list")
with self.assertExitCode(1):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, pattern="pattern",
load_list="load-list")
self.assertFalse(self.fake_api.verification.start.called)
verification = self.verification_data
@@ -396,17 +400,20 @@ class VerifyCommandsTestCase(test.TestCase):
self.fake_api.verification.get.return_value = verification
mock_exists.return_value = False
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, load_list="/p/a/t/h")
with self.assertExitCode(1):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
load_list="/p/a/t/h")
self.assertFalse(self.fake_api.verification.start.called)
mock_exists.return_value = True
tf = tempfile.NamedTemporaryFile()
with open(tf.name, "w") as f:
f.write("test_1\ntest_2")
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, tags=["foo"],
load_list=tf.name)
with self.assertExitCode(3):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, tags=["foo"],
load_list=tf.name)
self.fake_api.verification.start.assert_called_once_with(
verifier_id=self.verifier_uuid,
deployment_id=self.deployment_uuid,
@@ -415,16 +422,18 @@ class VerifyCommandsTestCase(test.TestCase):
mock_exists.return_value = False
self.fake_api.verification.start.reset_mock()
self.verify.start(self.fake_api, self.verifier_uuid,
self.verifier_uuid, skip_list="/p/a/t/h")
with self.assertExitCode(1):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.verifier_uuid, skip_list="/p/a/t/h")
self.assertFalse(self.fake_api.verification.start.called)
tf = tempfile.NamedTemporaryFile()
with open(tf.name, "w") as f:
f.write("test_1:\ntest_2: Reason\n")
mock_exists.return_value = True
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, skip_list=tf.name)
with self.assertExitCode(3):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, skip_list=tf.name)
self.fake_api.verification.start.assert_called_once_with(
verifier_id=self.verifier_uuid,
deployment_id=self.deployment_uuid,
@@ -433,16 +442,19 @@ class VerifyCommandsTestCase(test.TestCase):
mock_exists.return_value = False
self.fake_api.verification.start.reset_mock()
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, xfail_list="/p/a/t/h")
with self.assertExitCode(1):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
xfail_list="/p/a/t/h")
self.assertFalse(self.fake_api.verification.start.called)
tf = tempfile.NamedTemporaryFile()
with open(tf.name, "w") as f:
f.write("test_1:\ntest_2: Reason\n")
mock_exists.return_value = True
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, xfail_list=tf.name)
with self.assertExitCode(3):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, xfail_list=tf.name)
self.fake_api.verification.start.assert_called_once_with(
verifier_id=self.verifier_uuid,
deployment_id=self.deployment_uuid, tags=None,
@@ -455,9 +467,10 @@ class VerifyCommandsTestCase(test.TestCase):
self.fake_api.verification.get.reset_mock()
mock_update_globals_file.reset_mock()
self.verify.start(self.fake_api, self.verifier_uuid,
self.deployment_uuid, detailed=True,
do_use=False)
with self.assertExitCode(3):
verify.start(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, detailed=True,
no_use=True)
self.assertFalse(self.fake_api.verification.get.called)
self.assertFalse(mock_update_globals_file.called)
@@ -472,14 +485,14 @@ class VerifyCommandsTestCase(test.TestCase):
uuid=deployment_id,
status=consts.DeployStatus.DEPLOY_INIT)
self.fake_api.verification.start.side_effect = exc
self.assertEqual(
1, self.verify.start(self.fake_api,
self.deployment_uuid, deployment_id))
with self.assertExitCode(1):
verify.start(verifier_id=self.deployment_uuid,
deployment=deployment_id)
@mock.patch("rally.cli.commands.verify.envutils.update_globals_file")
def test_use(self, mock_update_globals_file):
self.fake_api.verification.get.return_value = self.verification_data
self.verify.use(self.fake_api, self.verification_uuid)
verify.use(verification_uuid=self.verification_uuid)
self.fake_api.verification.get.assert_called_once_with(
verification_uuid=self.verification_uuid)
mock_update_globals_file.assert_called_once_with(
@@ -493,8 +506,8 @@ class VerifyCommandsTestCase(test.TestCase):
"tests": self.results_data["tests"]}
self.fake_api.verification.get.return_value = self.verification_data
self.verify.rerun(self.fake_api, self.verification_uuid,
self.deployment_uuid, failed=True)
verify.rerun(self.verification_uuid,
deployment=self.deployment_uuid, failed=True)
self.fake_api.verification.rerun.assert_called_once_with(
verification_uuid=self.verification_uuid,
concurrency=None,
@@ -523,7 +536,7 @@ class VerifyCommandsTestCase(test.TestCase):
with mock.patch.object(verify.cliutils, "print_dict",
new=print_dict):
self.verify.show(self.fake_api, self.verifier_uuid, detailed=True)
verify.show(self.verifier_uuid, detailed=True)
self.assertEqual(1, len(print_dict_calls))
@@ -580,7 +593,7 @@ class VerifyCommandsTestCase(test.TestCase):
with mock.patch.object(verify.cliutils, "print_dict",
new=print_dict):
self.verify.show(self.fake_api, self.verifier_uuid, detailed=False)
verify.show(self.verifier_uuid, detailed=False)
self.assertEqual(2, len(print_dict_calls))
self.assertEqual(
@@ -639,12 +652,13 @@ class VerifyCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
def test_list_empty_verifications(self, mock_print_list):
self.fake_api.verification.list.return_value = []
self.verify.list(self.fake_api, self.verifier_uuid,
self.deployment_uuid)
verify.list_(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid)
self.verify.list(self.fake_api, self.verifier_uuid,
self.deployment_uuid, "foo", "bar")
self.verify.list(self.fake_api)
verify.list_(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid, tags="foo",
status="bar")
verify.list_()
self.fake_api.verification.list.assert_has_calls(
[mock.call(verifier_id=self.verifier_uuid,
@@ -659,8 +673,8 @@ class VerifyCommandsTestCase(test.TestCase):
@mock.patch("rally.cli.commands.verify.cliutils.print_list")
def test_list(self, mock_print_list):
self.fake_api.verification.list.return_value = [self.verification_data]
self.verify.list(self.fake_api, self.verifier_uuid,
self.deployment_uuid)
verify.list_(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid)
additional_fields = ["UUID", "Tags", "Verifier name",
"Deployment name", "Started at", "Finished at",
@@ -675,11 +689,11 @@ class VerifyCommandsTestCase(test.TestCase):
list(mock_print_list.call_args[1].keys()).sort())
def test_delete(self):
self.verify.delete(self.fake_api, "v_uuid")
verify.delete(verification_uuid=["v_uuid"])
self.fake_api.verification.delete.assert_called_once_with(
verification_uuid="v_uuid")
self.verify.delete(self.fake_api, ["v1_uuid", "v2_uuid"])
verify.delete(verification_uuid=["v1_uuid", "v2_uuid"])
self.fake_api.verification.delete.assert_has_calls(
[mock.call(verification_uuid="v1_uuid"),
mock.call(verification_uuid="v2_uuid")])
@@ -695,10 +709,9 @@ class VerifyCommandsTestCase(test.TestCase):
"files": {output_dest: content}, "open": output_dest}
mock_os.path.exists.return_value = False
self.verify.report(self.fake_api,
verification_uuid=self.verifier_uuid,
output_type=output_type,
output_dest=output_dest, open_it=True)
verify.report(verification_uuid=[self.verifier_uuid],
output_type=output_type,
output_dest=output_dest, open_it=True)
self.fake_api.verification.report.assert_called_once_with(
uuids=[self.verifier_uuid], output_type=output_type,
output_dest=output_dest)
@@ -715,20 +728,22 @@ class VerifyCommandsTestCase(test.TestCase):
self.fake_api.verification.report.return_value = {
"files": {output_dest: content}, "print": "foo"}
self.verify.report(self.fake_api, self.verifier_uuid,
output_type=output_type,
output_dest=output_dest)
verify.report(verification_uuid=self.verifier_uuid,
output_type=output_type,
output_dest=output_dest)
self.assertFalse(mock_open_new_tab.called)
self.assertFalse(mock_os.makedirs.called)
@mock.patch("rally.cli.commands.verify.VerifyCommands.use")
@mock.patch("rally.cli.commands.verify._use")
@mock.patch("rally.cli.commands.verify.open", create=True)
@mock.patch("rally.cli.commands.verify.os.path.exists")
def test_import_results(self, mock_exists, mock_open, mock_use):
def test_import_results(self, mock_exists, mock_open, mock__use):
mock_exists.return_value = False
self.verify.import_results(self.fake_api, self.verifier_uuid,
self.deployment_uuid)
with self.assertExitCode(1):
verify.import_results(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
file_to_parse="/p/a/t/h")
self.assertFalse(self.fake_api.verification.import_results.called)
verification = self.verification_data
@@ -738,21 +753,21 @@ class VerifyCommandsTestCase(test.TestCase):
mock_exists.return_value = True
mock_open.return_value = mock.mock_open(read_data="data").return_value
self.verify.import_results(self.fake_api,
verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
file_to_parse="/p/a/t/h")
verify.import_results(verifier_id=self.verifier_uuid,
deployment=self.deployment_uuid,
file_to_parse="/p/a/t/h")
mock_open.assert_called_once_with("/p/a/t/h", "r")
self.fake_api.verification.import_results.assert_called_once_with(
verifier_id=self.verifier_uuid,
deployment_id=self.deployment_uuid,
data="data")
mock_use.assert_called_with(self.fake_api, self.verification_uuid)
mock__use.assert_called_with(self.fake_api, self.verification_uuid)
mock_use.reset_mock()
self.verify.import_results(self.fake_api, "v_id", "d_id", do_use=False)
self.assertFalse(mock_use.called)
mock__use.reset_mock()
verify.import_results(verifier_id="v_id", deployment="d_id",
file_to_parse="/p/a/t/h", no_use=True)
self.assertFalse(mock__use.called)
@plugins.ensure_plugins_are_loaded
def test_default_reporters(self):
+112
View File
@@ -0,0 +1,112 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import io
import os
import typing as t
from unittest import mock
import ddt
import typer
from rally.cli import argutils
from tests.unit import test
def _build(define: t.Callable) -> t.Any:
"""Build a two-level app, wire the parser, return the click command."""
app = typer.Typer(no_args_is_help=False)
group = typer.Typer()
app.add_typer(group, name="task")
define(group)
cli = typer.main.get_command(app)
argutils.install(cli)
return cli
def _scalar_cli():
def define(group):
@group.command()
def go(
uuid: t.Annotated[
str,
argutils.ArgumentOrKeyword("--uuid", envvar="RALLY_TASK")
]
) -> None:
print("UUID=%s" % uuid)
return _build(define)
def _list_cli():
def define(group):
@group.command()
def go(
uuids: t.Annotated[
list[str],
argutils.ArgumentOrKeyword("--uuid")
]
) -> None:
print("UUIDS=%s" % ",".join(uuids))
return _build(define)
@ddt.ddt
class ArgumentOrKeywordTestCase(test.TestCase):
"""Exercise the actual click parser wiring, not the command functions."""
def _invoke(self, cli, args, env=None):
out = io.StringIO()
code = 0
with mock.patch.dict(os.environ, env or {}), \
contextlib.redirect_stdout(out), \
contextlib.redirect_stderr(io.StringIO()):
try:
cli(args, prog_name="rally", standalone_mode=True)
except SystemExit as e:
code = e.code if isinstance(e.code, int) else 1
return code, out.getvalue()
@ddt.data(
# positional, --flag, env-var default, and --flag winning over the env
{"args": ["task", "go", "POS"], "env": "", "expected": "POS"},
{"args": ["task", "go", "--uuid", "FLG"], "env": "",
"expected": "FLG"},
{"args": ["task", "go"], "env": "ENV", "expected": "ENV"},
{"args": ["task", "go", "--uuid", "FLG"], "env": "ENV",
"expected": "FLG"},
)
@ddt.unpack
def test_scalar_resolves(self, args, env, expected):
code, out = self._invoke(_scalar_cli(), args, env={"RALLY_TASK": env})
self.assertEqual(0, code)
self.assertIn("UUID=%s" % expected, out)
@ddt.data(
["task", "go"], # required: nothing given, no env
["task", "go", "--bogus"], # ``--uuid`` is real, so a typo is rejected
["task", "go", "A", "B"], # extra positional argument
)
def test_scalar_rejects(self, args):
code, _ = self._invoke(_scalar_cli(), args, env={"RALLY_TASK": ""})
self.assertEqual(2, code)
@ddt.data(
{"args": ["task", "go", "a", "b"], "expected": "a,b"},
{"args": ["task", "go", "--uuid", "x", "--uuid", "y"],
"expected": "x,y"},
)
@ddt.unpack
def test_list_resolves(self, args, expected):
code, out = self._invoke(_list_cli(), args)
self.assertEqual(0, code)
self.assertIn("UUIDS=%s" % expected, out)
+30 -615
View File
@@ -17,37 +17,15 @@ import io
from unittest import mock
import ddt
import sqlalchemy.exc
import typer
from rally import exceptions
from rally.cli import cliutils
from rally.cli.commands import deployment
from rally.cli.commands import task
from rally.cli.commands import verify
from rally.common import cfg
from tests.unit import test
CONF = cfg.CONF
FAKE_TASK_UUID = "bb0f621c-29bd-495c-9d7a-d844335ed0fa"
@ddt.ddt
class CliUtilsTestCase(test.TestCase):
def setUp(self):
super(CliUtilsTestCase, self).setUp()
self.categories = {
"deployment": deployment.DeploymentCommands,
"task": task.TaskCommands,
"verify": verify.VerifyCommands
}
def tearDown(self):
self._unregister_opts()
super(CliUtilsTestCase, self).tearDown()
def test_print_dict(self):
out = io.StringIO()
dict = {"key": "value"}
@@ -202,112 +180,6 @@ class CliUtilsTestCase(test.TestCase):
else:
self.assertEqual(expected, formatter(obj))
def test__methods_of_with_class(self):
class fake_class(object):
def public(self):
pass
def _private(self):
pass
result = cliutils._methods_of(fake_class)
self.assertEqual(1, len(result))
self.assertEqual("public", result[0][0])
def test__methods_of_with_object(self):
class fake_class(object):
def public(self):
pass
def _private(self):
pass
mock_obj = fake_class()
result = cliutils._methods_of(mock_obj)
self.assertEqual(1, len(result))
self.assertEqual("public", result[0][0])
def test__methods_of_empty_result(self):
class fake_class(object):
def _private(self):
pass
def _private2(self):
pass
mock_obj = fake_class()
result = cliutils._methods_of(mock_obj)
self.assertEqual([], result)
def _unregister_opts(self):
CONF.reset()
category_opt = cfg.SubCommandOpt("category",
title="Command categories",
help="Available categories"
)
CONF.unregister_opt(category_opt)
@mock.patch("rally.api.API",
side_effect=exceptions.RallyException("config_file"))
def test_run_fails(self, mock_rally_api_api):
ret = cliutils.run(["rally", "task list"], self.categories)
self.assertEqual(2, ret)
mock_rally_api_api.assert_called_once_with(
config_args=["task list"], skip_db_check=True)
@mock.patch("rally.api.API.check_db_revision")
def test_run_version(self, mock_api_check_db_revision):
ret = cliutils.run(["rally", "version"], self.categories)
self.assertEqual(0, ret)
@mock.patch("rally.api.API.check_db_revision")
def test_run_bash_completion(self, mock_api_check_db_revision):
ret = cliutils.run(["rally", "bash-completion"], self.categories)
self.assertEqual(0, ret)
@mock.patch("rally.api.API.check_db_revision")
@mock.patch("rally.common.db.api.task_get",
side_effect=exceptions.DBRecordNotFound(
criteria="uuid: %s" % FAKE_TASK_UUID, table="tasks"))
def test_run_task_not_found(self, mock_task_get,
mock_api_check_db_revision):
ret = cliutils.run(["rally", "task", "status", "%s" % FAKE_TASK_UUID],
self.categories)
self.assertTrue(mock_task_get.called)
self.assertEqual(203, ret)
@mock.patch("rally.api.API.check_db_revision")
@mock.patch("rally.cli.cliutils.validate_args",
side_effect=cliutils.MissingArgs("missing"))
def test_run_task_failed(self, mock_validate_args,
mock_api_check_db_revision):
ret = cliutils.run(["rally", "task", "status", "%s" % FAKE_TASK_UUID],
self.categories)
self.assertTrue(mock_validate_args.called)
self.assertEqual(1, ret)
@mock.patch("rally.api.API.check_db_revision")
def test_run_failed_to_open_file(self, mock_api_check_db_revision):
class FailuresCommands(object):
def failed_to_open_file(self):
raise IOError("No such file")
ret = cliutils.run(["rally", "failure", "failed-to-open-file"],
{"failure": FailuresCommands})
self.assertEqual(1, ret)
@mock.patch("rally.api.API.check_db_revision")
def test_run_sqlalchmey_operational_failure(self,
mock_api_check_db_revision):
class SQLAlchemyCommands(object):
def operational_failure(self):
raise sqlalchemy.exc.OperationalError("Can't open DB file")
ret = cliutils.run(["rally", "failure", "operational-failure"],
{"failure": SQLAlchemyCommands})
self.assertEqual(1, ret)
class TestObj(object):
x = 1
y = 2
@@ -577,500 +449,43 @@ class CliUtilsTestCase(test.TestCase):
[self.TestObj()], ["x"],
field_labels=["x", "y"], sortby_index=None, out=out)
def test_help_for_grouped_methods(self):
class SomeCommand(object):
@cliutils.help_group("1_manage")
def install(self):
pass
@cliutils.help_group("1_manage")
def uninstall(self):
pass
class IterCommandsTestCase(test.TestCase):
@cliutils.help_group("1_manage")
def reinstall(self):
pass
def _build(self):
app = typer.Typer()
group = typer.Typer()
@cliutils.help_group("2_launch")
def run(self):
pass
@cliutils.help_group("2_launch")
def rerun(self):
pass
@cliutils.help_group("3_results")
def show(self):
pass
@cliutils.help_group("3_results")
def list(self):
pass
def do_do_has_do_has_mesh(self):
pass
self.assertEqual(
"\n\nCommands:\n"
" do-do-has-do-has-mesh \n"
"\n"
" install \n"
" reinstall \n"
" uninstall \n"
"\n"
" rerun \n"
" run \n"
"\n"
" list \n"
" show \n",
cliutils._compose_category_description(SomeCommand))
class ValidateArgsTest(test.TestCase):
def test_lambda_no_args(self):
cliutils.validate_args(lambda: None)
def _test_lambda_with_args(self, *args, **kwargs):
cliutils.validate_args(lambda x, y: None, *args, **kwargs)
def test_lambda_positional_args(self):
self._test_lambda_with_args(1, 2)
def test_lambda_kwargs(self):
self._test_lambda_with_args(x=1, y=2)
def test_lambda_mixed_kwargs(self):
self._test_lambda_with_args(1, y=2)
def test_lambda_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_args)
def test_lambda_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_args, 1)
def test_lambda_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_args, y=2)
def test_lambda_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_args, 1, x=2)
def _test_lambda_with_default(self, *args, **kwargs):
cliutils.validate_args(lambda x, y, z=3: None, *args, **kwargs)
def test_lambda_positional_args_with_default(self):
self._test_lambda_with_default(1, 2)
def test_lambda_kwargs_with_default(self):
self._test_lambda_with_default(x=1, y=2)
def test_lambda_mixed_kwargs_with_default(self):
self._test_lambda_with_default(1, y=2)
def test_lambda_positional_args_all_with_default(self):
self._test_lambda_with_default(1, 2, 3)
def test_lambda_kwargs_all_with_default(self):
self._test_lambda_with_default(x=1, y=2, z=3)
def test_lambda_mixed_kwargs_all_with_default(self):
self._test_lambda_with_default(1, y=2, z=3)
def test_lambda_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_default)
def test_lambda_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_default, 1)
def test_lambda_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_default, y=2)
def test_lambda_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_lambda_with_default, y=2, z=3)
def test_function_no_args(self):
def func():
pass
cliutils.validate_args(func)
def _test_function_with_args(self, *args, **kwargs):
def func(x, y):
pass
cliutils.validate_args(func, *args, **kwargs)
def test_function_positional_args(self):
self._test_function_with_args(1, 2)
def test_function_kwargs(self):
self._test_function_with_args(x=1, y=2)
def test_function_mixed_kwargs(self):
self._test_function_with_args(1, y=2)
def test_function_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_args)
def test_function_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_args, 1)
def test_function_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_args, y=2)
def _test_function_with_default(self, *args, **kwargs):
def func(x, y, z=3):
pass
cliutils.validate_args(func, *args, **kwargs)
def test_function_positional_args_with_default(self):
self._test_function_with_default(1, 2)
def test_function_kwargs_with_default(self):
self._test_function_with_default(x=1, y=2)
def test_function_mixed_kwargs_with_default(self):
self._test_function_with_default(1, y=2)
def test_function_positional_args_all_with_default(self):
self._test_function_with_default(1, 2, 3)
def test_function_kwargs_all_with_default(self):
self._test_function_with_default(x=1, y=2, z=3)
def test_function_mixed_kwargs_all_with_default(self):
self._test_function_with_default(1, y=2, z=3)
def test_function_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_default)
def test_function_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_default, 1)
def test_function_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_default, y=2)
def test_function_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_function_with_default, y=2, z=3)
def test_bound_method_no_args(self):
class Foo(object):
def bar(self):
pass
cliutils.validate_args(Foo().bar)
def _test_bound_method_with_args(self, *args, **kwargs):
class Foo(object):
def bar(self, x, y):
pass
cliutils.validate_args(Foo().bar, *args, **kwargs)
def test_bound_method_positional_args(self):
self._test_bound_method_with_args(1, 2)
def test_bound_method_kwargs(self):
self._test_bound_method_with_args(x=1, y=2)
def test_bound_method_mixed_kwargs(self):
self._test_bound_method_with_args(1, y=2)
def test_bound_method_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_args)
def test_bound_method_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_args, 1)
def test_bound_method_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_args, y=2)
def _test_bound_method_with_default(self, *args, **kwargs):
class Foo(object):
def bar(self, x, y, z=3):
pass
cliutils.validate_args(Foo().bar, *args, **kwargs)
def test_bound_method_positional_args_with_default(self):
self._test_bound_method_with_default(1, 2)
def test_bound_method_kwargs_with_default(self):
self._test_bound_method_with_default(x=1, y=2)
def test_bound_method_mixed_kwargs_with_default(self):
self._test_bound_method_with_default(1, y=2)
def test_bound_method_positional_args_all_with_default(self):
self._test_bound_method_with_default(1, 2, 3)
def test_bound_method_kwargs_all_with_default(self):
self._test_bound_method_with_default(x=1, y=2, z=3)
def test_bound_method_mixed_kwargs_all_with_default(self):
self._test_bound_method_with_default(1, y=2, z=3)
def test_bound_method_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_default)
def test_bound_method_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_default, 1)
def test_bound_method_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_default, y=2)
def test_bound_method_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_bound_method_with_default, y=2, z=3)
def test_unbound_method_no_args(self):
class Foo(object):
def bar(self):
pass
cliutils.validate_args(Foo.bar, Foo())
def _test_unbound_method_with_args(self, *args, **kwargs):
class Foo(object):
def bar(self, x, y):
pass
cliutils.validate_args(Foo.bar, Foo(), *args, **kwargs)
def test_unbound_method_positional_args(self):
self._test_unbound_method_with_args(1, 2)
def test_unbound_method_kwargs(self):
self._test_unbound_method_with_args(x=1, y=2)
def test_unbound_method_mixed_kwargs(self):
self._test_unbound_method_with_args(1, y=2)
def test_unbound_method_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_args)
def test_unbound_method_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_args, 1)
def test_unbound_method_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_args, y=2)
def _test_unbound_method_with_default(self, *args, **kwargs):
class Foo(object):
def bar(self, x, y, z=3):
pass
cliutils.validate_args(Foo.bar, Foo(), *args, **kwargs)
def test_unbound_method_positional_args_with_default(self):
self._test_unbound_method_with_default(1, 2)
def test_unbound_method_kwargs_with_default(self):
self._test_unbound_method_with_default(x=1, y=2)
def test_unbound_method_mixed_kwargs_with_default(self):
self._test_unbound_method_with_default(1, y=2)
def test_unbound_method_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_default)
def test_unbound_method_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_default, 1)
def test_unbound_method_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_default, y=2)
def test_unbound_method_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_unbound_method_with_default, y=2, z=3)
def test_class_method_no_args(self):
class Foo(object):
@classmethod
def bar(cls):
pass
cliutils.validate_args(Foo.bar)
def _test_class_method_with_args(self, *args, **kwargs):
class Foo(object):
@classmethod
def bar(cls, x, y):
pass
cliutils.validate_args(Foo.bar, *args, **kwargs)
def test_class_method_positional_args(self):
self._test_class_method_with_args(1, 2)
def test_class_method_kwargs(self):
self._test_class_method_with_args(x=1, y=2)
def test_class_method_mixed_kwargs(self):
self._test_class_method_with_args(1, y=2)
def test_class_method_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_args)
def test_class_method_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_args, 1)
def test_class_method_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_args, y=2)
def _test_class_method_with_default(self, *args, **kwargs):
class Foo(object):
@classmethod
def bar(cls, x, y, z=3):
pass
cliutils.validate_args(Foo.bar, *args, **kwargs)
def test_class_method_positional_args_with_default(self):
self._test_class_method_with_default(1, 2)
def test_class_method_kwargs_with_default(self):
self._test_class_method_with_default(x=1, y=2)
def test_class_method_mixed_kwargs_with_default(self):
self._test_class_method_with_default(1, y=2)
def test_class_method_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_default)
def test_class_method_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_default, 1)
def test_class_method_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_default, y=2)
def test_class_method_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_class_method_with_default, y=2, z=3)
def test_static_method_no_args(self):
class Foo(object):
@staticmethod
def bar():
pass
cliutils.validate_args(Foo.bar)
def _test_static_method_with_args(self, *args, **kwargs):
class Foo(object):
@staticmethod
def bar(x, y):
pass
cliutils.validate_args(Foo.bar, *args, **kwargs)
def test_static_method_positional_args(self):
self._test_static_method_with_args(1, 2)
def test_static_method_kwargs(self):
self._test_static_method_with_args(x=1, y=2)
def test_static_method_mixed_kwargs(self):
self._test_static_method_with_args(1, y=2)
def test_static_method_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_args)
def test_static_method_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_args, 1)
def test_static_method_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_args, y=2)
def _test_static_method_with_default(self, *args, **kwargs):
class Foo(object):
@staticmethod
def bar(x, y, z=3):
pass
cliutils.validate_args(Foo.bar, *args, **kwargs)
def test_static_method_positional_args_with_default(self):
self._test_static_method_with_default(1, 2)
def test_static_method_kwargs_with_default(self):
self._test_static_method_with_default(x=1, y=2)
def test_static_method_mixed_kwargs_with_default(self):
self._test_static_method_with_default(1, y=2)
def test_static_method_with_default_missing_args1(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_default)
def test_static_method_with_default_missing_args2(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_default, 1)
def test_static_method_with_default_missing_args3(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_default, y=2)
def test_static_method_with_default_missing_args4(self):
self.assertRaises(cliutils.MissingArgs,
self._test_static_method_with_default, y=2, z=3)
def test_alias_decorator(self):
alias_fn = mock.Mock(name="alias_fn")
cmd_name = "test-command"
wrapped = cliutils.alias(cmd_name)
self.assertEqual(cmd_name, wrapped(alias_fn).alias)
def test_deprecated_args(self):
def command():
@app.command()
def top() -> None:
pass
def deprecated_args(func, *args, **kwargs):
cliutils.deprecated_args(*args, **kwargs)(func)
e = self.assertRaises(ValueError, deprecated_args, command,