Merge tag '0.3.0' into debian/kilo

Release 0.3.0

This tempest-lib release includes:
 * Switch to using oslo.log instead of the oslo-incubator logging module
 * Documentation improvements
 * Handling of additional test result conditions in subunit-trace
 * Addition of the data_utils module from tempest
This commit is contained in:
Thomas Goirand
2015-03-05 17:42:55 +01:00
23 changed files with 539 additions and 826 deletions

View File

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

View File

@@ -1,3 +1,8 @@
.. _cli:
CLI Testing Framework Usage
===========================
-------------------
The cli.base module
-------------------

13
doc/source/decorators.rst Normal file
View File

@@ -0,0 +1,13 @@
.. _decorators:
Decorators Usage Guide
======================
---------------------
The decorators module
---------------------
.. automodule:: tempest_lib.decorators
:members:

View File

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

View File

@@ -0,0 +1,11 @@
.. _rest_client:
Rest Client Usage
=================
----------------------
The rest_client module
----------------------
.. automodule:: tempest_lib.common.rest_client
:members:

View File

@@ -5,3 +5,20 @@ Usage
To use tempest-lib in a project::
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

11
doc/source/utils.rst Normal file
View File

@@ -0,0 +1,11 @@
.. _utils:
Utils Usage
===========
---------------
The misc module
---------------
.. automodule:: tempest_lib.common.utils.misc
:members:

View File

@@ -4,8 +4,8 @@
pbr>=0.6,!=0.7,<1.0
Babel>=1.3
fixtures>=0.3.14
oslo.config>=1.4.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

View File

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

View File

@@ -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
@@ -251,6 +254,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:

View File

@@ -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
@@ -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',
@@ -423,9 +424,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)

View File

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

View File

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

42
tempest_lib/decorators.py Normal file
View File

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

View File

@@ -50,18 +50,6 @@ class RestClientException(TempestException,
pass
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 +62,14 @@ class Unauthorized(RestClientException):
message = 'Unauthorized'
class InvalidServiceTag(TempestException):
message = "Invalid service tag"
class Forbidden(RestClientException):
message = "Forbidden"
class TimeoutException(TempestException):
class TimeoutException(RestClientException):
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,43 +94,16 @@ 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):
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")

View File

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

View File

View File

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

View File

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

View File

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

View File

@@ -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})
@@ -310,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"))
@@ -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):

View File

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