diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py index 02f8c05ded..c33589a0c7 100644 --- a/tempest/cli/__init__.py +++ b/tempest/cli/__init__.py @@ -13,14 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import os import shlex import subprocess +import testtools + import tempest.cli.output_parser from tempest import config from tempest import exceptions from tempest.openstack.common import log as logging +from tempest.openstack.common import versionutils import tempest.test @@ -29,6 +33,65 @@ LOG = logging.getLogger(__name__) CONF = config.CONF +def execute(cmd, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes specified command for the given action.""" + cmd = ' '.join([os.path.join(CONF.cli.cli_dir, cmd), + flags, action, params]) + LOG.info("running: '%s'" % cmd) + cmd = shlex.split(cmd.encode('utf-8')) + result = '' + result_err = '' + stdout = subprocess.PIPE + stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE + proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr) + result, result_err = proc.communicate() + if not fail_ok and proc.returncode != 0: + raise exceptions.CommandFailed(proc.returncode, + cmd, + result, + result_err) + return result + + +def check_client_version(client, version): + """Checks if the client's version is compatible with the given version + + @param client: The client to check. + @param version: The version to compare against. + @return: True if the client version is compatible with the given version + parameter, False otherwise. + """ + current_version = execute(client, '', params='--version', + merge_stderr=True) + + if not current_version.strip(): + raise exceptions.TempestException('"%s --version" output was empty' % + client) + + return versionutils.is_compatible(version, current_version, + same_major=False) + + +def min_client_version(*args, **kwargs): + """A decorator to skip tests if the client used isn't of the right version. + + @param client: The client command to run. For python-novaclient, this is + 'nova', for python-cinderclient this is 'cinder', etc. + @param version: The minimum version required to run the CLI test. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*func_args, **func_kwargs): + if not check_client_version(kwargs['client'], kwargs['version']): + msg = "requires %s client version >= %s" % (kwargs['client'], + kwargs['version']) + raise testtools.TestCase.skipException(msg) + return func(*func_args, **func_kwargs) + return wrapper + return decorator + + class ClientTestBase(tempest.test.BaseTestCase): @classmethod def setUpClass(cls): @@ -50,7 +113,7 @@ class ClientTestBase(tempest.test.BaseTestCase): def nova_manage(self, action, flags='', params='', fail_ok=False, merge_stderr=False): """Executes nova-manage command for the given action.""" - return self.cmd( + return execute( 'nova-manage', action, flags, params, fail_ok, merge_stderr) def keystone(self, action, flags='', params='', admin=True, fail_ok=False): @@ -114,28 +177,7 @@ class ClientTestBase(tempest.test.BaseTestCase): CONF.identity.admin_password, CONF.identity.uri)) flags = creds + ' ' + flags - return self.cmd(cmd, action, flags, params, fail_ok, merge_stderr) - - def cmd(self, cmd, action, flags='', params='', fail_ok=False, - merge_stderr=False): - """Executes specified command for the given action.""" - cmd = ' '.join([os.path.join(CONF.cli.cli_dir, cmd), - flags, action, params]) - LOG.info("running: '%s'" % cmd) - cmd = shlex.split(cmd.encode('utf-8')) - result = '' - result_err = '' - stdout = subprocess.PIPE - stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE - proc = subprocess.Popen( - cmd, stdout=stdout, stderr=stderr) - result, result_err = proc.communicate() - if not fail_ok and proc.returncode != 0: - raise exceptions.CommandFailed(proc.returncode, - cmd, - result, - result_err) - return result + return execute(cmd, action, flags, params, fail_ok, merge_stderr) def assertTableStruct(self, items, field_names): """Verify that all items has keys listed in field_names.""" diff --git a/tempest/cli/simple_read_only/test_heat.py b/tempest/cli/simple_read_only/test_heat.py index 7a952fcf89..bd79fa6f38 100644 --- a/tempest/cli/simple_read_only/test_heat.py +++ b/tempest/cli/simple_read_only/test_heat.py @@ -85,6 +85,7 @@ class SimpleReadOnlyHeatClientTest(tempest.cli.ClientTestBase): def test_heat_help(self): self.heat('help') + @tempest.cli.min_client_version(client='heat', version='0.2.7') def test_heat_bash_completion(self): self.heat('bash-completion') diff --git a/tempest/cli/simple_read_only/test_nova.py b/tempest/cli/simple_read_only/test_nova.py index 7085cc9e79..9bac7a6668 100644 --- a/tempest/cli/simple_read_only/test_nova.py +++ b/tempest/cli/simple_read_only/test_nova.py @@ -144,6 +144,7 @@ class SimpleReadOnlyNovaClientTest(cli.ClientTestBase): def test_admin_secgroup_list_rules(self): self.nova('secgroup-list-rules') + @tempest.cli.min_client_version(client='nova', version='2.18') def test_admin_server_group_list(self): self.nova('server-group-list') diff --git a/tempest/tests/cli/test_cli.py b/tempest/tests/cli/test_cli.py new file mode 100644 index 0000000000..1fd5ccbd3d --- /dev/null +++ b/tempest/tests/cli/test_cli.py @@ -0,0 +1,59 @@ +# Copyright 2014 IBM Corp. +# +# 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 +import testtools + +from tempest import cli +from tempest import exceptions +from tempest.tests import base + + +class TestMinClientVersion(base.TestCase): + """Tests for the min_client_version decorator. + """ + + def _test_min_version(self, required, installed, expect_skip): + + @cli.min_client_version(client='nova', version=required) + def fake(self, expect_skip): + if expect_skip: + # If we got here, the decorator didn't raise a skipException as + # expected so we need to fail. + self.fail('Should not have gotten past the decorator.') + + with mock.patch.object(cli, 'execute', + return_value=installed) as mock_cmd: + if expect_skip: + self.assertRaises(testtools.TestCase.skipException, fake, + self, expect_skip) + else: + fake(self, expect_skip) + mock_cmd.assert_called_once_with('nova', '', params='--version', + merge_stderr=True) + + def test_min_client_version(self): + # required, installed, expect_skip + cases = (('2.17.0', '2.17.0', False), + ('2.17.0', '2.18.0', False), + ('2.18.0', '2.17.0', True)) + + for case in cases: + self._test_min_version(*case) + + @mock.patch.object(cli, 'execute', return_value=' ') + def test_check_client_version_empty_output(self, mock_execute): + # Tests that an exception is raised if the command output is empty. + self.assertRaises(exceptions.TempestException, + cli.check_client_version, 'nova', '2.18.0')