Merge "Add "rally info" command"

This commit is contained in:
Jenkins 2014-08-19 20:26:26 +00:00 committed by Gerrit Code Review
commit c0874b2667
11 changed files with 458 additions and 1 deletions

View File

@ -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

View 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

View File

@ -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
View 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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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')

View 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
View 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)

View File

@ -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)