opencafe/cafe/drivers/unittest/fixtures.py

218 lines
8.2 KiB
Python

# Copyright 2015 Rackspace
# 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.
"""
@summary: Base Classes for Test Fixtures
@note: Corresponds DIRECTLY TO A unittest.TestCase
@see: http://docs.python.org/library/unittest.html#unittest.TestCase
"""
import os
import re
import six
import sys
import unittest
from cafe.drivers.base import FixtureReporter
class BaseTestFixture(unittest.TestCase):
"""
@summary: This should be used as the base class for any unittest tests,
meant to be used instead of unittest.TestCase.
@see: http://docs.python.org/library/unittest.html#unittest.TestCase
"""
__test__ = True
def shortDescription(self):
"""
@summary: Returns a formatted description of the test
"""
short_desc = None
if os.environ.get("VERBOSE", None) == "true" and self._testMethodDoc:
temp = self._testMethodDoc.strip("\n")
short_desc = re.sub(r"[ ]{2,}", "", temp).strip("\n")
return short_desc
def logDescription(self):
"""
@summary: Returns a formatted description from the _testMethodDoc
"""
log_desc = None
if self._testMethodDoc:
log_desc = "\n{0}".format(
re.sub(r"[ ]{2,}", "", self._testMethodDoc).strip("\n"))
return log_desc
@classmethod
def assertClassSetupFailure(cls, message):
"""
@summary: Use this if you need to fail from a Test Fixture's
setUpClass() method
"""
cls.fixture_log.error("FATAL: %s:%s", cls.__name__, message)
raise AssertionError("FATAL: %s:%s" % (cls.__name__, message))
@classmethod
def assertClassTeardownFailure(cls, message):
"""
@summary: Use this if you need to fail from a Test Fixture's
tearUpClass() method
"""
cls.fixture_log.error("FATAL: %s:%s", cls.__name__, message)
raise AssertionError("FATAL: %s:%s" % (cls.__name__, message))
@classmethod
def setUpClass(cls):
"""@summary: Adds logging/reporting to Unittest setUpClass"""
super(BaseTestFixture, cls).setUpClass()
cls._reporter = FixtureReporter(cls)
cls.fixture_log = cls._reporter.logger.log
cls._reporter.start()
cls._class_cleanup_tasks = []
@classmethod
def tearDownClass(cls):
"""@summary: Adds stop reporting to Unittest setUpClass"""
cls._reporter.stop()
# Call super teardown after to avoid tearing down the class before we
# can run our own tear down stuff.
super(BaseTestFixture, cls).tearDownClass()
def setUp(self):
"""@summary: Logs test metrics"""
self.shortDescription()
self._reporter.start_test_metrics(
self.__class__.__name__, self._testMethodName,
self.logDescription())
self._duration = 0.00
super(BaseTestFixture, self).setUp()
def tearDown(self):
"""
@todo: This MUST be upgraded this from resultForDoCleanups into a
better pattern or working with the result object directly.
This is related to the todo in L{TestRunMetrics}
"""
if sys.version_info < (3, 4):
if six.PY2:
report = self._resultForDoCleanups
else:
report = self._outcomeForDoCleanups
if any(r for r in report.failures
if self._test_name_matches_result(self._testMethodName, r)):
self._reporter.stop_test_metrics(self._testMethodName,
'Failed')
elif any(r for r in report.errors
if self._test_name_matches_result(self._testMethodName,
r)):
self._reporter.stop_test_metrics(self._testMethodName,
'ERRORED')
else:
self._reporter.stop_test_metrics(self._testMethodName,
'Passed')
try:
self._duration = \
self._reporter.test_metrics.timer.get_elapsed_time()
except AttributeError:
# If the reporter was not appropriately called at test start
# or end tests will fail unless we catch this. This is common
# in the case where test writers did not appropriately call
# 'super' in the setUp or setUpClass of their fixture or
# test class.
self._duration = float('nan')
else:
for method, _ in self._outcome.errors:
if self._test_name_matches_result(self._testMethodName,
method):
self._reporter.stop_test_metrics(self._testMethodName,
'Failed')
else:
self._reporter.stop_test_metrics(self._testMethodName,
'Passed')
self._duration = \
self._reporter.test_metrics.timer.get_elapsed_time()
# Continue inherited tearDown()
super(BaseTestFixture, self).tearDown()
@staticmethod
def _test_name_matches_result(name, test_result):
"""@summary: Checks if a test result matches a specific test name."""
if sys.version_info < (3, 4):
# Try to get the result portion of the tuple
try:
result = test_result[0]
except IndexError:
return False
else:
result = test_result
# Verify the object has the correct property
if hasattr(result, '_testMethodName'):
return result._testMethodName == name
else:
return False
@classmethod
def _do_class_cleanup_tasks(cls):
"""@summary: Runs class cleanup tasks added during testing"""
for func, args, kwargs in reversed(cls._class_cleanup_tasks):
cls.fixture_log.debug(
"Running class cleanup task: %s(%s, %s)",
func.__name__,
", ".join([str(arg) for arg in args]),
", ".join(["{0}={1}".format(
str(k), str(kwargs[k])) for k in kwargs]))
try:
func(*args, **kwargs)
except Exception as exception:
# Pretty prints method signature in the following format:
# "classTearDown failure: Unable to execute FnName(a, b, c=42)"
cls.fixture_log.exception(exception)
cls.fixture_log.error(
"classTearDown failure: Exception occured while trying to"
" execute class teardown task: %s(%s, %s)",
func.__name__,
", ".join([str(arg) for arg in args]),
", ".join(["{0}={1}".format(
str(k), str(kwargs[k])) for k in kwargs]))
@classmethod
def addClassCleanup(cls, function, *args, **kwargs):
"""@summary: Named to match unittest's addCleanup.
ClassCleanup tasks run if setUpClass fails, or after tearDownClass.
(They don't depend on tearDownClass running)
"""
cls._class_cleanup_tasks.append((function, args or [], kwargs or {}))
class BaseBurnInTestFixture(BaseTestFixture):
"""
@summary: Base test fixture that allows for Burn-In tests
"""
@classmethod
def setUpClass(cls):
"""@summary: inits burning testing variables"""
super(BaseBurnInTestFixture, cls).setUpClass()
cls.test_list = []
cls.iterations = 0
@classmethod
def addTest(cls, test_case):
"""@summary: Adds a test case"""
cls.test_list.append(test_case)