Merge "Add "rally info" command"
This commit is contained in:
commit
c0874b2667
@ -13,6 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import functools
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
@ -24,6 +25,7 @@ from rally.openstack.common.gettextutils import _
|
||||
|
||||
|
||||
def tempest_log_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def inner_func(scenario_obj, *args, **kwargs):
|
||||
if "log_file" not in kwargs:
|
||||
# set temporary log file
|
||||
|
81
rally/cmd/commands/info.py
Normal file
81
rally/cmd/commands/info.py
Normal file
@ -0,0 +1,81 @@
|
||||
# Copyright 2014: 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
""" Rally command: info
|
||||
|
||||
Samples:
|
||||
|
||||
$ rally info find create_meter_and_get_stats
|
||||
CeilometerStats.create_meter_and_get_stats (benchmark scenario).
|
||||
|
||||
Test creating a meter and fetching its statistics.
|
||||
|
||||
Meter is first created and then statistics is fetched for the same
|
||||
using GET /v2/meters/(meter_name)/statistics.
|
||||
Parameters:
|
||||
- name_length: length of generated (random) part of meter name
|
||||
- kwargs: contains optional arguments to create a meter
|
||||
|
||||
$ rally info find Authenticate
|
||||
Authenticate (benchmark scenario group).
|
||||
|
||||
This class should contain authentication mechanism.
|
||||
|
||||
For different types of clients like Keystone.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from rally.cmd import cliutils
|
||||
from rally import searchutils
|
||||
from rally import utils
|
||||
|
||||
|
||||
class InfoCommands(object):
|
||||
|
||||
@cliutils.args("--query", dest="query", type=str, help="Search query.")
|
||||
def find(self, query):
|
||||
"""Search for an entity that matches the query and print info about it.
|
||||
|
||||
:param query: search query.
|
||||
"""
|
||||
scenario_group = searchutils.find_benchmark_scenario_group(query)
|
||||
if scenario_group:
|
||||
print("%s (benchmark scenario group).\n" % scenario_group.__name__)
|
||||
# TODO(msdubov): Provide all scenario classes with docstrings.
|
||||
doc = utils.format_docstring(scenario_group.__doc__)
|
||||
print(doc)
|
||||
return
|
||||
|
||||
scenario = searchutils.find_benchmark_scenario(query)
|
||||
if scenario:
|
||||
print("%(scenario_group)s.%(scenario_name)s "
|
||||
"(benchmark scenario).\n" %
|
||||
{"scenario_group": utils.get_method_class(scenario).__name__,
|
||||
"scenario_name": scenario.__name__})
|
||||
doc = utils.parse_docstring(scenario.__doc__)
|
||||
print(doc["short_description"] + "\n")
|
||||
if doc["long_description"]:
|
||||
print(doc["long_description"] + "\n")
|
||||
if doc["params"]:
|
||||
print("Parameters:")
|
||||
for param in doc["params"]:
|
||||
print(" - %(name)s: %(doc)s" % param)
|
||||
if doc["returns"]:
|
||||
print("Returns: %s" % doc["returns"])
|
||||
return
|
||||
|
||||
print("Failed to find any docs for query: '%s'" % query)
|
||||
return 1
|
@ -21,6 +21,7 @@ import sys
|
||||
|
||||
from rally.cmd import cliutils
|
||||
from rally.cmd.commands import deployment
|
||||
from rally.cmd.commands import info
|
||||
from rally.cmd.commands import show
|
||||
from rally.cmd.commands import task
|
||||
from rally.cmd.commands import use
|
||||
@ -30,6 +31,7 @@ from rally.cmd.commands import verify
|
||||
def main():
|
||||
categories = {
|
||||
'deployment': deployment.DeploymentCommands,
|
||||
'info': info.InfoCommands,
|
||||
'show': show.ShowCommands,
|
||||
'task': task.TaskCommands,
|
||||
'use': use.UseCommands,
|
||||
|
64
rally/searchutils.py
Normal file
64
rally/searchutils.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright 2014: 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
""" Rally entities discovery by queries. """
|
||||
|
||||
from rally.benchmark.scenarios import base as scenarios_base
|
||||
from rally import exceptions
|
||||
from rally import utils
|
||||
|
||||
|
||||
def find_benchmark_scenario_group(query):
|
||||
"""Find a scenario class by query.
|
||||
|
||||
:param query: string with the name of the class being searched.
|
||||
:returns: class object or None if the query doesn't match any
|
||||
scenario class.
|
||||
"""
|
||||
try:
|
||||
# TODO(msdubov): support approximate string matching
|
||||
# (here and in other find_* methods).
|
||||
return scenarios_base.Scenario.get_by_name(query)
|
||||
except exceptions.NoSuchScenario:
|
||||
return None
|
||||
|
||||
|
||||
def find_benchmark_scenario(query):
|
||||
"""Find a scenario method by query.
|
||||
|
||||
:param query: string with the name of the benchmark scenario being
|
||||
searched. It can be either a full name (e.g,
|
||||
'NovaServers.boot_server') or just a method name (e.g.,
|
||||
'boot_server')
|
||||
:returns: method object or None if the query doesn't match any
|
||||
scenario method.
|
||||
"""
|
||||
if "." in query:
|
||||
scenario_group, scenario_name = query.split(".", 1)
|
||||
else:
|
||||
scenario_group = None
|
||||
scenario_name = query
|
||||
|
||||
if scenario_group:
|
||||
scenario_cls = find_benchmark_scenario_group(scenario_group)
|
||||
if hasattr(scenario_cls, scenario_name):
|
||||
return getattr(scenario_cls, scenario_name)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
for scenario_cls in utils.itersubclasses(scenarios_base.Scenario):
|
||||
if scenario_name in dir(scenario_cls):
|
||||
return getattr(scenario_cls, scenario_name)
|
||||
return None
|
112
rally/utils.py
112
rally/utils.py
@ -15,13 +15,16 @@
|
||||
|
||||
import functools
|
||||
import imp
|
||||
import inspect
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import StringIO
|
||||
import sys
|
||||
import time
|
||||
|
||||
import six
|
||||
from sphinx.util import docstrings
|
||||
|
||||
from rally import exceptions
|
||||
from rally.openstack.common.gettextutils import _
|
||||
@ -193,3 +196,112 @@ def load_plugins(directory):
|
||||
except Exception as e:
|
||||
LOG.error(_("Couldn't load module from %(path)s: %(msg)s") %
|
||||
{"path": fullpath, "msg": six.text_type(e)})
|
||||
|
||||
|
||||
def get_method_class(func):
|
||||
"""Return the class that defined the given method.
|
||||
|
||||
:param func: function to get the class for.
|
||||
:returns: class object or None if func is not an instance method.
|
||||
"""
|
||||
if not hasattr(func, "im_class"):
|
||||
return None
|
||||
for cls in inspect.getmro(func.im_class):
|
||||
if func.__name__ in cls.__dict__:
|
||||
return cls
|
||||
return None
|
||||
|
||||
|
||||
def first_index(lst, predicate):
|
||||
"""Return the index of the first element that matches a predicate.
|
||||
|
||||
:param lst: list to find the matching element in.
|
||||
:param predicate: predicate object.
|
||||
:returns: the index of the first matching element or None if no element
|
||||
matches the predicate.
|
||||
"""
|
||||
for i in range(len(lst)):
|
||||
if predicate(lst[i]):
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def format_docstring(docstring):
|
||||
"""Format the docstring to make it well-readable.
|
||||
|
||||
:param docstring: string.
|
||||
:returns: formatted string.
|
||||
"""
|
||||
if docstring:
|
||||
return "\n".join(docstrings.prepare_docstring(docstring))
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def parse_docstring(docstring):
|
||||
"""Parse the docstring into its components.
|
||||
|
||||
:returns: a dictionary of form
|
||||
{
|
||||
"short_description": ...,
|
||||
"long_description": ...,
|
||||
"params": [{"name": ..., "doc": ...}, ...],
|
||||
"returns": ...
|
||||
}
|
||||
"""
|
||||
|
||||
if docstring:
|
||||
docstring_lines = docstrings.prepare_docstring(docstring)
|
||||
docstring_lines = filter(lambda line: line != "", docstring_lines)
|
||||
else:
|
||||
docstring_lines = []
|
||||
|
||||
if docstring_lines:
|
||||
|
||||
short_description = docstring_lines[0]
|
||||
|
||||
param_lines_start = first_index(docstring_lines,
|
||||
lambda line: line.startswith(":param")
|
||||
or line.startswith(":returns"))
|
||||
if param_lines_start:
|
||||
long_description = "\n".join(docstring_lines[1:param_lines_start])
|
||||
else:
|
||||
long_description = "\n".join(docstring_lines[1:])
|
||||
|
||||
if not long_description:
|
||||
long_description = None
|
||||
|
||||
params = []
|
||||
param_regex = re.compile("^:param (?P<name>\w+): (?P<doc>.*)$")
|
||||
for param_line in filter(lambda line: line.startswith(":param"),
|
||||
docstring_lines):
|
||||
match = param_regex.match(param_line)
|
||||
if match:
|
||||
params.append({
|
||||
"name": match.group("name"),
|
||||
"doc": match.group("doc")
|
||||
})
|
||||
|
||||
returns = None
|
||||
returns_line = filter(lambda line: line.startswith(":returns"),
|
||||
docstring_lines)
|
||||
if returns_line:
|
||||
returns_regex = re.compile("^:returns: (?P<doc>.*)$")
|
||||
match = returns_regex.match(returns_line[0])
|
||||
if match:
|
||||
returns = match.group("doc")
|
||||
|
||||
return {
|
||||
"short_description": short_description,
|
||||
"long_description": long_description,
|
||||
"params": params,
|
||||
"returns": returns
|
||||
}
|
||||
|
||||
else:
|
||||
return {
|
||||
"short_description": None,
|
||||
"long_description": None,
|
||||
"params": [],
|
||||
"returns": None
|
||||
}
|
||||
|
@ -23,5 +23,6 @@ python-saharaclient>=0.6.0
|
||||
python-subunit>=0.0.18
|
||||
requests>=1.2.1
|
||||
SQLAlchemy>=0.8.4,<=0.8.99,>=0.9.7,<=0.9.99
|
||||
sphinx>=1.1.2,!=1.2.0,<1.3
|
||||
six>=1.7.0
|
||||
WSME>=0.6
|
||||
|
@ -6,6 +6,5 @@ mock>=1.0
|
||||
testrepository>=0.0.18
|
||||
testtools>=0.9.34
|
||||
|
||||
sphinx>=1.1.2,!=1.2.0,<1.3
|
||||
oslosphinx
|
||||
oslotest
|
||||
|
@ -38,6 +38,7 @@ class TempestLogWrappersTestCase(test.TestCase):
|
||||
def test_launch_without_specified_log_file(self, mock_tmp):
|
||||
mock_tmp.NamedTemporaryFile().name = "tmp_file"
|
||||
target_func = mock.MagicMock()
|
||||
target_func.__name__ = "target_func"
|
||||
func = utils.tempest_log_wrapper(target_func)
|
||||
|
||||
func(self.scenario)
|
||||
@ -48,6 +49,7 @@ class TempestLogWrappersTestCase(test.TestCase):
|
||||
@mock.patch(TS + ".utils.tempfile")
|
||||
def test_launch_with_specified_log_file(self, mock_tmp):
|
||||
target_func = mock.MagicMock()
|
||||
target_func.__name__ = "target_func"
|
||||
func = utils.tempest_log_wrapper(target_func)
|
||||
|
||||
func(self.scenario, log_file='log_file')
|
||||
|
50
tests/cmd/commands/test_info.py
Normal file
50
tests/cmd/commands/test_info.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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
|
||||
#
|
||||
# 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 mock
|
||||
|
||||
from rally.benchmark.scenarios.dummy import dummy
|
||||
from rally.cmd.commands import info
|
||||
from tests import test
|
||||
|
||||
|
||||
class InfoCommandsTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(InfoCommandsTestCase, self).setUp()
|
||||
self.info = info.InfoCommands()
|
||||
|
||||
@mock.patch("rally.searchutils.find_benchmark_scenario_group")
|
||||
def test_find_dummy_scenario_group(self, mock_find):
|
||||
query = "Dummy"
|
||||
mock_find.return_value = dummy.Dummy
|
||||
status = self.info.find(query)
|
||||
mock_find.assert_called_once_with(query)
|
||||
self.assertEqual(None, status)
|
||||
|
||||
@mock.patch("rally.searchutils.find_benchmark_scenario")
|
||||
def test_find_dummy_scenario(self, mock_find):
|
||||
query = "Dummy.dummy"
|
||||
mock_find.return_value = dummy.Dummy.dummy
|
||||
status = self.info.find(query)
|
||||
mock_find.assert_called_once_with(query)
|
||||
self.assertEqual(None, status)
|
||||
|
||||
@mock.patch("rally.searchutils.find_benchmark_scenario")
|
||||
def test_find_failure_status(self, mock_find):
|
||||
query = "Dummy.non_existing"
|
||||
mock_find.return_value = None
|
||||
status = self.info.find(query)
|
||||
mock_find.assert_called_once_with(query)
|
||||
self.assertEqual(1, status)
|
54
tests/test_searchutils.py
Normal file
54
tests/test_searchutils.py
Normal file
@ -0,0 +1,54 @@
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Test for Rally search utils."""
|
||||
|
||||
from rally.benchmark.scenarios.dummy import dummy
|
||||
from rally import searchutils
|
||||
from tests import test
|
||||
|
||||
|
||||
class FindBenchmarkScenarioGroupTestCase(test.TestCase):
|
||||
|
||||
def test_find_success(self):
|
||||
scenario_group = searchutils.find_benchmark_scenario_group("Dummy")
|
||||
self.assertEqual(scenario_group, dummy.Dummy)
|
||||
|
||||
def test_find_failure(self):
|
||||
scenario_group = searchutils.find_benchmark_scenario_group("Dumy")
|
||||
self.assertEqual(scenario_group, None)
|
||||
|
||||
|
||||
class FindBenchmarkScenarioTestCase(test.TestCase):
|
||||
|
||||
def test_find_success_full_path(self):
|
||||
scenario_method = searchutils.find_benchmark_scenario("Dummy.dummy")
|
||||
self.assertEqual(scenario_method, dummy.Dummy.dummy)
|
||||
|
||||
def test_find_success_shortened_path(self):
|
||||
scenario_method = searchutils.find_benchmark_scenario("dummy")
|
||||
self.assertEqual(scenario_method, dummy.Dummy.dummy)
|
||||
|
||||
def test_find_failure_bad_shortening(self):
|
||||
scenario_method = searchutils.find_benchmark_scenario("dumy")
|
||||
self.assertEqual(scenario_method, None)
|
||||
|
||||
def test_find_failure_bad_group_name(self):
|
||||
scenario_method = searchutils.find_benchmark_scenario("Dumy.dummy")
|
||||
self.assertEqual(scenario_method, None)
|
||||
|
||||
def test_find_failure_bad_scenario_name(self):
|
||||
scenario_method = searchutils.find_benchmark_scenario("Dummy.dumy")
|
||||
self.assertEqual(scenario_method, None)
|
@ -219,3 +219,93 @@ class LoadExtraModulesTestCase(test.TestCase):
|
||||
# test no fails if module is broken
|
||||
# TODO(olkonami): check exception is handled correct
|
||||
utils.load_plugins("/somwhere")
|
||||
|
||||
|
||||
def module_level_method():
|
||||
pass
|
||||
|
||||
|
||||
class MethodClassTestCase(test.TestCase):
|
||||
|
||||
def test_method_class_for_class_level_method(self):
|
||||
class A:
|
||||
def m(self):
|
||||
pass
|
||||
self.assertEqual(utils.get_method_class(A.m), A)
|
||||
|
||||
def test_method_class_for_module_level_method(self):
|
||||
self.assertIsNone(utils.get_method_class(module_level_method))
|
||||
|
||||
|
||||
class FirstIndexTestCase(test.TestCase):
|
||||
|
||||
def test_list_with_existing_matching_element(self):
|
||||
lst = [1, 3, 5, 7]
|
||||
self.assertEqual(utils.first_index(lst, lambda e: e == 1), 0)
|
||||
self.assertEqual(utils.first_index(lst, lambda e: e == 5), 2)
|
||||
self.assertEqual(utils.first_index(lst, lambda e: e == 7), 3)
|
||||
|
||||
def test_list_with_non_existing_matching_element(self):
|
||||
lst = [1, 3, 5, 7]
|
||||
self.assertEqual(utils.first_index(lst, lambda e: e == 2), None)
|
||||
|
||||
|
||||
class DocstringTestCase(test.TestCase):
|
||||
|
||||
def test_parse_complete_docstring(self):
|
||||
docstring = """One-line description.
|
||||
|
||||
Multi-
|
||||
line-
|
||||
description.
|
||||
|
||||
:param p1: Param 1 description.
|
||||
:param p2: Param 2 description.
|
||||
:returns: Return value description.
|
||||
"""
|
||||
|
||||
dct = utils.parse_docstring(docstring)
|
||||
expected = {
|
||||
"short_description": "One-line description.",
|
||||
"long_description": "Multi-\nline-\ndescription.",
|
||||
"params": [{"name": "p1", "doc": "Param 1 description."},
|
||||
{"name": "p2", "doc": "Param 2 description."}],
|
||||
"returns": "Return value description."
|
||||
}
|
||||
self.assertEqual(dct, expected)
|
||||
|
||||
def test_parse_incomplete_docstring(self):
|
||||
docstring = """One-line description.
|
||||
|
||||
:param p1: Param 1 description.
|
||||
:param p2: Param 2 description.
|
||||
"""
|
||||
|
||||
dct = utils.parse_docstring(docstring)
|
||||
expected = {
|
||||
"short_description": "One-line description.",
|
||||
"long_description": None,
|
||||
"params": [{"name": "p1", "doc": "Param 1 description."},
|
||||
{"name": "p2", "doc": "Param 2 description."}],
|
||||
"returns": None
|
||||
}
|
||||
self.assertEqual(dct, expected)
|
||||
|
||||
def test_parse_docstring_with_no_params(self):
|
||||
docstring = """One-line description.
|
||||
|
||||
Multi-
|
||||
line-
|
||||
description.
|
||||
|
||||
:returns: Return value description.
|
||||
"""
|
||||
|
||||
dct = utils.parse_docstring(docstring)
|
||||
expected = {
|
||||
"short_description": "One-line description.",
|
||||
"long_description": "Multi-\nline-\ndescription.",
|
||||
"params": [],
|
||||
"returns": "Return value description."
|
||||
}
|
||||
self.assertEqual(dct, expected)
|
||||
|
Loading…
Reference in New Issue
Block a user