From f568e319b4261198b8eac6b373e4ee680c13ce22 Mon Sep 17 00:00:00 2001 From: wachambo Date: Tue, 4 Apr 2017 19:46:28 +0200 Subject: [PATCH 1/4] Added test suite attributes See: https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd --- junit_xml/__init__.py | 51 +++++++++++++++++++++++++----- test_junit_xml.py | 72 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/junit_xml/__init__.py b/junit_xml/__init__.py index f53e9e5..7ff1c5f 100644 --- a/junit_xml/__init__.py +++ b/junit_xml/__init__.py @@ -79,8 +79,9 @@ class TestSuite(object): Can handle unicode strings or binary strings if their encoding is provided. """ - def __init__(self, name, test_cases=None, hostname=None, id=None, - package=None, timestamp=None, properties=None): + def __init__(self, name, test_cases=None, timestamp=None, hostname=None, + id=None, package=None, file=None, log=None, url=None, + stdout=None, stderr=None, properties=None): self.name = name if not test_cases: test_cases = [] @@ -89,10 +90,15 @@ def __init__(self, name, test_cases=None, hostname=None, id=None, except TypeError: raise Exception('test_cases must be a list of test cases') self.test_cases = test_cases + self.timestamp = timestamp self.hostname = hostname self.id = id self.package = package - self.timestamp = timestamp + self.file = file + self.log = log + self.url = url + self.stdout = stdout + self.stderr = stderr self.properties = properties def build_xml_doc(self, encoding=None): @@ -106,6 +112,11 @@ def build_xml_doc(self, encoding=None): # build the test suite element test_suite_attributes = dict() test_suite_attributes['name'] = decode(self.name, encoding) + if any(c.assertions for c in self.test_cases): + test_suite_attributes['assertions'] = \ + str(sum([int(c.assertions) for c in self.test_cases if c.assertions])) + test_suite_attributes['disabled'] = \ + str(len([c for c in self.test_cases if c.is_disabled()])) test_suite_attributes['failures'] = \ str(len([c for c in self.test_cases if c.is_failure()])) test_suite_attributes['errors'] = \ @@ -124,6 +135,12 @@ def build_xml_doc(self, encoding=None): test_suite_attributes['package'] = decode(self.package, encoding) if self.timestamp: test_suite_attributes['timestamp'] = decode(self.timestamp, encoding) + if self.timestamp: + test_suite_attributes['file'] = decode(self.file, encoding) + if self.timestamp: + test_suite_attributes['log'] = decode(self.log, encoding) + if self.timestamp: + test_suite_attributes['url'] = decode(self.url, encoding) xml_element = ET.Element("testsuite", test_suite_attributes) @@ -134,12 +151,22 @@ def build_xml_doc(self, encoding=None): attrs = {'name': decode(k, encoding), 'value': decode(v, encoding)} ET.SubElement(props_element, "property", attrs) + # add test suite stdout + if self.stdout: + stdout_element = ET.SubElement(xml_element, "system-out") + stdout_element.text = decode(self.stdout, encoding) + + # add test suite stderr + if self.stderr: + stderr_element = ET.SubElement(xml_element, "system-err") + stderr_element.text = decode(self.stderr, encoding) + # test cases for case in self.test_cases: test_case_attributes = dict() test_case_attributes['name'] = decode(case.name, encoding) if case.assertions: - test_case_attributes['assertions'] = decode(case.assertions, encoding) + test_case_attributes['assertions'] = "%d" % case.assertions if case.elapsed_sec: test_case_attributes['time'] = "%f" % case.elapsed_sec if case.timestamp: @@ -227,7 +254,7 @@ def to_xml_string(test_suites, prettyprint=True, encoding=None): attributes = defaultdict(int) for ts in test_suites: ts_xml = ts.build_xml_doc(encoding=encoding) - for key in ['failures', 'errors', 'tests']: + for key in ['failures', 'errors', 'tests', 'disabled']: attributes[key] += int(ts_xml.get(key, 0)) for key in ['time']: attributes[key] += float(ts_xml.get(key, 0)) @@ -292,8 +319,9 @@ class TestCase(object): """A JUnit test case with a result and possibly some stdout or stderr""" def __init__(self, name, assertions=None, elapsed_sec=None, - timestamp=None, classname=None, status=None, category=None, file=None, line=None, - log=None, group=None, url=None, stdout=None, stderr=None): + timestamp=None, classname=None, status=None, category=None, + file=None, line=None, log=None, group=None, url=None, stdout=None, + stderr=None): self.name = name self.assertions = assertions self.elapsed_sec = elapsed_sec @@ -308,6 +336,7 @@ def __init__(self, name, assertions=None, elapsed_sec=None, self.stdout = stdout self.stderr = stderr + self.enable = True self.error_message = None self.error_output = None self.error_type = None @@ -317,6 +346,10 @@ def __init__(self, name, assertions=None, elapsed_sec=None, self.skipped_message = None self.skipped_output = None + def disable(self): + """Disable test case""" + self.enable = False + def add_error_info(self, message=None, output=None, error_type=None): """Adds an error message, output, or both to the test case""" if message: @@ -342,6 +375,10 @@ def add_skipped_info(self, message=None, output=None): if output: self.skipped_output = output + def is_disabled(self): + """Returns true if this test case is disable""" + return not self.enable + def is_failure(self): """returns true if this test case is a failure""" return self.failure_output or self.failure_message diff --git a/test_junit_xml.py b/test_junit_xml.py index 51cf8ef..eb0ee60 100644 --- a/test_junit_xml.py +++ b/test_junit_xml.py @@ -73,8 +73,8 @@ def test_single_suite_no_test_cases(self): (ts, tcs) = serialize_and_read( TestSuite( - 'test', - [], + name='test', + test_cases=[], hostname='localhost', id=1, properties=properties, @@ -100,8 +100,8 @@ def test_single_suite_no_test_cases_utf8(self): timestamp = 1398382805 test_suite = TestSuite( - 'äöü', - [], + name='äöü', + test_cases=[], hostname='löcalhost', id='äöü', properties=properties, @@ -131,8 +131,8 @@ def test_single_suite_no_test_cases_unicode(self): (ts, tcs) = serialize_and_read( TestSuite( - decode('äöü', 'utf-8'), - [], + name=decode('äöü', 'utf-8'), + test_cases=[], hostname=decode('löcalhost', 'utf-8'), id=decode('äöü', 'utf-8'), properties=properties, @@ -198,9 +198,8 @@ def test_multiple_suites_to_string(self): verify_test_case(self, suites[1][1][0], {'name': 'Test2'}) def test_attribute_time(self): - tss = [TestSuite('suite1', - [TestCase('Test1', 'some.class.name', 123.345), - TestCase('Test2', 'some2.class.name', 123.345)]), + tss = [TestSuite('suite1', [TestCase(name='Test1', classname='some.class.name', elapsed_sec=123.345), + TestCase(name='Test2', classname='some2.class.name', elapsed_sec=123.345)]), TestSuite('suite2', [TestCase('Test2')])] suites = serialize_and_read(tss) @@ -212,21 +211,60 @@ def test_attribute_time(self): # testcase self.assertEqual('0', suites[1][0].attributes['time'].value) + def test_attribute_disable(self): + tc = TestCase('Disabled-Test') + tc.disable() + tss = [TestSuite('suite1', [tc])] + suites = serialize_and_read(tss) + + self.assertEqual('1', suites[0][0].attributes['disabled'].value) + + def test_stderr(self): + suites = serialize_and_read( + TestSuite(name='test', stderr='I am stderr!', + test_cases=[TestCase(name='Test1')]))[0] + self.assertEqual('I am stderr!', + suites[0].getElementsByTagName('system-err')[0].firstChild.data) + + def test_stdout_stderr(self): + suites = serialize_and_read( + TestSuite(name='test', stdout='I am stdout!', + stderr='I am stderr!', + test_cases=[TestCase(name='Test1')]))[0] + self.assertEqual('I am stderr!', + suites[0].getElementsByTagName('system-err')[0].firstChild.data) + self.assertEqual('I am stdout!', + suites[0].getElementsByTagName('system-out')[0].firstChild.data) + + def test_no_assertions(self): + suites = serialize_and_read( + TestSuite(name='test', + test_cases=[TestCase(name='Test1')]))[0] + self.assertFalse(suites[0].getElementsByTagName('testcase')[0].hasAttribute('assertions')) + + def test_assertions(self): + suites = serialize_and_read( + TestSuite(name='test', + test_cases=[TestCase(name='Test1', + assertions=5)]))[0] + self.assertEquals('5', + suites[0].getElementsByTagName('testcase')[0].attributes['assertions'].value) + # @todo: add more tests for the other attributes and properties def test_to_xml_string(self): - test_suites = [TestSuite('suite1', [TestCase('Test1')]), - TestSuite('suite2', [TestCase('Test2')])] + test_suites = [TestSuite(name='suite1', test_cases=[TestCase(name='Test1')]), + TestSuite(name='suite2', test_cases=[TestCase(name='Test2')])] xml_string = TestSuite.to_xml_string(test_suites) if PY2: self.assertTrue(isinstance(xml_string, unicode)) expected_xml_string = textwrap.dedent(""" - - \t + + \t \t\t \t - \t + \t \t\t \t @@ -298,6 +336,12 @@ def test_init_stdout_stderr(self): 'time': ("%f" % 123.345)}, stdout='I am stdout!', stderr='I am stderr!') + def test_init_disable(self): + tc = TestCase('Disabled-Test') + tc.disable() + (ts, tcs) = serialize_and_read(TestSuite('test', [tc]))[0] + verify_test_case(self, tcs[0], {'name': 'Disabled-Test'}) + def test_init_failure_message(self): tc = TestCase('Failure-Message') tc.add_failure_info("failure message") From 6623601202da04dfa853ab8a74165d197a5dad74 Mon Sep 17 00:00:00 2001 From: wachambo Date: Wed, 5 Apr 2017 12:43:10 +0200 Subject: [PATCH 2/4] Backward compatibility New attributes do not break old code --- junit_xml/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/junit_xml/__init__.py b/junit_xml/__init__.py index 7ff1c5f..f1f1933 100644 --- a/junit_xml/__init__.py +++ b/junit_xml/__init__.py @@ -79,9 +79,9 @@ class TestSuite(object): Can handle unicode strings or binary strings if their encoding is provided. """ - def __init__(self, name, test_cases=None, timestamp=None, hostname=None, - id=None, package=None, file=None, log=None, url=None, - stdout=None, stderr=None, properties=None): + def __init__(self, name, test_cases=None, hostname=None, id=None, + package=None, timestamp=None, properties=None, file=None, + log=None, url=None, stdout=None, stderr=None): self.name = name if not test_cases: test_cases = [] @@ -318,10 +318,10 @@ def _clean_illegal_xml_chars(string_to_clean): class TestCase(object): """A JUnit test case with a result and possibly some stdout or stderr""" - def __init__(self, name, assertions=None, elapsed_sec=None, - timestamp=None, classname=None, status=None, category=None, - file=None, line=None, log=None, group=None, url=None, stdout=None, - stderr=None): + def __init__(self, name, classname=None, elapsed_sec=None, stdout=None, + stderr=None, assertions=None, timestamp=None, status=None, + category=None, file=None, line=None, log=None, group=None, + url=None): self.name = name self.assertions = assertions self.elapsed_sec = elapsed_sec From 503efa3e9fba2db837abbe01ea767e67343ddc62 Mon Sep 17 00:00:00 2001 From: wachambo Date: Tue, 11 Apr 2017 08:45:18 +0200 Subject: [PATCH 3/4] Removed getter/setter for is_enabled TC attr Added explanatory comment about 'assertions' attribute --- junit_xml/__init__.py | 13 +++---------- test_junit_xml.py | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/junit_xml/__init__.py b/junit_xml/__init__.py index f1f1933..4ab3ab8 100644 --- a/junit_xml/__init__.py +++ b/junit_xml/__init__.py @@ -116,7 +116,7 @@ def build_xml_doc(self, encoding=None): test_suite_attributes['assertions'] = \ str(sum([int(c.assertions) for c in self.test_cases if c.assertions])) test_suite_attributes['disabled'] = \ - str(len([c for c in self.test_cases if c.is_disabled()])) + str(len([c for c in self.test_cases if not c.is_enabled])) test_suite_attributes['failures'] = \ str(len([c for c in self.test_cases if c.is_failure()])) test_suite_attributes['errors'] = \ @@ -166,6 +166,7 @@ def build_xml_doc(self, encoding=None): test_case_attributes = dict() test_case_attributes['name'] = decode(case.name, encoding) if case.assertions: + # Number of assertions in the test case test_case_attributes['assertions'] = "%d" % case.assertions if case.elapsed_sec: test_case_attributes['time'] = "%f" % case.elapsed_sec @@ -336,7 +337,7 @@ def __init__(self, name, classname=None, elapsed_sec=None, stdout=None, self.stdout = stdout self.stderr = stderr - self.enable = True + self.is_enabled = True self.error_message = None self.error_output = None self.error_type = None @@ -346,10 +347,6 @@ def __init__(self, name, classname=None, elapsed_sec=None, stdout=None, self.skipped_message = None self.skipped_output = None - def disable(self): - """Disable test case""" - self.enable = False - def add_error_info(self, message=None, output=None, error_type=None): """Adds an error message, output, or both to the test case""" if message: @@ -375,10 +372,6 @@ def add_skipped_info(self, message=None, output=None): if output: self.skipped_output = output - def is_disabled(self): - """Returns true if this test case is disable""" - return not self.enable - def is_failure(self): """returns true if this test case is a failure""" return self.failure_output or self.failure_message diff --git a/test_junit_xml.py b/test_junit_xml.py index eb0ee60..5ddaedd 100644 --- a/test_junit_xml.py +++ b/test_junit_xml.py @@ -213,7 +213,7 @@ def test_attribute_time(self): def test_attribute_disable(self): tc = TestCase('Disabled-Test') - tc.disable() + tc.is_enabled=False tss = [TestSuite('suite1', [tc])] suites = serialize_and_read(tss) @@ -338,7 +338,7 @@ def test_init_stdout_stderr(self): def test_init_disable(self): tc = TestCase('Disabled-Test') - tc.disable() + tc.is_enabled=False (ts, tcs) = serialize_and_read(TestSuite('test', [tc]))[0] verify_test_case(self, tcs[0], {'name': 'Disabled-Test'}) From 7fd8a27ca97215a74fe07d95b01c647bea81d57e Mon Sep 17 00:00:00 2001 From: wachambo Date: Tue, 11 Apr 2017 10:41:10 +0200 Subject: [PATCH 4/4] PEP8 --- test_junit_xml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_junit_xml.py b/test_junit_xml.py index 5ddaedd..1c59aed 100644 --- a/test_junit_xml.py +++ b/test_junit_xml.py @@ -213,7 +213,7 @@ def test_attribute_time(self): def test_attribute_disable(self): tc = TestCase('Disabled-Test') - tc.is_enabled=False + tc.is_enabled = False tss = [TestSuite('suite1', [tc])] suites = serialize_and_read(tss) @@ -338,7 +338,7 @@ def test_init_stdout_stderr(self): def test_init_disable(self): tc = TestCase('Disabled-Test') - tc.is_enabled=False + tc.is_enabled = False (ts, tcs) = serialize_and_read(TestSuite('test', [tc]))[0] verify_test_case(self, tcs[0], {'name': 'Disabled-Test'})