import collections
import pytest
import attr
from cfme.utils import log
from cfme.utils.appliance import find_appliance
#: A dict of tests, and their state at various test phases
test_tracking = collections.defaultdict(dict)
# Expose the cfme logger as a fixture for convenience
@pytest.fixture(scope='session')
[docs]def logger():
return log.logger
@pytest.mark.hookwrapper
[docs]def pytest_runtest_setup(item):
path, lineno, domaininfo = item.location
logger().info(log.format_marker(_format_nodeid(item.nodeid), mark="-"),
extra={'source_file': path, 'source_lineno': lineno})
yield
[docs]def pytest_collection_modifyitems(session, config, items):
logger().info(log.format_marker('Starting new test run', mark="="))
expression = config.getvalue('keyword') or False
expr_string = ', will filter with "{}"'.format(expression) if expression else ''
logger().info('Collected {} items{}'.format(len(items), expr_string))
@attr.s(frozen=True)
[docs]def pytest_exception_interact(node, call, report):
# Despite the name, call.excinfo is a py.code.ExceptionInfo object. Its traceback property
# is similarly a py.code.TracebackEntry. The following lines, including "entry.lineno+1" are
# based on the code there, which does unintuitive things with a traceback's line number.
# This is the same code that powers py.test's output, so we gain py.test's magical ability
# to get useful AssertionError output by doing it this way, which makes the voodoo worth it.
entry = call.excinfo.traceback.getcrashentry()
logger().error(call.excinfo.getrepr(),
extra={'source_file': entry.path, 'source_lineno': entry.lineno + 1})
[docs]def pytest_sessionfinish(session, exitstatus):
c = collections.Counter()
for test in test_tracking:
c[_test_status(test)] += 1
# Prepend a total to the summary list
results = ['total: {}'.format(sum(c.values()))] + [
'{}: {}'.format(k, v) for k, v in c.items()]
# Then join it with commas
summary = ', '.join(results)
logger().info(log.format_marker('Finished test run', mark='='))
logger().info(log.format_marker(str(summary), mark='='))
def _test_status(test_name):
test_phase = test_tracking[test_name]
# Test failure in setup or teardown is an error, which pytest doesn't report internally
if 'failed' in (test_phase.get('setup', 'failed'), test_phase.get('teardown', 'failed')):
return 'error'
# A test can also be skipped
elif 'skipped' in test_phase.get('setup', 'skipped'):
return 'skipped'
# Otherwise, report the call phase outcome (passed, skipped, or failed)
else:
return test_phase.get('call', 'skipped')
def _format_nodeid(nodeid, strip_filename=True):
# Remove test class instances and filenames, replace with a dot to impersonate a method call
nodeid = nodeid.replace('::()::', '.')
# Trim double-colons to single
nodeid = nodeid.replace('::', ':')
# Strip filename (everything before and including the first colon)
if strip_filename:
try:
return nodeid.split(':', 1)[1]
except IndexError:
# No colon to split on, return the whole nodeid
return nodeid
else:
return nodeid