diff --git a/etc/rally.bash_completion b/etc/rally.bash_completion index f17b52dd81..7398325425 100644 --- a/etc/rally.bash_completion +++ b/etc/rally.bash_completion @@ -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" diff --git a/rally/cli/cliutils.py b/rally/cli/cliutils.py index bc98166ed5..4958f20e12 100644 --- a/rally/cli/cliutils.py +++ b/rally/cli/cliutils.py @@ -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", []): diff --git a/rally/cli/commands/task.py b/rally/cli/commands/task.py index 0506370bdb..0b521360c5 100644 --- a/rally/cli/commands/task.py +++ b/rally/cli/commands/task.py @@ -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", diff --git a/tests/ci/rally_gate_functions.sh b/tests/ci/rally_gate_functions.sh index dbaba9eebc..a1b5025383 100644 --- a/tests/ci/rally_gate_functions.sh +++ b/tests/ci/rally_gate_functions.sh @@ -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 diff --git a/tests/ci/wip-rally-gate.py b/tests/ci/wip-rally-gate.py index 37930cefe9..4baee40a6c 100755 --- a/tests/ci/wip-rally-gate.py +++ b/tests/ci/wip-rally-gate.py @@ -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) diff --git a/tests/functional/test_cli_task.py b/tests/functional/test_cli_task.py index e8d9da4904..4be26d3c40 100644 --- a/tests/functional/test_cli_task.py +++ b/tests/functional/test_cli_task.py @@ -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) diff --git a/tests/unit/cli/test_cliutils.py b/tests/unit/cli/test_cliutils.py index 7c6aab2cdd..02cd36a306 100644 --- a/tests/unit/cli/test_cliutils.py +++ b/tests/unit/cli/test_cliutils.py @@ -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):