# -*- coding: utf8 -*-
"""
Step by step case
"""
import re
import sys
import logging
import traceback
from functools import wraps
from six import with_metaclass
from noseapp.utils import pyv
from noseapp.case.base import TestCase
logger = logging.getLogger(__name__)
DOC_ATTRIBUTE_NAME = '__DOC__'
WEIGHT_ATTRIBUTE_NAME = '__WEIGHT__'
SCREENPLAY_ATTRIBUTE_NAME = '__SCREENPLAY__'
STEP_INFO_PATTERN = u'<{}.{}> Step {} "{}"'
EXCLUDE_METHOD_PATTERN = re.compile(
r'^fail[A-Z]|^fail$|^assert[A-Z]|^assert_$',
)
ERROR_MESSAGE_FULL_TEMPLATE = u"""
* Traceback:
{traceback}
* History:
{history}
* Point:
{case}.{method} -> Step {step} "{step_doc}"
* Flow:
{flow}
* Raised:
{raised}
* Message:
{message}
"""
class StepFail(AssertionError):
pass
class StepError(Exception):
pass
def re_raise_exc(e, msg):
"""
Re raise exception with new message
:param e: exception instance
:param msg: exception message
"""
def error(m):
if isinstance(e, AssertionError):
return StepFail(m)
return StepError(m)
if pyv.IS_PYTHON_2:
raise error(msg)
_, _, tb = sys.exc_info()
raise error(msg).with_traceback(tb)
def step(num, doc=''):
"""
Decorator. Use wrapper method as case step.
:param num: step num
:type num: int
:param doc: step description
:type doc: str, unicode
"""
def wrapper(f):
if not isinstance(num, int):
raise ValueError('step num mast be int only')
setattr(f, DOC_ATTRIBUTE_NAME, doc)
setattr(f, WEIGHT_ATTRIBUTE_NAME, num)
setattr(f, SCREENPLAY_ATTRIBUTE_NAME, '')
@wraps(f)
def wrapped(*args, **kwargs):
return f(*args, **kwargs)
return wrapped
return wrapper
def format_traceback(tb, step_info=None):
"""
Delete first line of traceback.
Add step info to last line of traceback message.
:param tb: traceback message
"""
tb = tb.splitlines()[1:]
if step_info:
last_line = tb[-1:][0]
tb = tb[:-1]
try:
exc_name, exc_message = last_line.split(':')
except ValueError:
exc_message = None
exc_name = last_line.split(':')[0]
if exc_message:
last_line = '{} on {}: {}'.format(exc_name, step_info, exc_message)
else:
last_line = '{} on {}'.format(exc_name, step_info)
tb.append(last_line)
return '\n'.join(tb)
def unicode_string(string):
try:
return pyv.unicode(string)
except UnicodeDecodeError as e:
try:
return pyv.unicode(string.decode('utf-8'))
except AttributeError: # for py 3 only
raise e
def get_step_info(case, method):
"""
:param case: TestCase instance
:param method: step method
:return: tuple
"""
case_name = case.__class__.__name__
try:
method_name = method.__func__.__name__
except AttributeError:
method_name = method.__name__
weight = getattr(method, WEIGHT_ATTRIBUTE_NAME)
doc = getattr(method, DOC_ATTRIBUTE_NAME)
return case_name, method_name, weight, doc
def get_exception_message(e):
"""
Get message from exception instance
:param e: exception instance
"""
if hasattr(e, 'message'):
return e.message
return u''.join(e.args)
def perform_prompt(case_name, method_name, exit_code=0):
"""
Interactive mode for debug
:param case_name: case class name
:param method_name: step method name
:param exit_code: exit code
"""
commands = { # prompt commands
'exit': 'q',
'debug': 'd',
'continue': 'c',
}
prompt = '>> {}.{} [{}]: '.format(
case_name, method_name, ', '.join([c for _, c in commands.items()])
)
command = raw_input(prompt).strip()
if command == commands['exit']:
sys.exit(exit_code)
elif command == commands['debug']:
try:
import ipdb as pdb
except ImportError:
import pdb
pdb.set_trace()
def run_step(case, method, flow=None):
"""
Run step
:param case: TestCase instance
:param method: step method
:param flow: from FLOWS property
"""
case_name, method_name, weight, doc = get_step_info(case, method)
step_info = STEP_INFO_PATTERN.format(
case_name, method_name, weight, doc,
)
if case.USE_PROMPT:
perform_prompt(case_name, method_name)
logger.info(step_info)
try:
if flow is not None:
method(case, flow)
else:
method(case)
except BaseException as e:
if not case.ERROR_MESSAGE_TEMPLATE:
raise
orig_tb = traceback.format_exc()
history = u'\n'.join(case.__history)
exc_cls_name = e.__class__.__name__
if case.RENDER_ERROR_MESSAGE and pyv.IS_PYTHON_2: # feature for python 2 only
msg = case.render_error_message(
history=unicode_string(history),
case=unicode_string(case_name),
method=unicode_string(method_name),
step=unicode_string(weight),
step_doc=unicode_string(doc),
flow=unicode_string(flow),
raised=unicode_string(exc_cls_name),
traceback=unicode_string(format_traceback(orig_tb)),
message=unicode_string(get_exception_message(e)),
)
else:
msg = '\n' + unicode_string(
format_traceback(
orig_tb,
step_info='<{case}.{method}(num={step}, doc={doc}, flow={flow})>'.format(
case=unicode_string(case_name),
method=unicode_string(method_name),
step=unicode_string(weight),
doc=unicode_string(doc),
flow=unicode_string(flow),
),
),
)
re_raise_exc(e, msg)
def make_run_test(steps):
"""
Create runTest method
:param steps: steps list
"""
def run_test(self):
run_test.__doc__ = self.__doc__
self.begin()
self.__history = []
history_line = u'{}. {}'
if self.FLOWS and hasattr(self.FLOWS, '__iter__'):
for flow in self.FLOWS:
for step_method in steps:
_, _, step, doc = get_step_info(self, step_method)
self.__history.append(history_line.format(step, doc))
run_step(self, step_method, flow=flow)
else:
self.__history = []
else:
for step_method in steps:
_, _, step, doc = get_step_info(self, step_method)
self.__history.append(history_line.format(step, doc))
run_step(self, step_method)
self.finalize()
return run_test
class ScreenPlayCaseMeta(type):
"""
Build step methods and create runTest
"""
def __new__(mcs, name, bases, dct):
steps = []
cls = type.__new__(mcs, name, bases, dct)
attributes = (
a for a in dir(cls)
if not a.startswith('_')
and
EXCLUDE_METHOD_PATTERN.search(a) is None
)
for atr in attributes:
method = getattr(cls, atr, None)
if hasattr(method, SCREENPLAY_ATTRIBUTE_NAME):
steps.append(method)
if steps:
steps.sort(
key=lambda m: getattr(m, WEIGHT_ATTRIBUTE_NAME),
)
cls.runTest = make_run_test(steps)
return cls
[docs]class ScreenPlayCase(with_metaclass(ScreenPlayCaseMeta, TestCase)):
"""
Test case for implementation by step script
Usage::
class CaseExample(ScreenPlayCase):
USE_PROMPT = True # usage interactive debug
@step(1, 'description')
def step_1(self):
self.assertTrue(True)
@step(2, 'description')
def step_2(self):
self.assertTrue(True)
class CaseParametrizeExample(ScreenPlayCase):
FLOWS = (
1, 2, 3
)
@step(1, u'Step 1')
def step_1(self, i):
self.assertGreater(i, 0)
@step(2, u'Step 2')
def step_2(self, i):
self.assertGreater(i, 0)
class SimpleCaseClass(ScreenPlayCase):
def test(self):
pass
"""
FLOWS = None
USE_PROMPT = False
RENDER_ERROR_MESSAGE = False
ERROR_MESSAGE_TEMPLATE = ERROR_MESSAGE_FULL_TEMPLATE
@property
def error_template_params(self):
"""
Params for ERROR_MESSAGE_TEMPLATE render.
:rtype: dict
"""
return {}
[docs] def begin(self):
"""
Callback. Will be called before run steps.
"""
pass
[docs] def finalize(self):
"""
Callback. Will be called after run steps.
If exception at step method will be raised then
method can't to be called.
"""
pass
[docs] def render_error_message(self, **kwargs):
"""
Render error message by template.
"""
kwargs.update(self.error_template_params)
message = unicode_string(self.ERROR_MESSAGE_TEMPLATE)
for k, v in kwargs.items():
message = message.replace(u'{%s}' % k, v)
return message.encode('utf-8', 'replace')