Merge "[CLI] group commands in help message"

This commit is contained in:
Jenkins 2016-11-30 23:14:28 +00:00 committed by Gerrit Code Review
commit 51cbe8df32
7 changed files with 118 additions and 21 deletions

View File

@ -40,6 +40,7 @@ _rally()
OPTS["task_list"]="--deployment --all-deployments --status --uuids-only"
OPTS["task_report"]="--tasks --out --open --html --html-static --junit"
OPTS["task_results"]="--uuid"
OPTS["task_sla-check"]="--uuid --json"
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"

View File

@ -362,6 +362,24 @@ def deprecated_args(*args, **kwargs):
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.
@ -373,6 +391,20 @@ def _methods_of(cls):
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
@ -386,10 +418,13 @@ def _compose_category_description(category):
description = doc.strip()
if descr_pairs:
description += "\n\nCommands:\n"
sublen = lambda item: len(item[0])
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:
name = getattr(item[1], "alias", item[0])
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"]
@ -436,6 +471,9 @@ def _add_command_parsers(categories, subparsers):
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),
@ -647,7 +685,7 @@ complete -o filenames -F _rally rally
completion = []
for category, cmds in main.categories.items():
for name, command in _methods_of(cmds):
command_name = getattr(command, "alias", name)
command_name = getattr(command, "alias", name.replace("_", "-"))
args_list = []
for arg in getattr(command, "args", []):
if getattr(command, "deprecated_args", []):

View File

@ -739,6 +739,16 @@ class TaskCommands(object):
else:
_delete_single_task(task_id, force)
@cliutils.args("--uuid", type=str, dest="task_id", help="UUID of task.")
@cliutils.args("--json", dest="tojson",
action="store_true",
help="Output in JSON format.")
@envutils.with_default_task_id
@cliutils.alias("sla_check")
def sla_check_deprecated(self, task_id=None, tojson=False):
"""DEPRECATED since Rally 0.8.0, use `rally task sla-check` instead."""
return self.sla_check(task_id=task_id, tojson=tojson)
@cliutils.args("--uuid", type=str, dest="task_id", help="UUID of task.")
@cliutils.args("--json", dest="tojson",
action="store_true",

View File

@ -110,7 +110,7 @@ function run () {
# NOTE(stpierre): if the sla check fails, we still want osresources.py
# to run, so we turn off -e and save the return value
set +e
rally task sla_check | tee rally-plot/sla.txt
rally task sla-check | tee rally-plot/sla.txt
retval=$?
set -e

View File

@ -111,7 +111,7 @@ def run_task(task, tags=None):
"%s/%s.html" % (pub_dir, task_name)])
run(["rally", "task", "results"],
stdout="%s/results-%s.json" % (pub_dir, task_name))
status = run(["rally", "task", "sla_check"],
status = run(["rally", "task", "sla-check"],
stdout="%s/%s.sla.txt" % (pub_dir, task_name))
run(["rally", "task", "detailed"],
stdout="rally-plot/detailed-%s.txt" % task_name)

View File

@ -206,7 +206,7 @@ class TaskTestCase(unittest.TestCase):
def test_sla_check_with_wrong_task_id(self):
rally = utils.Rally()
self.assertRaises(utils.RallyCliError,
rally, "task sla_check --uuid %s" % FAKE_TASK_UUID)
rally, "task sla-check --uuid %s" % FAKE_TASK_UUID)
def test_status_with_wrong_task_id(self):
rally = utils.Rally()
@ -883,13 +883,13 @@ class SLATestCase(unittest.TestCase):
cfg = self._get_sample_task_config(max_seconds_per_iteration=0.001)
config = utils.TaskConfig(cfg)
rally("task start --task %s" % config.filename)
self.assertRaises(utils.RallyCliError, rally, "task sla_check")
self.assertRaises(utils.RallyCliError, rally, "task sla-check")
def test_sla_success(self):
rally = utils.Rally()
config = utils.TaskConfig(self._get_sample_task_config())
rally("task start --task %s" % config.filename)
rally("task sla_check")
rally("task sla-check")
expected = [
{"benchmark": "KeystoneBasic.create_and_list_users",
"criterion": "failure_rate",
@ -900,7 +900,7 @@ class SLATestCase(unittest.TestCase):
"detail": mock.ANY,
"pos": 0, "status": "PASS"}
]
data = rally("task sla_check --json", getjson=True)
data = rally("task sla-check --json", getjson=True)
self.assertEqual(expected, data)
@ -935,11 +935,11 @@ class SLAExtraFlagsTestCase(unittest.TestCase):
"pos": 0, "status": "FAIL"}
]
try:
rally("task sla_check --json", getjson=True)
rally("task sla-check --json", getjson=True)
except utils.RallyCliError as expected_error:
self.assertEqual(json.loads(expected_error.output), expected)
else:
self.fail("`rally task sla_check` command should return non-zero "
self.fail("`rally task sla-check` command should return non-zero "
"exit code")
def _test_broken_context(self, runner):
@ -963,11 +963,11 @@ class SLAExtraFlagsTestCase(unittest.TestCase):
"pos": 0, "status": "FAIL"}
]
try:
rally("task sla_check --json", getjson=True)
rally("task sla-check --json", getjson=True)
except utils.RallyCliError as expected_error:
self.assertEqual(json.loads(expected_error.output), expected)
else:
self.fail("`rally task sla_check` command should return non-zero "
self.fail("`rally task sla-check` command should return non-zero "
"exit code")
def test_broken_context_with_constant_runner(self):
@ -1012,20 +1012,20 @@ class SLAPerfDegrTestCase(unittest.TestCase):
cfg = self._get_sample_task_config(max_degradation=1)
config = utils.TaskConfig(cfg)
rally("task start --task %s" % config.filename)
self.assertRaises(utils.RallyCliError, rally, "task sla_check")
self.assertRaises(utils.RallyCliError, rally, "task sla-check")
def test_sla_success(self):
rally = utils.Rally()
config = utils.TaskConfig(self._get_sample_task_config())
rally("task start --task %s" % config.filename)
rally("task sla_check")
rally("task sla-check")
expected = [
{"benchmark": "Dummy.dummy_random_action",
"criterion": "performance_degradation",
"detail": mock.ANY,
"pos": 0, "status": "PASS"},
]
data = rally("task sla_check --json", getjson=True)
data = rally("task sla-check --json", getjson=True)
self.assertEqual(expected, data)

View File

@ -17,7 +17,7 @@ import ddt
from keystoneclient import exceptions as keystone_exc
import mock
from oslo_config import cfg
from six import moves
import six
import sqlalchemy.exc
from rally.cli import cliutils
@ -226,7 +226,7 @@ class CliUtilsTestCase(test.TestCase):
def failed_to_open_file(self):
raise IOError("No such file")
ret = cliutils.run(["rally", "failure", "failed_to_open_file"],
ret = cliutils.run(["rally", "failure", "failed-to-open-file"],
{"failure": FailuresCommands})
self.assertEqual(1, ret)
@ -237,7 +237,7 @@ class CliUtilsTestCase(test.TestCase):
def operational_failure(self):
raise sqlalchemy.exc.OperationalError("Can't open DB file")
ret = cliutils.run(["rally", "failure", "operational_failure"],
ret = cliutils.run(["rally", "failure", "operational-failure"],
{"failure": SQLAlchemyCommands})
self.assertEqual(1, ret)
@ -376,13 +376,13 @@ class CliUtilsTestCase(test.TestCase):
"+---+---+")})
@ddt.unpack
def test_print_list(self, args, kwargs, expected):
out = moves.StringIO()
out = six.moves.StringIO()
kwargs["out"] = out
cliutils.print_list(*args, **kwargs)
self.assertEqual(expected, out.getvalue().strip())
def test_print_list_raises(self):
out = moves.StringIO()
out = six.moves.StringIO()
self.assertRaisesRegexp(
ValueError,
"Field labels list.*has different number "
@ -391,6 +391,54 @@ 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
@cliutils.help_group("1_manage")
def reinstall(self):
pass
@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):