# Copyright (c) Fraunhofer MEVIS, Germany. All rights reserved.
# **InsertLicense** code author="Lennart Tautz"
import functools
import inspect
import sys
from TestSupport.TestHelper import CancelTestException
from fmeTestSupport.TestCenterTestCase import TestCenterTestCaseBase
import TestSupport.Logging as Logging
def _isNameOfTestFunction(name):
return name.startswith("TEST") or name.startswith("DISABLED_TEST")
def _isNameOfIterativeTestFunction(name):
return name.startswith("ITERATIVETEST")
def _isBoundMethodOf(fct, testCaseClass):
isBoundTo = None
if sys.version_info.major >= 3:
isBoundTo = isinstance(fct.__self__, testCaseClass)
else:
isBoundTo = isinstance(fct.im_self, testCaseClass)
return isBoundTo
def _callFunction(fct, *args, **kwargs):
if sys.version_info.major >= 3:
fct.__func__(*args, **kwargs)
else:
fct.im_func(*args, **kwargs)
[docs]def injectTestCase(testCaseClass, context):
testFunctions, arguments = _createTestFunctionsFromTestCaseClass(testCaseClass)
_populateContextWithTestFunctions(context, testCaseClass, testFunctions)
_populateContextWithArguments(context, arguments)
[docs]def iterTestCenterTestCaseClasses( context ):
""" Generator yielding all TestCenterTestCaseBase derived classes in the given context. """
for cls in context.values():
if inspect.isclass( cls ):
if issubclass( cls, TestCenterTestCaseBase ):
yield cls
[docs]def injectAllTestCenterTestCases( context ):
""" Injects all test methods found in classes derived from TestCenterTestCaseBase into
the given context (e.g. globals()). Will ignore test classes that are superclasses
of other test classes in the given context to prevent duplicate test functions
for test hierarchies.
"""
allTestClasses = list( iterTestCenterTestCaseClasses( context ) )
for cls in list( iterTestCenterTestCaseClasses( context ) ):
if not _containsDerivedClasses( allTestClasses, cls ):
injectTestCase( cls, context )
def _containsDerivedClasses( classes, superClass ):
for c in classes:
if issubclass( c, superClass ) and c is not superClass:
return True
return False
def _createTestFunctionsFromTestCaseClass(testCaseClass):
testFunctions = dict()
arguments = dict()
for testFctName in dir(testCaseClass):
if _isNameOfTestFunction(testFctName):
testFunction = _createTestFunction(testCaseClass, testFctName)
testFunctions.update(testFunction)
elif _isNameOfIterativeTestFunction(testFctName):
newTestFunctions, newArguments = _createIterativeTestFunctions(testCaseClass, testFctName)
testFunctions.update(newTestFunctions)
arguments.update(newArguments)
return testFunctions, arguments
def _createTestFunction(testCaseClass, testFctName):
testFunction = dict()
potentialFunction = getattr(testCaseClass, testFctName)
if callable(potentialFunction):
potentialFunction.__name__ = testFctName
potentialFunction._testCaseClass = testCaseClass
testFunction[ testFctName ] = potentialFunction
return testFunction
def _createIterativeTestFunctions(testCaseClass, testFctName):
testFunctions = dict()
arguments = dict()
potentialFunction = getattr(testCaseClass, testFctName)
if not callable(potentialFunction):
Logging.error('Potential iterative test function {} is not callable'.format(testFctName))
return dict(), dict()
testCase = testCaseClass()
cases, actualFunction = potentialFunction(testCase)
if not callable(actualFunction):
Logging.error('Function returned by iterative test {} is not callable'.format(testFctName))
return dict(), dict()
iterativeTestName = testFctName.replace('ITERATIVETEST_', '')
if not isinstance(cases, dict):
try:
cases = {k: v for k, v in enumerate(cases)}
except TypeError:
Logging.error('Iterative test {} did not return an iterable'.format(testFctName))
return dict(), dict()
if inspect.ismethod(actualFunction) and _isBoundMethodOf(actualFunction, testCaseClass):
Logging.warning('The callable returned by iterative test {} is a bound method, and will be redirected to a new instance of the test case class.'.format(testFctName))
for caseName, argsTuple in cases.items():
fullName = 'TEST_{}_{}'.format(iterativeTestName, caseName)
testFunctions[fullName] = _createIteratedFunction(fullName, testCaseClass, actualFunction)
arguments[fullName] = _createArguments(argsTuple)
return testFunctions, arguments
def _createIteratedFunction(fullName, testCaseClass, actualFunction):
if inspect.isfunction(actualFunction):
def containerFunction(*args, **kwargs):
spec = inspect.getfullargspec(actualFunction)
if spec.args and spec.args[0] == 'self':
actualArgs = args
else:
actualArgs = args[1:]
actualFunction(*actualArgs, **kwargs)
else:
def containerFunction(*args, **kwargs):
_callFunction(actualFunction, *args, **kwargs)
containerFunction.__name__ = fullName
containerFunction._testCaseClass = testCaseClass
return containerFunction
def _createArguments(argsTuple):
if not isinstance(argsTuple, tuple):
argsTuple = (argsTuple,)
return argsTuple
def _populateContextWithTestFunctions(context, testCaseClass, testFunctions):
STORED_SETUP_FUNCTION = 'STORED_setUpTestCase'
SETUP_FUNCTION = 'setUpTestCase'
if testFunctions:
for fctName, fct in testFunctions.items():
context[fctName] = fct
context['GROUP_{}'.format(testCaseClass.__name__)] = lambda: list(testFunctions.values())
hasAlreadyInjected = context.get('VISITED', False)
if hasAlreadyInjected:
originalSetupFunction = context.get(STORED_SETUP_FUNCTION, None)
else:
originalSetupFunction = context.get(SETUP_FUNCTION, None)
context[STORED_SETUP_FUNCTION] = originalSetupFunction
def _callSetUp():
# CAVEAT
# Partial functions are not considered "callable" by MLABModule::callableFunctions,
# so we have to wrap the function call
functools.partial(__setUpTestCase, context)()
def _callSetUpWithExisting():
originalSetupFunction()
functools.partial(__setUpTestCase, context)()
if originalSetupFunction:
context[SETUP_FUNCTION] = _callSetUpWithExisting
else:
context[SETUP_FUNCTION] = _callSetUp
context['VISITED'] = True
def __setUpTestCase(context):
for fctName, fct in context.items():
if _isNameOfTestFunction(fctName):
if 'ARGUMENTS_{}'.format(fctName) in context:
args = context['ARGUMENTS_{}'.format(fctName)]
context[fctName] = functools.partial(_decorate(fct), args)
else:
context[fctName] = _decorate(fct)
def _decorate( fct ):
@functools.wraps( fct ) # used to copy function properties like docstring etc to wrapped function
def decorated( *args, **kwargs ):
testCase = fct._testCaseClass()
testCase.setUp()
try:
if len(args) > 0:
actualArgs = args[0]
else:
actualArgs = args
fct(testCase, *actualArgs, **kwargs)
except CancelTestException:
raise
except Exception:
testCase.tearDown()
raise
else:
testCase.tearDown()
return decorated
def _populateContextWithArguments(context, arguments):
if arguments:
for fctName, args in arguments.items():
context['ARGUMENTS_{}'.format(fctName)] = args