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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -24,6 +25,7 @@ from rally.openstack.common.gettextutils import _
|
|||||||
|
|
||||||
|
|
||||||
def tempest_log_wrapper(func):
|
def tempest_log_wrapper(func):
|
||||||
|
@functools.wraps(func)
|
||||||
def inner_func(scenario_obj, *args, **kwargs):
|
def inner_func(scenario_obj, *args, **kwargs):
|
||||||
if "log_file" not in kwargs:
|
if "log_file" not in kwargs:
|
||||||
# set temporary log file
|
# 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 import cliutils
|
||||||
from rally.cmd.commands import deployment
|
from rally.cmd.commands import deployment
|
||||||
|
from rally.cmd.commands import info
|
||||||
from rally.cmd.commands import show
|
from rally.cmd.commands import show
|
||||||
from rally.cmd.commands import task
|
from rally.cmd.commands import task
|
||||||
from rally.cmd.commands import use
|
from rally.cmd.commands import use
|
||||||
@ -30,6 +31,7 @@ from rally.cmd.commands import verify
|
|||||||
def main():
|
def main():
|
||||||
categories = {
|
categories = {
|
||||||
'deployment': deployment.DeploymentCommands,
|
'deployment': deployment.DeploymentCommands,
|
||||||
|
'info': info.InfoCommands,
|
||||||
'show': show.ShowCommands,
|
'show': show.ShowCommands,
|
||||||
'task': task.TaskCommands,
|
'task': task.TaskCommands,
|
||||||
'use': use.UseCommands,
|
'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 functools
|
||||||
import imp
|
import imp
|
||||||
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import StringIO
|
import StringIO
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from sphinx.util import docstrings
|
||||||
|
|
||||||
from rally import exceptions
|
from rally import exceptions
|
||||||
from rally.openstack.common.gettextutils import _
|
from rally.openstack.common.gettextutils import _
|
||||||
@ -193,3 +196,112 @@ def load_plugins(directory):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(_("Couldn't load module from %(path)s: %(msg)s") %
|
LOG.error(_("Couldn't load module from %(path)s: %(msg)s") %
|
||||||
{"path": fullpath, "msg": six.text_type(e)})
|
{"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
|
python-subunit>=0.0.18
|
||||||
requests>=1.2.1
|
requests>=1.2.1
|
||||||
SQLAlchemy>=0.8.4,<=0.8.99,>=0.9.7,<=0.9.99
|
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
|
six>=1.7.0
|
||||||
WSME>=0.6
|
WSME>=0.6
|
||||||
|
@ -6,6 +6,5 @@ mock>=1.0
|
|||||||
testrepository>=0.0.18
|
testrepository>=0.0.18
|
||||||
testtools>=0.9.34
|
testtools>=0.9.34
|
||||||
|
|
||||||
sphinx>=1.1.2,!=1.2.0,<1.3
|
|
||||||
oslosphinx
|
oslosphinx
|
||||||
oslotest
|
oslotest
|
||||||
|
@ -38,6 +38,7 @@ class TempestLogWrappersTestCase(test.TestCase):
|
|||||||
def test_launch_without_specified_log_file(self, mock_tmp):
|
def test_launch_without_specified_log_file(self, mock_tmp):
|
||||||
mock_tmp.NamedTemporaryFile().name = "tmp_file"
|
mock_tmp.NamedTemporaryFile().name = "tmp_file"
|
||||||
target_func = mock.MagicMock()
|
target_func = mock.MagicMock()
|
||||||
|
target_func.__name__ = "target_func"
|
||||||
func = utils.tempest_log_wrapper(target_func)
|
func = utils.tempest_log_wrapper(target_func)
|
||||||
|
|
||||||
func(self.scenario)
|
func(self.scenario)
|
||||||
@ -48,6 +49,7 @@ class TempestLogWrappersTestCase(test.TestCase):
|
|||||||
@mock.patch(TS + ".utils.tempfile")
|
@mock.patch(TS + ".utils.tempfile")
|
||||||
def test_launch_with_specified_log_file(self, mock_tmp):
|
def test_launch_with_specified_log_file(self, mock_tmp):
|
||||||
target_func = mock.MagicMock()
|
target_func = mock.MagicMock()
|
||||||
|
target_func.__name__ = "target_func"
|
||||||
func = utils.tempest_log_wrapper(target_func)
|
func = utils.tempest_log_wrapper(target_func)
|
||||||
|
|
||||||
func(self.scenario, log_file='log_file')
|
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
|
# test no fails if module is broken
|
||||||
# TODO(olkonami): check exception is handled correct
|
# TODO(olkonami): check exception is handled correct
|
||||||
utils.load_plugins("/somwhere")
|
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