From b0132891637a3a8f6b0bdf425e36551eae34ed1f Mon Sep 17 00:00:00 2001 From: Lilywei123 Date: Tue, 6 Jan 2015 17:44:21 +0800 Subject: [PATCH 01/15] Update base.py to include python-openstackclient. Change-Id: I8a213960e41fb1403a6f0e19068ffef178cbaccf --- tempest_lib/cli/base.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tempest_lib/cli/base.py b/tempest_lib/cli/base.py index a573fc4..95eb499 100644 --- a/tempest_lib/cli/base.py +++ b/tempest_lib/cli/base.py @@ -307,6 +307,25 @@ class CLIClient(object): return self.cmd_with_auth( 'sahara', action, flags, params, fail_ok, merge_stderr) + def openstack(self, action, flags='', params='', fail_ok=False, + merge_stderr=False): + """Executes openstack command for the given action. + + :param action: the cli command to run using openstack + :type action: string + :param flags: any optional cli flags to use + :type flags: string + :param params: any optional positional args to use + :type params: string + :param fail_ok: if True an exception is not raised when the + cli return code is non-zero + :type fail_ok: boolean + :param merge_stderr: if True the stderr buffer is merged into stdout + :type merge_stderr: boolean + """ + return self.cmd_with_auth( + 'openstack', action, flags, params, fail_ok, merge_stderr) + def cmd_with_auth(self, cmd, action, flags='', params='', fail_ok=False, merge_stderr=False): """Executes given command with auth attributes appended. From efb491bded68617f6361f2fc432f881c4e1ff93b Mon Sep 17 00:00:00 2001 From: OpenStack Proposal Bot Date: Fri, 16 Jan 2015 13:30:47 +0000 Subject: [PATCH 02/15] Updated from global requirements Change-Id: Iff36ed9b762cda634434dd14b8e1a27c139c2ce9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e7e97ad..8ce9a01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pbr>=0.6,!=0.7,<1.0 Babel>=1.3 fixtures>=0.3.14 -oslo.config>=1.4.0 # Apache-2.0 +oslo.config>=1.6.0 # Apache-2.0 iso8601>=0.1.9 jsonschema>=2.0.0,<3.0.0 httplib2>=0.7.5 From c473cda72f2ea6e3af6b16f61fd73234ba42cd8b Mon Sep 17 00:00:00 2001 From: "Mauro S. M. Rodrigues" Date: Fri, 16 Jan 2015 20:12:04 +0000 Subject: [PATCH 03/15] Fix migrate_from_tempest script to work with multiple files The '--follow' option from git log was used to follow the file even if it was renamed for instance, although the option can't deal with multiple files which was blocking us to migrate multiple files at once. This patches fixes the issue by looking one file at time and concatenating their ids in the end. Change-Id: I39a2e94e1c27a83de2b976f263928c5199d44b22 Closes-bug: #1411499 --- tools/migrate_from_tempest.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/migrate_from_tempest.sh b/tools/migrate_from_tempest.sh index 4751d0b..7719362 100755 --- a/tools/migrate_from_tempest.sh +++ b/tools/migrate_from_tempest.sh @@ -46,11 +46,14 @@ function count_commits { git clone $TEMPEST_GIT_URL $tmpdir cd $tmpdir -# get only commits that touch our files -commits="$(git log --format=format:%h --no-merges --follow -- $files)" -# then their merge commits - which works fina since we merge commits -# individually. -merge_commits="$(git log --format=format:%h --merges --first-parent -- $files)" +for file in $files; do + # get only commits that touch our files + commits="$commits $(git log --format=format:%h --no-merges --follow -- $file)" + # then their merge commits - which works fina since we merge commits + # individually. + merge_commits="$merge_commits $(git log --format=format:%h --merges --first-parent -- $file)" +done + pattern="\n$(echo $commits $merge_commits | sed -e 's/ /\\|/g')" # order them by filtering each one in the order it appears on rev-list From 9018af62c46e1cecb955ebe29960c98c548b63f8 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 19 Jan 2015 04:46:39 +0000 Subject: [PATCH 04/15] Add RateLimitExceeded test case The rest_client module raises RateLimitExceeded exception, but there was not any tests for the exception. This patch adds it. Change-Id: I933ebe2ed52750088e3941b308b669f088d7f8ed --- tempest_lib/tests/test_rest_client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tempest_lib/tests/test_rest_client.py b/tempest_lib/tests/test_rest_client.py index 5721b18..dc819d0 100644 --- a/tempest_lib/tests/test_rest_client.py +++ b/tempest_lib/tests/test_rest_client.py @@ -274,10 +274,15 @@ class TestRestClientParseRespJSON(BaseRestClientTestClass): class TestRestClientErrorCheckerJSON(base.TestCase): c_type = "application/json" - def set_data(self, r_code, enc=None, r_body=None): + def set_data(self, r_code, enc=None, r_body=None, absolute_limit=True): if enc is None: enc = self.c_type resp_dict = {'status': r_code, 'content-type': enc} + resp_body = {'resp_body': 'fake_resp_body'} + + if absolute_limit is False: + resp_dict.update({'retry-after': 120}) + resp_body.update({'overLimit': {'message': 'fake_message'}}) resp = httplib2.Response(resp_dict) data = { "method": "fake_method", @@ -285,7 +290,7 @@ class TestRestClientErrorCheckerJSON(base.TestCase): "headers": "fake_headers", "body": "fake_body", "resp": resp, - "resp_body": '{"resp_body": "fake_resp_body"}', + "resp_body": json.dumps(resp_body) } if r_body is not None: data.update({"resp_body": r_body}) @@ -329,6 +334,11 @@ class TestRestClientErrorCheckerJSON(base.TestCase): self.rest_client._error_checker, **self.set_data("413")) + def test_response_413_without_absolute_limit(self): + self.assertRaises(exceptions.RateLimitExceeded, + self.rest_client._error_checker, + **self.set_data("413", absolute_limit=False)) + def test_response_415(self): self.assertRaises(exceptions.InvalidContentType, self.rest_client._error_checker, @@ -381,6 +391,11 @@ class TestRestClientErrorCheckerTEXT(TestRestClientErrorCheckerJSON): self.rest_client._error_checker, **self.set_data("405", enc="fake_enc")) + def test_response_413_without_absolute_limit(self): + # Skip this test because rest_client cannot get overLimit message + # from text body. + pass + class TestRestClientUtils(BaseRestClientTestClass): From 26828ccc9e371200c44df88c36a4cfc641384de8 Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 19 Jan 2015 01:49:00 +0000 Subject: [PATCH 05/15] Remove unused exceptions Before this patch, exceptions.py is synced with Tempest's exceptions.py. However, it is difficult to do it now because rest_client raises its own exceptions on tempest-lib and Tempest should handle them without Tempest's exceptions. This patch removes unused exceptions in tempest-lib for knowing tempest-lib own exceptions easily. Change-Id: I435f0752685f54e6b86d1f243a5236d79c9e250c --- tempest_lib/exceptions.py | 88 --------------------------------------- 1 file changed, 88 deletions(-) diff --git a/tempest_lib/exceptions.py b/tempest_lib/exceptions.py index 69aaf2a..8907cd1 100644 --- a/tempest_lib/exceptions.py +++ b/tempest_lib/exceptions.py @@ -54,14 +54,6 @@ class RFCViolation(RestClientException): message = "RFC Violation" -class InvalidConfiguration(TempestException): - message = "Invalid Configuration" - - -class InvalidCredentials(TempestException): - message = "Invalid Credentials" - - class InvalidHttpSuccessCode(RestClientException): message = "The success code is different than the expected one" @@ -74,54 +66,10 @@ class Unauthorized(RestClientException): message = 'Unauthorized' -class InvalidServiceTag(TempestException): - message = "Invalid service tag" - - class TimeoutException(TempestException): message = "Request timed out" -class BuildErrorException(TempestException): - message = "Server %(server_id)s failed to build and is in ERROR status" - - -class ImageKilledException(TempestException): - message = "Image %(image_id)s 'killed' while waiting for '%(status)s'" - - -class AddImageException(TempestException): - message = "Image %(image_id)s failed to become ACTIVE in the allotted time" - - -class EC2RegisterImageException(TempestException): - message = ("Image %(image_id)s failed to become 'available' " - "in the allotted time") - - -class VolumeBuildErrorException(TempestException): - message = "Volume %(volume_id)s failed to build and is in ERROR status" - - -class SnapshotBuildErrorException(TempestException): - message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status" - - -class VolumeBackupException(TempestException): - message = "Volume backup %(backup_id)s failed and is in ERROR status" - - -class StackBuildErrorException(TempestException): - message = ("Stack %(stack_identifier)s is in %(stack_status)s status " - "due to '%(stack_status_reason)s'") - - -class StackResourceBuildErrorException(TempestException): - message = ("Resource %(resource_name)s in stack %(stack_identifier)s is " - "in %(resource_status)s status due to " - "'%(resource_status_reason)s'") - - class BadRequest(RestClientException): message = "Bad request" @@ -130,15 +78,6 @@ class UnprocessableEntity(RestClientException): message = "Unprocessable entity" -class AuthenticationFailure(RestClientException): - message = ("Authentication with user %(user)s and password " - "%(password)s failed auth using tenant %(tenant)s.") - - -class EndpointNotFound(TempestException): - message = "Endpoint not found" - - class RateLimitExceeded(RestClientException): message = "Rate limit exceeded" @@ -155,37 +94,10 @@ class NotImplemented(RestClientException): message = "Got NotImplemented error" -class ImageFault(TempestException): - message = "Got image fault" - - -class IdentityError(TempestException): - message = "Got identity error" - - class Conflict(RestClientException): message = "An object with that identifier already exists" -class SSHTimeout(TempestException): - message = ("Connection to the %(host)s via SSH timed out.\n" - "User: %(user)s, Password: %(password)s") - - -class SSHExecCommandFailed(TempestException): - """Raised when remotely executed command returns nonzero status.""" - message = ("Command '%(command)s', exit status: %(exit_status)d, " - "Error:\n%(strerror)s") - - -class ServerUnreachable(TempestException): - message = "The server is not reachable via the configured network" - - -class TearDownException(TempestException): - message = "%(num)d cleanUp operation failed" - - class ResponseWithNonEmptyBody(RFCViolation): message = ("RFC Violation! Response with %(status)d HTTP Status Code " "MUST NOT have a body") From ebd5ed69fc6034e514c738d88a7daaa6c556952b Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Mon, 19 Jan 2015 01:56:45 +0000 Subject: [PATCH 06/15] Make rest_client exception inheritances easy There are rest_client own exceptions, but some of these inheritances are not reasonable. Then, tempest-lib users(like Tempest) can not know what exceptions happen when using rest_client module of tempest-lib. This patch makes them easy. Change-Id: Iba5108eb39b76153326020d2281f8f99ddf1bec6 --- tempest_lib/exceptions.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tempest_lib/exceptions.py b/tempest_lib/exceptions.py index 8907cd1..d97d158 100644 --- a/tempest_lib/exceptions.py +++ b/tempest_lib/exceptions.py @@ -50,10 +50,6 @@ class RestClientException(TempestException, pass -class RFCViolation(RestClientException): - message = "RFC Violation" - - class InvalidHttpSuccessCode(RestClientException): message = "The success code is different than the expected one" @@ -66,7 +62,7 @@ class Unauthorized(RestClientException): message = 'Unauthorized' -class TimeoutException(TempestException): +class TimeoutException(RestClientException): message = "Request timed out" @@ -98,12 +94,12 @@ class Conflict(RestClientException): message = "An object with that identifier already exists" -class ResponseWithNonEmptyBody(RFCViolation): +class ResponseWithNonEmptyBody(RestClientException): message = ("RFC Violation! Response with %(status)d HTTP Status Code " "MUST NOT have a body") -class ResponseWithEntity(RFCViolation): +class ResponseWithEntity(RestClientException): message = ("RFC Violation! Response with 205 HTTP Status Code " "MUST NOT have an entity") From 4927e4d4c8dad97431af51fc42fed5f6f0989b69 Mon Sep 17 00:00:00 2001 From: Joe Gordon Date: Tue, 27 Jan 2015 11:14:55 -0800 Subject: [PATCH 07/15] Fix typo: clien => client fix typo in docstring since they are used to generate documentation. Change-Id: I95f97da0992df5a332750824044eb16f86d17a7b --- tempest_lib/cli/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tempest_lib/cli/base.py b/tempest_lib/cli/base.py index 95eb499..ec966ca 100644 --- a/tempest_lib/cli/base.py +++ b/tempest_lib/cli/base.py @@ -76,7 +76,7 @@ class CLIClient(object): :type tenant_name: string :param uri: The auth uri for the OpenStack Deployment :type uri: string - :param cli_dir: The path where the python clien binaries are installed. + :param cli_dir: The path where the python client binaries are installed. defaults to /usr/bin :type cli_dir: string """ From e319a7f4854ca04adafb015660ca7c09c509e3ef Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Tue, 27 Jan 2015 21:55:39 -0800 Subject: [PATCH 08/15] Separate Forbidden exception from Unauthorized Closes-Bug:#1415143 Change-Id: I890498b2df6ae8d8f689537c8d6da1b5c06c2bd6 --- tempest_lib/common/rest_client.py | 5 ++++- tempest_lib/exceptions.py | 4 ++++ tempest_lib/tests/test_rest_client.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tempest_lib/common/rest_client.py b/tempest_lib/common/rest_client.py index 683efa5..aa003bd 100644 --- a/tempest_lib/common/rest_client.py +++ b/tempest_lib/common/rest_client.py @@ -423,9 +423,12 @@ class RestClient(object): else: raise exceptions.InvalidContentType(str(resp.status)) - if resp.status == 401 or resp.status == 403: + if resp.status == 401: raise exceptions.Unauthorized(resp_body) + if resp.status == 403: + raise exceptions.Forbidden(resp_body) + if resp.status == 404: raise exceptions.NotFound(resp_body) diff --git a/tempest_lib/exceptions.py b/tempest_lib/exceptions.py index d97d158..fbf9ee0 100644 --- a/tempest_lib/exceptions.py +++ b/tempest_lib/exceptions.py @@ -62,6 +62,10 @@ class Unauthorized(RestClientException): message = 'Unauthorized' +class Forbidden(RestClientException): + message = "Forbidden" + + class TimeoutException(RestClientException): message = "Request timed out" diff --git a/tempest_lib/tests/test_rest_client.py b/tempest_lib/tests/test_rest_client.py index dc819d0..b48c156 100644 --- a/tempest_lib/tests/test_rest_client.py +++ b/tempest_lib/tests/test_rest_client.py @@ -315,7 +315,7 @@ class TestRestClientErrorCheckerJSON(base.TestCase): **self.set_data("401")) def test_response_403(self): - self.assertRaises(exceptions.Unauthorized, + self.assertRaises(exceptions.Forbidden, self.rest_client._error_checker, **self.set_data("403")) From d817a030e21cc5ac2d25d13676c2f07be058681c Mon Sep 17 00:00:00 2001 From: Ken'ichi Ohmichi Date: Thu, 29 Jan 2015 01:37:32 +0000 Subject: [PATCH 09/15] Move api_version to a class value Volume v2 service clients pass api_version in __init__() now, but Iddd8306723c1ff33105f513c1993a0497a949c29 will move the passed api_version to class values for avoiding a redundant __init__() definition. For doing this, we need to move api_version to class value to avoid initializing it in RestClient __init__(). Change-Id: Ic86739dde83dcac8f68e53599967de53694f692f --- tempest_lib/common/rest_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tempest_lib/common/rest_client.py b/tempest_lib/common/rest_client.py index 683efa5..c5353ad 100644 --- a/tempest_lib/common/rest_client.py +++ b/tempest_lib/common/rest_client.py @@ -39,6 +39,9 @@ class RestClient(object): TYPE = "json" + # The version of the API this client implements + api_version = None + LOG = logging.getLogger(__name__) def __init__(self, auth_provider, service, region, @@ -54,8 +57,6 @@ class RestClient(object): self.build_timeout = build_timeout self.trace_requests = trace_requests - # The version of the API this client implements - self.api_version = None self._skip_path = False self.general_header_lc = set(('cache-control', 'connection', 'date', 'pragma', 'trailer', From 0ff7d27cc612358209fcf10fc8804b3cf650dd2f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 2 Feb 2015 18:07:11 -0500 Subject: [PATCH 10/15] Migrate the skip_because decorator from tempest This commit migrates the skip_because decorator out of tempest into tempest-lib. Since it is included as parts of existing files the migration script wasn't used to generate tempest sha1s to preserve history. Change-Id: I2c9551d3f9ca23bc9b204a652a87c6eb10bbac9a --- tempest_lib/decorators.py | 42 ++++++++++++++++++++ tempest_lib/tests/test_decorators.py | 59 ++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tempest_lib/decorators.py create mode 100644 tempest_lib/tests/test_decorators.py diff --git a/tempest_lib/decorators.py b/tempest_lib/decorators.py new file mode 100644 index 0000000..8c1d1c9 --- /dev/null +++ b/tempest_lib/decorators.py @@ -0,0 +1,42 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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 functools + +import testtools + + +def skip_because(*args, **kwargs): + """A decorator useful to skip tests hitting known bugs + + @param bug: bug number causing the test to skip + @param condition: optional condition to be True for the skip to have place + """ + def decorator(f): + @functools.wraps(f) + def wrapper(self, *func_args, **func_kwargs): + skip = False + if "condition" in kwargs: + if kwargs["condition"] is True: + skip = True + else: + skip = True + if "bug" in kwargs and skip is True: + if not kwargs['bug'].isdigit(): + raise ValueError('bug must be a valid bug number') + msg = "Skipped until Bug: %s is resolved." % kwargs["bug"] + raise testtools.TestCase.skipException(msg) + return f(self, *func_args, **func_kwargs) + return wrapper + return decorator diff --git a/tempest_lib/tests/test_decorators.py b/tempest_lib/tests/test_decorators.py new file mode 100644 index 0000000..261ffca --- /dev/null +++ b/tempest_lib/tests/test_decorators.py @@ -0,0 +1,59 @@ +# Copyright 2013 IBM Corp +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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 testtools + +from tempest_lib import base as test +from tempest_lib import decorators +from tempest_lib.tests import base + + +class TestSkipBecauseDecorator(base.TestCase): + def _test_skip_because_helper(self, expected_to_skip=True, + **decorator_args): + class TestFoo(test.BaseTestCase): + _interface = 'json' + + @decorators.skip_because(**decorator_args) + def test_bar(self): + return 0 + + t = TestFoo('test_bar') + if expected_to_skip: + self.assertRaises(testtools.TestCase.skipException, t.test_bar) + else: + # assert that test_bar returned 0 + self.assertEqual(TestFoo('test_bar').test_bar(), 0) + + def test_skip_because_bug(self): + self._test_skip_because_helper(bug='12345') + + def test_skip_because_bug_and_condition_true(self): + self._test_skip_because_helper(bug='12348', condition=True) + + def test_skip_because_bug_and_condition_false(self): + self._test_skip_because_helper(expected_to_skip=False, + bug='12349', condition=False) + + def test_skip_because_bug_without_bug_never_skips(self): + """Never skip without a bug parameter.""" + self._test_skip_because_helper(expected_to_skip=False, + condition=True) + self._test_skip_because_helper(expected_to_skip=False) + + def test_skip_because_invalid_bug_number(self): + """Raise ValueError if with an invalid bug number""" + self.assertRaises(ValueError, self._test_skip_because_helper, + bug='critical_bug') From a45b4da5e13f72ff0808738bff127e30d36c2cf1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 9 Feb 2015 14:25:47 -0500 Subject: [PATCH 11/15] Switch to using oslo.log from library This commit switches to using the oslo.log library instead of the locally synced version from oslo-incubator. Also, since Tempest-lib doesn't actually use oslo.config for anything except oslo.log now that we're using it as an external lib let's remove oslo.config from the requirements file. Change-Id: Ia4f12b747643637d93bf6873563b6c34924c5bb0 --- requirements.txt | 2 +- tempest_lib/common/rest_client.py | 2 +- tempest_lib/common/utils/misc.py | 2 +- tempest_lib/openstack/common/log.py | 713 ---------------------------- 4 files changed, 3 insertions(+), 716 deletions(-) delete mode 100644 tempest_lib/openstack/common/log.py diff --git a/requirements.txt b/requirements.txt index 8ce9a01..db29e2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,8 @@ pbr>=0.6,!=0.7,<1.0 Babel>=1.3 fixtures>=0.3.14 -oslo.config>=1.6.0 # Apache-2.0 iso8601>=0.1.9 jsonschema>=2.0.0,<3.0.0 httplib2>=0.7.5 six>=1.7.0 +oslo.log>=0.1.0 # Apache-2.0 diff --git a/tempest_lib/common/rest_client.py b/tempest_lib/common/rest_client.py index 99dbbce..e60d899 100644 --- a/tempest_lib/common/rest_client.py +++ b/tempest_lib/common/rest_client.py @@ -21,12 +21,12 @@ import re import time import jsonschema +from oslo_log import log as logging import six from tempest_lib.common import http from tempest_lib.common.utils import misc as misc_utils from tempest_lib import exceptions -from tempest_lib.openstack.common import log as logging # redrive rate limited calls at most twice MAX_RECURSION_DEPTH = 2 diff --git a/tempest_lib/common/utils/misc.py b/tempest_lib/common/utils/misc.py index 874dece..b97dd86 100644 --- a/tempest_lib/common/utils/misc.py +++ b/tempest_lib/common/utils/misc.py @@ -16,7 +16,7 @@ import inspect import re -from tempest_lib.openstack.common import log as logging +from oslo_log import log as logging LOG = logging.getLogger(__name__) diff --git a/tempest_lib/openstack/common/log.py b/tempest_lib/openstack/common/log.py deleted file mode 100644 index bafc7e0..0000000 --- a/tempest_lib/openstack/common/log.py +++ /dev/null @@ -1,713 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# 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. - -"""OpenStack logging handler. - -This module adds to logging functionality by adding the option to specify -a context object when calling the various log methods. If the context object -is not specified, default formatting is used. Additionally, an instance uuid -may be passed as part of the log message, which is intended to make it easier -for admins to find messages related to a specific instance. - -It also allows setting of formatting information through conf. - -""" - -import inspect -import itertools -import logging -import logging.config -import logging.handlers -import os -import socket -import sys -import traceback - -from oslo.config import cfg -import six -from six import moves - -_PY26 = sys.version_info[0:2] == (2, 6) - -from tempest_lib.openstack.common.gettextutils import _ -from tempest_lib.openstack.common import importutils -from tempest_lib.openstack.common import jsonutils -from tempest_lib.openstack.common import local -# NOTE(flaper87): Pls, remove when graduating this module -# from the incubator. -from tempest_lib.openstack.common.strutils import mask_password # noqa - - -_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - - -common_cli_opts = [ - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output (set logging level to ' - 'DEBUG instead of default WARNING level).'), - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output (set logging level to ' - 'INFO instead of default WARNING level).'), -] - -logging_cli_opts = [ - cfg.StrOpt('log-config-append', - metavar='PATH', - deprecated_name='log-config', - help='The name of a logging configuration file. This file ' - 'is appended to any existing logging configuration ' - 'files. For details about logging configuration files, ' - 'see the Python logging module documentation.'), - cfg.StrOpt('log-format', - metavar='FORMAT', - help='DEPRECATED. ' - 'A logging.Formatter log message format string which may ' - 'use any of the available logging.LogRecord attributes. ' - 'This option is deprecated. Please use ' - 'logging_context_format_string and ' - 'logging_default_format_string instead.'), - cfg.StrOpt('log-date-format', - default=_DEFAULT_LOG_DATE_FORMAT, - metavar='DATE_FORMAT', - help='Format string for %%(asctime)s in log records. ' - 'Default: %(default)s .'), - cfg.StrOpt('log-file', - metavar='PATH', - deprecated_name='logfile', - help='(Optional) Name of log file to output to. ' - 'If no default is set, logging will go to stdout.'), - cfg.StrOpt('log-dir', - deprecated_name='logdir', - help='(Optional) The base directory used for relative ' - '--log-file paths.'), - cfg.BoolOpt('use-syslog', - default=False, - help='Use syslog for logging. ' - 'Existing syslog format is DEPRECATED during I, ' - 'and will change in J to honor RFC5424.'), - cfg.BoolOpt('use-syslog-rfc-format', - # TODO(bogdando) remove or use True after existing - # syslog format deprecation in J - default=False, - help='(Optional) Enables or disables syslog rfc5424 format ' - 'for logging. If enabled, prefixes the MSG part of the ' - 'syslog message with APP-NAME (RFC5424). The ' - 'format without the APP-NAME is deprecated in I, ' - 'and will be removed in J.'), - cfg.StrOpt('syslog-log-facility', - default='LOG_USER', - help='Syslog facility to receive log lines.') -] - -generic_log_opts = [ - cfg.BoolOpt('use_stderr', - default=True, - help='Log output to standard error.') -] - -DEFAULT_LOG_LEVELS = ['amqp=WARN', 'amqplib=WARN', 'boto=WARN', - 'qpid=WARN', 'sqlalchemy=WARN', 'suds=INFO', - 'oslo.messaging=INFO', 'iso8601=WARN', - 'requests.packages.urllib3.connectionpool=WARN', - 'urllib3.connectionpool=WARN', 'websocket=WARN', - "keystonemiddleware=WARN", "routes.middleware=WARN", - "stevedore=WARN"] - -log_opts = [ - cfg.StrOpt('logging_context_format_string', - default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [%(request_id)s %(user_identity)s] ' - '%(instance)s%(message)s', - help='Format string to use for log messages with context.'), - cfg.StrOpt('logging_default_format_string', - default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [-] %(instance)s%(message)s', - help='Format string to use for log messages without context.'), - cfg.StrOpt('logging_debug_format_suffix', - default='%(funcName)s %(pathname)s:%(lineno)d', - help='Data to append to log format when level is DEBUG.'), - cfg.StrOpt('logging_exception_prefix', - default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s ' - '%(instance)s', - help='Prefix each line of exception output with this format.'), - cfg.ListOpt('default_log_levels', - default=DEFAULT_LOG_LEVELS, - help='List of logger=LEVEL pairs.'), - cfg.BoolOpt('publish_errors', - default=False, - help='Enables or disables publication of error events.'), - cfg.BoolOpt('fatal_deprecations', - default=False, - help='Enables or disables fatal status of deprecations.'), - - # NOTE(mikal): there are two options here because sometimes we are handed - # a full instance (and could include more information), and other times we - # are just handed a UUID for the instance. - cfg.StrOpt('instance_format', - default='[instance: %(uuid)s] ', - help='The format for an instance that is passed with the log ' - 'message.'), - cfg.StrOpt('instance_uuid_format', - default='[instance: %(uuid)s] ', - help='The format for an instance UUID that is passed with the ' - 'log message.'), -] - -CONF = cfg.CONF -CONF.register_cli_opts(common_cli_opts) -CONF.register_cli_opts(logging_cli_opts) -CONF.register_opts(generic_log_opts) -CONF.register_opts(log_opts) - -# our new audit level -# NOTE(jkoelker) Since we synthesized an audit level, make the logging -# module aware of it so it acts like other levels. -logging.AUDIT = logging.INFO + 1 -logging.addLevelName(logging.AUDIT, 'AUDIT') - - -try: - NullHandler = logging.NullHandler -except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7 - class NullHandler(logging.Handler): - def handle(self, record): - pass - - def emit(self, record): - pass - - def createLock(self): - self.lock = None - - -def _dictify_context(context): - if context is None: - return None - if not isinstance(context, dict) and getattr(context, 'to_dict', None): - context = context.to_dict() - return context - - -def _get_binary_name(): - return os.path.basename(inspect.stack()[-1][1]) - - -def _get_log_file_path(binary=None): - logfile = CONF.log_file - logdir = CONF.log_dir - - if logfile and not logdir: - return logfile - - if logfile and logdir: - return os.path.join(logdir, logfile) - - if logdir: - binary = binary or _get_binary_name() - return '%s.log' % (os.path.join(logdir, binary),) - - return None - - -class BaseLoggerAdapter(logging.LoggerAdapter): - - def audit(self, msg, *args, **kwargs): - self.log(logging.AUDIT, msg, *args, **kwargs) - - def isEnabledFor(self, level): - if _PY26: - # This method was added in python 2.7 (and it does the exact - # same logic, so we need to do the exact same logic so that - # python 2.6 has this capability as well). - return self.logger.isEnabledFor(level) - else: - return super(BaseLoggerAdapter, self).isEnabledFor(level) - - -class LazyAdapter(BaseLoggerAdapter): - def __init__(self, name='unknown', version='unknown'): - self._logger = None - self.extra = {} - self.name = name - self.version = version - - @property - def logger(self): - if not self._logger: - self._logger = getLogger(self.name, self.version) - if six.PY3: - # In Python 3, the code fails because the 'manager' attribute - # cannot be found when using a LoggerAdapter as the - # underlying logger. Work around this issue. - self._logger.manager = self._logger.logger.manager - return self._logger - - -class ContextAdapter(BaseLoggerAdapter): - warn = logging.LoggerAdapter.warning - - def __init__(self, logger, project_name, version_string): - self.logger = logger - self.project = project_name - self.version = version_string - self._deprecated_messages_sent = dict() - - @property - def handlers(self): - return self.logger.handlers - - def deprecated(self, msg, *args, **kwargs): - """Call this method when a deprecated feature is used. - - If the system is configured for fatal deprecations then the message - is logged at the 'critical' level and :class:`DeprecatedConfig` will - be raised. - - Otherwise, the message will be logged (once) at the 'warn' level. - - :raises: :class:`DeprecatedConfig` if the system is configured for - fatal deprecations. - - """ - stdmsg = _("Deprecated: %s") % msg - if CONF.fatal_deprecations: - self.critical(stdmsg, *args, **kwargs) - raise DeprecatedConfig(msg=stdmsg) - - # Using a list because a tuple with dict can't be stored in a set. - sent_args = self._deprecated_messages_sent.setdefault(msg, list()) - - if args in sent_args: - # Already logged this message, so don't log it again. - return - - sent_args.append(args) - self.warn(stdmsg, *args, **kwargs) - - def process(self, msg, kwargs): - # NOTE(jecarey): If msg is not unicode, coerce it into unicode - # before it can get to the python logging and - # possibly cause string encoding trouble - if not isinstance(msg, six.text_type): - msg = six.text_type(msg) - - if 'extra' not in kwargs: - kwargs['extra'] = {} - extra = kwargs['extra'] - - context = kwargs.pop('context', None) - if not context: - context = getattr(local.store, 'context', None) - if context: - extra.update(_dictify_context(context)) - - instance = kwargs.pop('instance', None) - instance_uuid = (extra.get('instance_uuid') or - kwargs.pop('instance_uuid', None)) - instance_extra = '' - if instance: - instance_extra = CONF.instance_format % instance - elif instance_uuid: - instance_extra = (CONF.instance_uuid_format - % {'uuid': instance_uuid}) - extra['instance'] = instance_extra - - extra.setdefault('user_identity', kwargs.pop('user_identity', None)) - - extra['project'] = self.project - extra['version'] = self.version - extra['extra'] = extra.copy() - return msg, kwargs - - -class JSONFormatter(logging.Formatter): - def __init__(self, fmt=None, datefmt=None): - # NOTE(jkoelker) we ignore the fmt argument, but its still there - # since logging.config.fileConfig passes it. - self.datefmt = datefmt - - def formatException(self, ei, strip_newlines=True): - lines = traceback.format_exception(*ei) - if strip_newlines: - lines = [moves.filter( - lambda x: x, - line.rstrip().splitlines()) for line in lines] - lines = list(itertools.chain(*lines)) - return lines - - def format(self, record): - message = {'message': record.getMessage(), - 'asctime': self.formatTime(record, self.datefmt), - 'name': record.name, - 'msg': record.msg, - 'args': record.args, - 'levelname': record.levelname, - 'levelno': record.levelno, - 'pathname': record.pathname, - 'filename': record.filename, - 'module': record.module, - 'lineno': record.lineno, - 'funcname': record.funcName, - 'created': record.created, - 'msecs': record.msecs, - 'relative_created': record.relativeCreated, - 'thread': record.thread, - 'thread_name': record.threadName, - 'process_name': record.processName, - 'process': record.process, - 'traceback': None} - - if hasattr(record, 'extra'): - message['extra'] = record.extra - - if record.exc_info: - message['traceback'] = self.formatException(record.exc_info) - - return jsonutils.dumps(message) - - -def _create_logging_excepthook(product_name): - def logging_excepthook(exc_type, value, tb): - extra = {'exc_info': (exc_type, value, tb)} - getLogger(product_name).critical( - "".join(traceback.format_exception_only(exc_type, value)), - **extra) - return logging_excepthook - - -class LogConfigError(Exception): - - message = _('Error loading logging config %(log_config)s: %(err_msg)s') - - def __init__(self, log_config, err_msg): - self.log_config = log_config - self.err_msg = err_msg - - def __str__(self): - return self.message % dict(log_config=self.log_config, - err_msg=self.err_msg) - - -def _load_log_config(log_config_append): - try: - logging.config.fileConfig(log_config_append, - disable_existing_loggers=False) - except (moves.configparser.Error, KeyError) as exc: - raise LogConfigError(log_config_append, six.text_type(exc)) - - -def setup(product_name, version='unknown'): - """Setup logging.""" - if CONF.log_config_append: - _load_log_config(CONF.log_config_append) - else: - _setup_logging_from_conf(product_name, version) - sys.excepthook = _create_logging_excepthook(product_name) - - -def set_defaults(logging_context_format_string=None, - default_log_levels=None): - # Just in case the caller is not setting the - # default_log_level. This is insurance because - # we introduced the default_log_level parameter - # later in a backwards in-compatible change - if default_log_levels is not None: - cfg.set_defaults( - log_opts, - default_log_levels=default_log_levels) - if logging_context_format_string is not None: - cfg.set_defaults( - log_opts, - logging_context_format_string=logging_context_format_string) - - -def _find_facility_from_conf(): - facility_names = logging.handlers.SysLogHandler.facility_names - facility = getattr(logging.handlers.SysLogHandler, - CONF.syslog_log_facility, - None) - - if facility is None and CONF.syslog_log_facility in facility_names: - facility = facility_names.get(CONF.syslog_log_facility) - - if facility is None: - valid_facilities = facility_names.keys() - consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON', - 'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS', - 'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP', - 'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3', - 'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7'] - valid_facilities.extend(consts) - raise TypeError(_('syslog facility must be one of: %s') % - ', '.join("'%s'" % fac - for fac in valid_facilities)) - - return facility - - -class RFCSysLogHandler(logging.handlers.SysLogHandler): - def __init__(self, *args, **kwargs): - self.binary_name = _get_binary_name() - # Do not use super() unless type(logging.handlers.SysLogHandler) - # is 'type' (Python 2.7). - # Use old style calls, if the type is 'classobj' (Python 2.6) - logging.handlers.SysLogHandler.__init__(self, *args, **kwargs) - - def format(self, record): - # Do not use super() unless type(logging.handlers.SysLogHandler) - # is 'type' (Python 2.7). - # Use old style calls, if the type is 'classobj' (Python 2.6) - msg = logging.handlers.SysLogHandler.format(self, record) - msg = self.binary_name + ' ' + msg - return msg - - -def _setup_logging_from_conf(project, version): - log_root = getLogger(None).logger - for handler in log_root.handlers: - log_root.removeHandler(handler) - - logpath = _get_log_file_path() - if logpath: - filelog = logging.handlers.WatchedFileHandler(logpath) - log_root.addHandler(filelog) - - if CONF.use_stderr: - streamlog = ColorHandler() - log_root.addHandler(streamlog) - - elif not logpath: - # pass sys.stdout as a positional argument - # python2.6 calls the argument strm, in 2.7 it's stream - streamlog = logging.StreamHandler(sys.stdout) - log_root.addHandler(streamlog) - - if CONF.publish_errors: - try: - handler = importutils.import_object( - "tempest_lib.openstack.common.log_handler.PublishErrorsHandler", - logging.ERROR) - except ImportError: - handler = importutils.import_object( - "oslo.messaging.notify.log_handler.PublishErrorsHandler", - logging.ERROR) - log_root.addHandler(handler) - - datefmt = CONF.log_date_format - for handler in log_root.handlers: - # NOTE(alaski): CONF.log_format overrides everything currently. This - # should be deprecated in favor of context aware formatting. - if CONF.log_format: - handler.setFormatter(logging.Formatter(fmt=CONF.log_format, - datefmt=datefmt)) - log_root.info('Deprecated: log_format is now deprecated and will ' - 'be removed in the next release') - else: - handler.setFormatter(ContextFormatter(project=project, - version=version, - datefmt=datefmt)) - - if CONF.debug: - log_root.setLevel(logging.DEBUG) - elif CONF.verbose: - log_root.setLevel(logging.INFO) - else: - log_root.setLevel(logging.WARNING) - - for pair in CONF.default_log_levels: - mod, _sep, level_name = pair.partition('=') - logger = logging.getLogger(mod) - # NOTE(AAzza) in python2.6 Logger.setLevel doesn't convert string name - # to integer code. - if sys.version_info < (2, 7): - level = logging.getLevelName(level_name) - logger.setLevel(level) - else: - logger.setLevel(level_name) - - if CONF.use_syslog: - try: - facility = _find_facility_from_conf() - # TODO(bogdando) use the format provided by RFCSysLogHandler - # after existing syslog format deprecation in J - if CONF.use_syslog_rfc_format: - syslog = RFCSysLogHandler(facility=facility) - else: - syslog = logging.handlers.SysLogHandler(facility=facility) - log_root.addHandler(syslog) - except socket.error: - log_root.error('Unable to add syslog handler. Verify that syslog' - 'is running.') - - -_loggers = {} - - -def getLogger(name='unknown', version='unknown'): - if name not in _loggers: - _loggers[name] = ContextAdapter(logging.getLogger(name), - name, - version) - return _loggers[name] - - -def getLazyLogger(name='unknown', version='unknown'): - """Returns lazy logger. - - Creates a pass-through logger that does not create the real logger - until it is really needed and delegates all calls to the real logger - once it is created. - """ - return LazyAdapter(name, version) - - -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.INFO): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg.rstrip()) - - -class ContextFormatter(logging.Formatter): - """A context.RequestContext aware formatter configured through flags. - - The flags used to set format strings are: logging_context_format_string - and logging_default_format_string. You can also specify - logging_debug_format_suffix to append extra formatting if the log level is - debug. - - For information about what variables are available for the formatter see: - http://docs.python.org/library/logging.html#formatter - - If available, uses the context value stored in TLS - local.store.context - - """ - - def __init__(self, *args, **kwargs): - """Initialize ContextFormatter instance - - Takes additional keyword arguments which can be used in the message - format string. - - :keyword project: project name - :type project: string - :keyword version: project version - :type version: string - - """ - - self.project = kwargs.pop('project', 'unknown') - self.version = kwargs.pop('version', 'unknown') - - logging.Formatter.__init__(self, *args, **kwargs) - - def format(self, record): - """Uses contextstring if request_id is set, otherwise default.""" - - # NOTE(jecarey): If msg is not unicode, coerce it into unicode - # before it can get to the python logging and - # possibly cause string encoding trouble - if not isinstance(record.msg, six.text_type): - record.msg = six.text_type(record.msg) - - # store project info - record.project = self.project - record.version = self.version - - # store request info - context = getattr(local.store, 'context', None) - if context: - d = _dictify_context(context) - for k, v in d.items(): - setattr(record, k, v) - - # NOTE(sdague): default the fancier formatting params - # to an empty string so we don't throw an exception if - # they get used - for key in ('instance', 'color', 'user_identity'): - if key not in record.__dict__: - record.__dict__[key] = '' - - if record.__dict__.get('request_id'): - fmt = CONF.logging_context_format_string - else: - fmt = CONF.logging_default_format_string - - if (record.levelno == logging.DEBUG and - CONF.logging_debug_format_suffix): - fmt += " " + CONF.logging_debug_format_suffix - - if sys.version_info < (3, 2): - self._fmt = fmt - else: - self._style = logging.PercentStyle(fmt) - self._fmt = self._style._fmt - # Cache this on the record, Logger will respect our formatted copy - if record.exc_info: - record.exc_text = self.formatException(record.exc_info, record) - return logging.Formatter.format(self, record) - - def formatException(self, exc_info, record=None): - """Format exception output with CONF.logging_exception_prefix.""" - if not record: - return logging.Formatter.formatException(self, exc_info) - - stringbuffer = moves.StringIO() - traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], - None, stringbuffer) - lines = stringbuffer.getvalue().split('\n') - stringbuffer.close() - - if CONF.logging_exception_prefix.find('%(asctime)') != -1: - record.asctime = self.formatTime(record, self.datefmt) - - formatted_lines = [] - for line in lines: - pl = CONF.logging_exception_prefix % record.__dict__ - fl = '%s%s' % (pl, line) - formatted_lines.append(fl) - return '\n'.join(formatted_lines) - - -class ColorHandler(logging.StreamHandler): - LEVEL_COLORS = { - logging.DEBUG: '\033[00;32m', # GREEN - logging.INFO: '\033[00;36m', # CYAN - logging.AUDIT: '\033[01;36m', # BOLD CYAN - logging.WARN: '\033[01;33m', # BOLD YELLOW - logging.ERROR: '\033[01;31m', # BOLD RED - logging.CRITICAL: '\033[01;31m', # BOLD RED - } - - def format(self, record): - record.color = self.LEVEL_COLORS[record.levelno] - return logging.StreamHandler.format(self, record) - - -class DeprecatedConfig(Exception): - message = _("Fatal call to deprecated config: %(msg)s") - - def __init__(self, msg): - super(Exception, self).__init__(self.message % dict(msg=msg)) From 21e3f6a29a368840b66fe6fccd60abd431bbc727 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 11 Feb 2015 11:16:42 -0500 Subject: [PATCH 12/15] Enable stdout passthrough for subunit-trace One of the biggest issues with subunit-trace is when non-subunit data is passed into it gets dropped by subunit trace. This patch is an attempt (based on a similar patch to subunit-2to1) to allow non subunit data existing before the subunit stream to be printed on stdout. After the stream is detected however any other stdin unrelated content will still be lost. Closes-Bug: #1420067 Change-Id: I94c428120892b987c372473ac2128d73e9ba8f2d --- tempest_lib/cmd/subunit_trace.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tempest_lib/cmd/subunit_trace.py b/tempest_lib/cmd/subunit_trace.py index 5ffaa7c..eb78610 100755 --- a/tempest_lib/cmd/subunit_trace.py +++ b/tempest_lib/cmd/subunit_trace.py @@ -251,6 +251,9 @@ def main(): failonly=args.failonly)) summary = testtools.StreamSummary() result = testtools.CopyStreamResult([outcomes, summary]) + result = testtools.StreamResultRouter(result) + cat = subunit.test_results.CatFiles(sys.stdout) + result.add_rule(cat, 'test_id', test_id=None) start_time = datetime.datetime.utcnow() result.startTestRun() try: From 292989688a2281f5f8dc6d5b8fb216ba712ea80d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 11 Feb 2015 18:17:58 -0500 Subject: [PATCH 13/15] Improve the library's documentation This commit is the first step in improving the library's documentation. It adds missing details to a lot of the default cookiecutter docs which have mostly sat untouched since the repo was created. The release notes are also brought up to date and adds autodoc pages for all the modules with public functions. Change-Id: Id249c95fb269d07628952a4182675bce1fc18a53 --- README.rst | 14 +++++++++++++- doc/source/cli.rst | 5 +++++ doc/source/decorators.rst | 13 +++++++++++++ doc/source/index.rst | 33 +++++++++++++++++++++++++++++++++ doc/source/rest_client.rst | 11 +++++++++++ doc/source/usage.rst | 19 ++++++++++++++++++- doc/source/utils.rst | 11 +++++++++++ 7 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 doc/source/decorators.rst create mode 100644 doc/source/rest_client.rst create mode 100644 doc/source/utils.rst diff --git a/README.rst b/README.rst index b28f368..1878b27 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,19 @@ OpenStack Functional Testing Library * Source: http://git.openstack.org/cgit/openstack/tempest-lib * Bugs: http://bugs.launchpad.net/tempest +tempest-lib is a library of common functionality that was originally in tempest +(or similar in scope to tempest) + Features -------- -* TODO +Some of the current functionality exposed from the library includes: + +* OpenStack python-* client CLI testing framework +* subunit-trace: A output filter for subunit streams. Useful in conjunction + with calling a test runner that emits subunit +* A unified REST Client +* Utility functions: + * skip_because: Skip a test because of a bug + * find_test_caller: Perform stack introspection to find the test caller. + common methods diff --git a/doc/source/cli.rst b/doc/source/cli.rst index 33e0110..301510e 100644 --- a/doc/source/cli.rst +++ b/doc/source/cli.rst @@ -1,3 +1,8 @@ +.. _cli: + +CLI Testing Framework Usage +=========================== + ------------------- The cli.base module ------------------- diff --git a/doc/source/decorators.rst b/doc/source/decorators.rst new file mode 100644 index 0000000..a0b7c78 --- /dev/null +++ b/doc/source/decorators.rst @@ -0,0 +1,13 @@ +.. _decorators: + +Decorators Usage Guide +====================== + +--------------------- +The decorators module +--------------------- + +.. automodule:: tempest_lib.decorators + :members: + + diff --git a/doc/source/index.rst b/doc/source/index.rst index d2a94bd..9c707ca 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,10 +16,43 @@ Contents: usage contributing cli + decorators Release Notes ============= +0.2.1 +----- + * Fix subunit-trace to enable stdout passthrough + +0.2.0 +----- + * Adds the skip_because decorator which was migrated from tempest + * Fixes to rest_client + * Separates the forbid + * Cleans up the exception classes to make inheritance simpler + * Doc typo fixes + +0.1.0 +----- + * Adds the RestClient class which was migrated from tempest + * Fix subunit-trace to handle when there isn't a worker tag in the subunit + stream + +0.0.4 +----- + * Fix subunit-trace when running with python < 2.7 + +0.0.3 +----- + * subunit-trace bug fixes: + * Switch to using elapsed time for the summary view + * Addition of --failonly option from nova's forked subunit-trace + +0.0.2 +----- + * Fix the MRO ordering in the base test class + 0.0.1 ----- * Adds cli testing framework diff --git a/doc/source/rest_client.rst b/doc/source/rest_client.rst new file mode 100644 index 0000000..513d8e4 --- /dev/null +++ b/doc/source/rest_client.rst @@ -0,0 +1,11 @@ +.. _rest_client: + +Rest Client Usage +================= + +---------------------- +The rest_client module +---------------------- + +.. automodule:: tempest_lib.common.rest_client + :members: diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 4aeb40f..e305244 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -4,4 +4,21 @@ Usage To use tempest-lib in a project:: - import tempest_lib \ No newline at end of file + import tempest_lib + +:ref:`cli` +---------- +The CLI testing framework allows you to test the command line interface for +an OpenStack project's python-*client + + +:ref:`decorators` +----------------- +These decorators enable common utility functions inside of your test suite + + +:ref:`rest_client` +------------------ +The base building block for making a project specific client + + diff --git a/doc/source/utils.rst b/doc/source/utils.rst new file mode 100644 index 0000000..0e481b2 --- /dev/null +++ b/doc/source/utils.rst @@ -0,0 +1,11 @@ +.. _utils: + +Utils Usage +=========== + +--------------- +The misc module +--------------- + +.. automodule:: tempest_lib.common.utils.misc + :members: From e29ec710eff862c0be03755e87b25ddb048d6d2a Mon Sep 17 00:00:00 2001 From: Chris Dent Date: Wed, 11 Feb 2015 14:39:10 +0000 Subject: [PATCH 14/15] Summarize expected failures The incoming stream has the xfail data but count_tests was not matching on the xfail status. Instead it was matching on the fragment 'fail' meaning that expected failures were counted as such. By bounding the regular expressions it is possible to get more specific results will still leaving the count_tests method flexible for other users where fragments would be useful. Counts of 'uxsuccess' are also summarized as "Unexpected Success". It's unclear how to effectively automate testing of this. Manual testing returns the expected results. Change-Id: I5b1458f9a98712ea3e424d2c9610b915055138af --- tempest_lib/cmd/subunit_trace.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tempest_lib/cmd/subunit_trace.py b/tempest_lib/cmd/subunit_trace.py index 5ffaa7c..49e2a04 100755 --- a/tempest_lib/cmd/subunit_trace.py +++ b/tempest_lib/cmd/subunit_trace.py @@ -205,9 +205,12 @@ def print_summary(stream, elapsed_time): stream.write("\n======\nTotals\n======\n") stream.write("Ran: %s tests in %.4f sec.\n" % ( count_tests('status', '.*'), total_seconds(elapsed_time))) - stream.write(" - Passed: %s\n" % count_tests('status', 'success')) - stream.write(" - Skipped: %s\n" % count_tests('status', 'skip')) - stream.write(" - Failed: %s\n" % count_tests('status', 'fail')) + stream.write(" - Passed: %s\n" % count_tests('status', '^success$')) + stream.write(" - Skipped: %s\n" % count_tests('status', '^skip$')) + stream.write(" - Expected Fail: %s\n" % count_tests('status', '^xfail$')) + stream.write(" - Unexpected Success: %s\n" % count_tests('status', + '^uxsuccess$')) + stream.write(" - Failed: %s\n" % count_tests('status', '^fail$')) stream.write("Sum of execute time for each test: %.4f sec.\n" % run_time()) # we could have no results, especially as we filter out the process-codes From bd6d154f14e8ecabc5d7751163eec42bbfa22e59 Mon Sep 17 00:00:00 2001 From: Andrey Pavlov Date: Sat, 7 Feb 2015 20:57:43 +0300 Subject: [PATCH 15/15] Move data_utils functionaly from tempest. This commit migrates all data_utils with tests and the misc utils unit tests from the tempest repo. This includes 3 files from tempest tempest/common/utils/data_utils.py, tempest/tests/common/test_data_utils.py, and tempest/tests/common/test_misc.py. This includes tempest commits: 2d88e49 Merge "DHCPv6 network tests" 41fa16d Hacking rule to forbid resource unsafe fixtures bfa86c6 Merge "Use random binary data for test images" 5c3b6fe Use random binary data for test images 165a743 Merge "Refactor random url generation into its own method" 064e965 Refactor random url generation into its own method 9f921d6 Merge "Refactor _find_caller into a public test finder utility" 7efa5c3 Refactor _find_caller into a public test finder utility f1794eb Merge "Add utils.misc unit tests" 4f46805 Add utils.misc unit tests 1fcf139 Merge "Add unit test for data_utils" 30f07b3 Merge "Remove unused build_url function in data_utils" ce7c696 Add unit test for data_utils c0a1e5c Remove unused build_url function in data_utils fea1a7e Merge "tighten up isolated creds create" 6969b90 tighten up isolated creds create 9051bb5 Merge "Remove vim headers" e8d31a0 Remove vim headers e40967e Merge "API tests for Ironic" 62b1ed1 API tests for Ironic 5331151 Merge "Test image member is enforced" a709b76 Test image member is enforced 4a2431d Inject "-tempest-" string to rand_name 39f9722 Replace OpenStack LLC with OpenStack Foundation bb7ce44 Merge "Reduce chance of name collision for resources." 88d4f7c Reduce chance of name collision for resources. d65aec0 Merge "Fix flavors tests so they can be run in parallel" 8abacf3 Fix flavors tests so they can be run in parallel fc9e333 Fix PEP8 compliance problems 59889b7 Merge "Fix T401 and T402 errors" f237ccb Fix T401 and T402 errors 34afe48 Merge "Fix import order to comply with import ordering rules." a83a16e Fix import order to comply with import ordering rules. 4ef897c Merge "Fix and simplify arbitrary_string. lp#1085048" 47737d8 Fix and simplify arbitrary_string. lp#1085048 7ccda8c Simplify parse_image_id. d246eb4 Merge "Initial add of Swift tests" 5d73443 Initial add of Swift tests a5feec9 Merge "make the rand_name value shorter" 6ec6fc2 make the rand_name value shorter aeddf63 Moved parse_image_id() to data_utils 3d9da9b Merge "Addresses lp#942382 - refactor configuration for clarity" 587385b Addresses lp#942382 - refactor configuration for clarity 25dd196 Merge "Fixed issue with white space after pep8 review Code... 7fb1efa Fixed issue with white space after pep8 review Code review... ed8bef3 Changes the namespace from storm to tempest, as well as ... e1b050d * Added build_url() utility that returns an endpoint URL based... cb5d954 Removed unnecessary 'self' reference 1465d61 Initial import of tests from the Zodiac project. On suggestion... to see the commit history for these files refer to the above sha1s in the tempest repository Change-Id: Idecf25bef6eeffb8b06387ac95d9060edb58be46 --- tempest_lib/common/utils/data_utils.py | 99 +++++++++++++++++++ tempest_lib/tests/common/__init__.py | 0 tempest_lib/tests/common/utils/__init__.py | 0 .../tests/common/utils/test_data_utils.py | 77 +++++++++++++++ tempest_lib/tests/common/utils/test_misc.py | 88 +++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 tempest_lib/common/utils/data_utils.py create mode 100644 tempest_lib/tests/common/__init__.py create mode 100644 tempest_lib/tests/common/utils/__init__.py create mode 100644 tempest_lib/tests/common/utils/test_data_utils.py create mode 100644 tempest_lib/tests/common/utils/test_misc.py diff --git a/tempest_lib/common/utils/data_utils.py b/tempest_lib/common/utils/data_utils.py new file mode 100644 index 0000000..eec2474 --- /dev/null +++ b/tempest_lib/common/utils/data_utils.py @@ -0,0 +1,99 @@ +# Copyright 2012 OpenStack Foundation +# 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 itertools +import netaddr +import random +import uuid + + +def rand_uuid(): + return str(uuid.uuid4()) + + +def rand_uuid_hex(): + return uuid.uuid4().hex + + +def rand_name(name=''): + randbits = str(random.randint(1, 0x7fffffff)) + if name: + return name + '-' + randbits + else: + return randbits + + +def rand_url(): + randbits = str(random.randint(1, 0x7fffffff)) + return 'https://url-' + randbits + '.com' + + +def rand_int_id(start=0, end=0x7fffffff): + return random.randint(start, end) + + +def rand_mac_address(): + """Generate an Ethernet MAC address.""" + # NOTE(vish): We would prefer to use 0xfe here to ensure that linux + # bridge mac addresses don't change, but it appears to + # conflict with libvirt, so we use the next highest octet + # that has the unicast and locally administered bits set + # properly: 0xfa. + # Discussion: https://bugs.launchpad.net/nova/+bug/921838 + mac = [0xfa, 0x16, 0x3e, + random.randint(0x00, 0xff), + random.randint(0x00, 0xff), + random.randint(0x00, 0xff)] + return ':'.join(["%02x" % x for x in mac]) + + +def parse_image_id(image_ref): + """Return the image id from a given image ref.""" + return image_ref.rsplit('/')[-1] + + +def arbitrary_string(size=4, base_text=None): + """Return size characters from base_text + + Repeating the base_text infinitely if needed. + """ + if not base_text: + base_text = 'test' + return ''.join(itertools.islice(itertools.cycle(base_text), size)) + + +def random_bytes(size=1024): + """Return size randomly selected bytes as a string.""" + return ''.join([chr(random.randint(0, 255)) + for i in range(size)]) + + +def get_ipv6_addr_by_EUI64(cidr, mac): + # Check if the prefix is IPv4 address + is_ipv4 = netaddr.valid_ipv4(cidr) + if is_ipv4: + msg = "Unable to generate IP address by EUI64 for IPv4 prefix" + raise TypeError(msg) + try: + eui64 = int(netaddr.EUI(mac).eui64()) + prefix = netaddr.IPNetwork(cidr) + return netaddr.IPAddress(prefix.first + eui64 ^ (1 << 57)) + except (ValueError, netaddr.AddrFormatError): + raise TypeError('Bad prefix or mac format for generating IPv6 ' + 'address by EUI-64: %(prefix)s, %(mac)s:' + % {'prefix': cidr, 'mac': mac}) + except TypeError: + raise TypeError('Bad prefix type for generate IPv6 address by ' + 'EUI-64: %s' % cidr) diff --git a/tempest_lib/tests/common/__init__.py b/tempest_lib/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_lib/tests/common/utils/__init__.py b/tempest_lib/tests/common/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tempest_lib/tests/common/utils/test_data_utils.py b/tempest_lib/tests/common/utils/test_data_utils.py new file mode 100644 index 0000000..76401cb --- /dev/null +++ b/tempest_lib/tests/common/utils/test_data_utils.py @@ -0,0 +1,77 @@ +# Copyright 2014 NEC Corporation. +# 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. + + +from tempest_lib.common.utils import data_utils +from tempest_lib.tests import base + + +class TestDataUtils(base.TestCase): + + def test_rand_uuid(self): + actual = data_utils.rand_uuid() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]" + "{4}-[0-9a-f]{4}-[0-9a-f]{12}$") + actual2 = data_utils.rand_uuid() + self.assertNotEqual(actual, actual2) + + def test_rand_uuid_hex(self): + actual = data_utils.rand_uuid_hex() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^[0-9a-f]{32}$") + + actual2 = data_utils.rand_uuid_hex() + self.assertNotEqual(actual, actual2) + + def test_rand_name(self): + actual = data_utils.rand_name() + self.assertIsInstance(actual, str) + actual2 = data_utils.rand_name() + self.assertNotEqual(actual, actual2) + + actual = data_utils.rand_name('foo') + self.assertTrue(actual.startswith('foo')) + actual2 = data_utils.rand_name('foo') + self.assertTrue(actual.startswith('foo')) + self.assertNotEqual(actual, actual2) + + def test_rand_int(self): + actual = data_utils.rand_int_id() + self.assertIsInstance(actual, int) + + actual2 = data_utils.rand_int_id() + self.assertNotEqual(actual, actual2) + + def test_rand_mac_address(self): + actual = data_utils.rand_mac_address() + self.assertIsInstance(actual, str) + self.assertRegexpMatches(actual, "^([0-9a-f][0-9a-f]:){5}" + "[0-9a-f][0-9a-f]$") + + actual2 = data_utils.rand_mac_address() + self.assertNotEqual(actual, actual2) + + def test_parse_image_id(self): + actual = data_utils.parse_image_id("/foo/bar/deadbeaf") + self.assertEqual("deadbeaf", actual) + + def test_arbitrary_string(self): + actual = data_utils.arbitrary_string() + self.assertEqual(actual, "test") + actual = data_utils.arbitrary_string(size=30, base_text="abc") + self.assertEqual(actual, "abc" * int(30 / len("abc"))) + actual = data_utils.arbitrary_string(size=5, base_text="deadbeaf") + self.assertEqual(actual, "deadb") diff --git a/tempest_lib/tests/common/utils/test_misc.py b/tempest_lib/tests/common/utils/test_misc.py new file mode 100644 index 0000000..aefaeef --- /dev/null +++ b/tempest_lib/tests/common/utils/test_misc.py @@ -0,0 +1,88 @@ +# Copyright 2014 NEC Corporation. +# 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. + + +from tempest_lib.common.utils import misc +from tempest_lib.tests import base + + +@misc.singleton +class TestFoo(object): + + count = 0 + + def increment(self): + self.count += 1 + return self.count + + +@misc.singleton +class TestBar(object): + + count = 0 + + def increment(self): + self.count += 1 + return self.count + + +class TestMisc(base.TestCase): + + def test_singleton(self): + test = TestFoo() + self.assertEqual(0, test.count) + self.assertEqual(1, test.increment()) + test2 = TestFoo() + self.assertEqual(1, test.count) + self.assertEqual(1, test2.count) + self.assertEqual(test, test2) + test3 = TestBar() + self.assertNotEqual(test, test3) + + def test_find_test_caller_test_case(self): + # Calling it from here should give us the method we're in. + self.assertEqual('TestMisc:test_find_test_caller_test_case', + misc.find_test_caller()) + + def test_find_test_caller_setup_self(self): + def setUp(self): + return misc.find_test_caller() + self.assertEqual('TestMisc:setUp', setUp(self)) + + def test_find_test_caller_setup_no_self(self): + def setUp(): + return misc.find_test_caller() + self.assertEqual(':setUp', setUp()) + + def test_find_test_caller_setupclass_cls(self): + def setUpClass(cls): # noqa + return misc.find_test_caller() + self.assertEqual('TestMisc:setUpClass', setUpClass(self.__class__)) + + def test_find_test_caller_teardown_self(self): + def tearDown(self): + return misc.find_test_caller() + self.assertEqual('TestMisc:tearDown', tearDown(self)) + + def test_find_test_caller_teardown_no_self(self): + def tearDown(): + return misc.find_test_caller() + self.assertEqual(':tearDown', tearDown()) + + def test_find_test_caller_teardown_class(self): + def tearDownClass(cls): # noqa + return misc.find_test_caller() + self.assertEqual('TestMisc:tearDownClass', + tearDownClass(self.__class__))