Skip to content

Add support for setting custom xml attributes #273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,35 @@ if __name__ == '__main__':
failfast=False, buffer=False, catchbreak=False)
````

### Custom XML attributes

In order to add custom xml attributes to the xml report, use the `annotate` decorator.
It takes a key and a value, or for more than one attribute, a dictionary.
Keys must be strings; values must be strings, integers, floats or booleans.

````python
import unittest
import xmlrunner
from xmlrunner.runner import annotate

@annotate("TestsuiteId", 1234567890)
class DemonstrateAnnotations(unittest.TestCase):

@annotate({
"Tester": "Nova Solomon",
"TestTicketId": 13,
"UsesUnittestModule": True})
def test_annotation(self):
pass

if __name__ == '__main__':
unittest.main(
testRunner=xmlrunner.XMLTestRunner(output='test-reports'),
# these make sure that some options that are not applicable
# remain hidden from the help menu.
failfast=False, buffer=False, catchbreak=False)
````

### Doctest support

The XMLTestRunner can also be used to report on docstrings style tests.
Expand Down
76 changes: 76 additions & 0 deletions tests/testsuite.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from xmlrunner.result import _DuplicateWriter
from xmlrunner.result import _XMLTestResult
from xmlrunner.result import resolve_filename
from xmlrunner.runner import annotate
import doctest
import tests.doctest_example
from io import StringIO, BytesIO
Expand Down Expand Up @@ -196,6 +197,47 @@ class DecoratedUnitTest(unittest.TestCase):
def test_pass(self):
pass

@annotate("type", "testsuite")
class AnnotatedUnitTest(unittest.TestCase):

@annotate("two", "arguments")
def test_two_arguments(self):
pass

@annotate("int", 1)
def test_int_annotation(self):
pass

@annotate("float", 1.0)
def test_float_annotation(self):
1 / 0

@annotate("bool", True)
def test_bool_annotation(self):
pass

@annotate({"type": "dict"})
def test_dict_annotation(self):
pass

@annotate({"str": "test", "int": 1, "float": 1.0, "bool": True})
def test_dict_annotation_types(self):
pass

@annotate()
def test_empty_annotation(self):
pass

@unittest.expectedFailure
@annotate("should", "fail")
def test_annotated_failure(self):
self.assertTrue(False)

@unittest.skip("Testing annotated skip")
@annotate("should", "skip")
def test_annotated_skip(self):
pass

class DummyErrorInCallTest(unittest.TestCase):

def __call__(self, result):
Expand Down Expand Up @@ -809,6 +851,40 @@ def test_xmlrunner_hold_traceback(self):
countAfterTest = sys.getrefcount(self.DummyRefCountTest.dummy)
self.assertEqual(countBeforeTest, countAfterTest)

def test_annotations(self):
suite = unittest.TestSuite()
test_methods = (
"test_two_arguments",
"test_int_annotation",
"test_float_annotation",
"test_bool_annotation",
"test_dict_annotation",
"test_dict_annotation_types",
"test_empty_annotation",
"test_annotated_failure",
"test_annotated_skip"
)
for method in test_methods:
suite.addTest(self.AnnotatedUnitTest(method))
outdir = BytesIO()

self._test_xmlrunner(suite, outdir=outdir)

output = outdir.getvalue()
xml_attributes = (
b'type="testsuite"',
b'two="arguments"',
b'int="1"',
b'float="1.0"',
b'bool="True"',
b'type="dict"',
b'str="test"',
b'should="fail"',
b'should="skip"'
)
for attr in xml_attributes:
self.assertIn(attr, output)

class StderrXMLTestRunner(xmlrunner.XMLTestRunner):
"""
XMLTestRunner that outputs to sys.stderr that might be replaced
Expand Down
25 changes: 25 additions & 0 deletions xmlrunner/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ def __init__(self, test_result, test_method, outcome=SUCCESS, err=None, subTest=
self.filename = filename
self.lineno = lineno
self.doc = doc
self.annotations = test_result._annotations
self.suite_annotations = test_result._suite_annotations

def id(self):
return self.test_id
Expand Down Expand Up @@ -332,6 +334,7 @@ def addSuccess(self, test):
Called when a test executes successfully.
"""
self._save_output_data()
self._copy_annotations(test)
self._prepare_callback(
self.infoclass(self, test), self.successes, 'ok', '.'
)
Expand All @@ -342,6 +345,7 @@ def addFailure(self, test, err):
Called when a test method fails.
"""
self._save_output_data()
self._copy_annotations(test)
testinfo = self.infoclass(
self, test, self.infoclass.FAILURE, err)
self.failures.append((
Expand All @@ -356,6 +360,7 @@ def addError(self, test, err):
Called when a test method raises an error.
"""
self._save_output_data()
self._copy_annotations(test)
testinfo = self.infoclass(
self, test, self.infoclass.ERROR, err)
self.errors.append((
Expand Down Expand Up @@ -384,6 +389,7 @@ def addSubTest(self, testcase, test, err):
errorList = self.errors

self._save_output_data()
self._copy_annotations(test)

testinfo = self.infoclass(
self, testcase, errorValue, err, subTest=test)
Expand All @@ -398,6 +404,7 @@ def addSkip(self, test, reason):
Called when a test method was skipped.
"""
self._save_output_data()
self._copy_annotations(test)
testinfo = self.infoclass(
self, test, self.infoclass.SKIP, reason)
testinfo.test_exception_name = 'skip'
Expand All @@ -410,6 +417,7 @@ def addExpectedFailure(self, test, err):
Missing in xmlrunner, copy-pasted from xmlrunner addError.
"""
self._save_output_data()
self._copy_annotations(test)

testinfo = self.infoclass(self, test, self.infoclass.SKIP, err)
testinfo.test_exception_name = 'XFAIL'
Expand All @@ -424,6 +432,7 @@ def addUnexpectedSuccess(self, test):
Missing in xmlrunner, copy-pasted from xmlrunner addSuccess.
"""
self._save_output_data()
self._copy_annotations(test)

testinfo = self.infoclass(self, test) # do not set outcome here because it will need exception
testinfo.outcome = self.infoclass.ERROR
Expand All @@ -449,6 +458,11 @@ def printErrorList(self, flavour, errors):
self.stream.writeln('%s' % test_info.get_error_info())
self.stream.flush()

def _copy_annotations(self, test):
test_method = getattr(test, test._testMethodName)
self._annotations = getattr(test_method, "_annotations", None)
self._suite_annotations = getattr(test, "_annotations", None)

def _get_info_by_testcase(self):
"""
Organizes test results by TestCase module. This information is
Expand Down Expand Up @@ -512,6 +526,13 @@ def _report_testsuite(suite_name, tests, xml_document, parentElement,
skips = filter(lambda e: e.outcome == _TestInfo.SKIP, tests)
testsuite.setAttribute('skipped', str(len(list(skips))))

# indexing is necessary since each test info instance
# carries the annotations of its test suite
annotations = tests[0].suite_annotations
if annotations:
for attr, value in annotations.items():
testsuite.setAttribute(attr, str(value))

_XMLTestResult._report_testsuite_properties(
testsuite, xml_document, properties)

Expand Down Expand Up @@ -575,6 +596,10 @@ def _report_testcase(test_result, xml_testsuite, xml_document):
if test_result.lineno is not None:
testcase.setAttribute('line', str(test_result.lineno))

if test_result.annotations:
for attr, value in test_result.annotations.items():
testcase.setAttribute(attr, str(value))

if test_result.doc is not None:
comment = str(test_result.doc)
# The use of '--' is forbidden in XML comments
Expand Down
24 changes: 24 additions & 0 deletions xmlrunner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,27 @@ def runTests(self):
finally:
if output_file is not None:
output_file.close()

def annotate(*args):
"""
Add further information as xml attributes to a test cases or a test suite
"""
def decorator(test_item): # data as dict
data = {}
if len(args) == 1 and isinstance(args[0], dict):
for key, value in args[0].items():
if not (isinstance(key, str) or isinstance(value, (str, int, float, bool))):
raise TypeError("add_info takes (key: str, value: (str, int, float)) or (dict(key:str, value, (str, int, float, bool)))")
data = args[0]
elif (
len(args) == 2 and
isinstance(args[0], str) and
isinstance(args[1], (str, int, float, bool))):
data = {args[0]: args[1]}
elif not args:
return test_item
else:
raise TypeError("add_info takes (key: str, value: (str, int, float)) or (dict(key:str, value, (str, int, float, bool)))")
test_item._annotations = data
return test_item
return decorator