Claudiu Popa 0b40f6368b Provide the error code in the Windows exception messages
This patch adds a type of exception for the Windows code, which interpolates
the given exception message with the last Windows API error, retrieved with
ctypes.GetLastError. This is useful in the case where we have only the log
at our disposal for debugging and some API method failed with reasons unknown.
Since we can't replicate what the user does everytime, having some additional
clue why an API failed could improve our bug detection workflow.

Change-Id: I364324ad5a8529b5363be3a7c6dc03ca52eb637c
2015-03-16 16:51:40 +02:00

201 lines
6.4 KiB
Python

# Copyright 2014 Cloudbase Solutions Srl
#
# 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 contextlib
import functools
import logging as base_logging
import os
import shutil
import tempfile
import unittest
try:
import unittest.mock as mock
except ImportError:
import mock
from oslo.config import cfg
from cloudbaseinit import exception
from cloudbaseinit.openstack.common import log as logging
CONF = cfg.CONF
__all__ = (
'create_tempfile',
'create_tempdir',
'LogSnatcher',
'CloudbaseInitTestBase',
'ConfPatcher',
)
@contextlib.contextmanager
def create_tempdir():
"""Create a temporary directory.
This is a context manager, which creates a new temporary
directory and removes it when exiting from the context manager
block.
"""
tempdir = tempfile.mkdtemp(prefix="cloudbaseinit-tests")
try:
yield tempdir
finally:
shutil.rmtree(tempdir)
@contextlib.contextmanager
def create_tempfile(content=None):
"""Create a temporary file.
This is a context manager, which uses `create_tempdir` to obtain a
temporary directory, where the file will be placed.
:param content:
Additionally, a string which will be written
in the new file.
"""
with create_tempdir() as temp:
fd, path = tempfile.mkstemp(dir=temp)
os.close(fd)
if content:
with open(path, 'w') as stream:
stream.write(content)
yield path
# This is similar with unittest.TestCase.assertLogs from Python 3.4.
class SnatchHandler(base_logging.Handler):
def __init__(self, *args, **kwargs):
super(SnatchHandler, self).__init__(*args, **kwargs)
self.output = []
def emit(self, record):
msg = self.format(record)
self.output.append(msg)
class LogSnatcher(object):
"""A context manager to capture emitted logged messages.
The class can be used as following::
with LogSnatcher('plugins.windows.createuser') as snatcher:
LOG.info("doing stuff")
LOG.info("doing stuff %s", 1)
LOG.warn("doing other stuff")
...
self.assertEqual(snatcher.output,
['INFO:unknown:doing stuff',
'INFO:unknown:doing stuff 1',
'WARN:unknown:doing other stuff'])
"""
@property
def output(self):
return self._snatch_handler.output
def __init__(self, logger_name):
self._logger_name = logger_name
self._snatch_handler = SnatchHandler()
self._logger = logging.getLogger(self._logger_name)
self._previous_level = self._logger.logger.getEffectiveLevel()
def __enter__(self):
self._logger.logger.setLevel(base_logging.DEBUG)
self._logger.handlers.append(self._snatch_handler)
return self
def __exit__(self, *args):
self._logger.handlers.remove(self._snatch_handler)
self._logger.logger.setLevel(self._previous_level)
class ConfPatcher(object):
"""Override the configuration for the given key, with the given value.
This class can be used both as a context manager and as a decorator.
"""
# TODO(cpopa): mock.patch.dict would have been a better solution
# but oslo.config.cfg doesn't support item
# assignment.
def __init__(self, key, value, conf=CONF):
self._original_value = conf.get(key)
self._key = key
self._value = value
self._conf = conf
def __call__(self, func, *args, **kwargs):
def _wrapped_f(*args, **kwargs):
with self:
return func(*args, **kwargs)
functools.update_wrapper(_wrapped_f, func)
return _wrapped_f
def __enter__(self):
self._conf.set_override(self._key, self._value)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._conf.set_override(self._key, self._original_value)
class CloudbaseInitTestBase(unittest.TestCase):
"""A test base class, which provides a couple of useful methods."""
@contextlib.contextmanager
def assert_raises_windows_message(
self, expected_msg, error_code,
exc=exception.WindowsCloudbaseInitException):
"""Helper method for testing raised error messages
This assert method is similar to :meth:`~assertRaises`, but
it can only be used as a context manager. It will check that the
block of the with statement raises an exception of type :class:`exc`,
having as message the result of the interpolation between
`expected_msg` and a formatted string, obtained through
`ctypes.FormatError(error_code)`.
"""
# Can't use the decorator form, since it will not be properly set
# after the function passes control with the `yield` (so the
# with statement block will have the original value, not the
# mocked one).
with self.assertRaises(exc) as cm:
with mock.patch('cloudbaseinit.exception.'
'ctypes.FormatError',
create=True) as mock_format_error:
with mock.patch('cloudbaseinit.exception.ctypes.'
'GetLastError',
create=True) as mock_get_last_error:
mock_format_error.return_value = "description"
yield
if mock_get_last_error.called:
# This can be called when the error code is not given,
# but we don't have control over that, so test that
# it's actually called only once.
mock_get_last_error.assert_called_once_with()
mock_format_error.assert_called_once_with(
mock_get_last_error.return_value)
else:
mock_format_error.assert_called_once_with(error_code)
expected_msg = expected_msg % mock_format_error.return_value
self.assertEqual(expected_msg, cm.exception.args[0])