Skip to content

Commit

Permalink
fix: multiple results won't crash the parser (#55)
Browse files Browse the repository at this point in the history
* fix: multiple results won't crash the parser

* fix: a case

* more coverage

* minor improvements
  • Loading branch information
weiwei committed Nov 27, 2020
1 parent ec77530 commit e1270eb
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 116 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog


## [2.0.0] - 2020-11-28
### Breaking
- `TestCase.result` is now a list instead of a single item.

### Added
- `TestCase` constructor supports `time` and `classname` as params.
- `Result` object supports `text` attribute.

## [1.6.3] - 2020-11-24
### Fixed
- `JunitXML.fromstring()` now handles various inputs.
Expand Down
73 changes: 25 additions & 48 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,29 @@ junitparser -- Pythonic JUnit/xUnit Result XML Parser
.. image:: https://codecov.io/gh/weiwei/junitparser/branch/master/graph/badge.svg?token=UotlfRXNnK
:target: https://codecov.io/gh/weiwei/junitparser

What does it do?
----------------

junitparser is a JUnit/xUnit Result XML Parser. Use it to parse and manipulate
junitparser handles JUnit/xUnit Result XML files. Use it to parse and manipulate
existing Result XML files, or create new JUnit/xUnit result XMLs from scratch.

There are already a lot of modules that converts JUnit/xUnit XML from a
specific format, but you may run into some proprietory or less-known formats
and you want to convert them and feed the result to another tool, or, you may
want to manipulate the results in your own way. This is where junitparser come
into handy.
Features
--------

* Parse or modify existing JUnit/xUnit xml files.
* Parse or modify non-standard or customized JUnit/xUnit xml files, by monkey
patching existing element definitions.
* Create JUnit/xUnit test results from scratch.
* Merge test result xml files.
* Specify xml parser. For example you can use lxml to speed things up.
* Invoke from command line, or `python -m junitparser`
* Python 2 and 3 support (As of Nov 2020, 1/4 of the users are still on Python
2, so there is no plan to drop Python 2 support)

Why junitparser?
----------------
Note on version 2
-----------------

* Functionality. There are various JUnit/xUnit XML libraries, some does
parsing, some does XML generation, some does manipulation. This module does
all in a single package.
* Extensibility. JUnit/xUnit is hardly a standardized format. The base format
is somewhat universally agreed with, but beyond that, there could be "custom"
elements and attributes. junitparser aims to support them all, by
allowing the user to monkeypatch and subclass some base classes.
* Pythonic. You can manipulate test cases and suites in a pythonic way.
Version 2 improved support for pytest result xml files by fixing a few issues,
notably that there could be multiple <Failure> or <Error> entries. There is a
breaking change that ``TestCase.result`` is now a list instead of a single item.
If you are using this attribute, please update your code accordingly.

Installation
-------------
Expand All @@ -54,10 +54,11 @@ format.
from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error
# Create cases
case1 = TestCase('case1')
case1.result = Skipped()
case1 = TestCase('case1', 'class.name', 0.5) # params are optional
case1.classname = "modified.class.name" # specify or change case attrs
case1.result = [Skipped()] # You can have a list of results
case2 = TestCase('case2')
case2.result = Error('Example error message', 'the_error_type')
case2.result = [Error('Example error message', 'the_error_type')]
# Create suite and add cases
suite = TestSuite('suite1')
Expand Down Expand Up @@ -225,35 +226,11 @@ Command Line
Test
----
You can run the cases directly::
python test.py
Or use pytest::
The tests are written with python `unittest`, to run them, use pytest::
pytest test.py
Notes
-----
There are some other packages providing similar functionalities. They are
out there for a longer time, but might not be as feature-rich or fun as
junitparser:
* xunitparser_: Read JUnit/XUnit XML files and map them to Python objects
* xunitgen_: Generate xUnit.xml files
* xunitmerge_: Utility for merging multiple XUnit xml reports into a single
xml report.
* `junit-xml`_: Creates JUnit XML test result documents that can be read by
tools such as Jenkins
.. _xunitparser: https://pypi.python.org/pypi/xunitparser
.. _xunitgen: https://pypi.python.org/pypi/xunitgen
.. _xunitmerge: https://pypi.python.org/pypi/xunitmerge
.. _`junit-xml`: https://pypi.python.org/pypi/junit-xml
Contribute
----------
Please do!
PRs are welcome!
2 changes: 1 addition & 1 deletion junitparser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
FloatAttr,
)

version = "1.6.3"
version = "2.0.0b1"
94 changes: 50 additions & 44 deletions junitparser/junitparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,7 @@ class IntAttr(Attr):

def __get__(self, instance, cls):
result = super(IntAttr, self).__get__(instance, cls)
if result is None and (
isinstance(instance, JUnitXml) or isinstance(instance, TestSuite)
):
if result is None and isinstance(instance, (JUnitXml, TestSuite)):
instance.update_statistics()
result = super(IntAttr, self).__get__(instance, cls)
return int(result) if result else None
Expand All @@ -114,9 +112,7 @@ class FloatAttr(Attr):

def __get__(self, instance, cls):
result = super(FloatAttr, self).__get__(instance, cls)
if result is None and (
isinstance(instance, JUnitXml) or isinstance(instance, TestSuite)
):
if result is None and isinstance(instance, (JUnitXml, TestSuite)):
instance.update_statistics()
result = super(FloatAttr, self).__get__(instance, cls)
return float(result) if result else None
Expand Down Expand Up @@ -161,8 +157,8 @@ def __repr__(self):
['%s="%s"' % (key, self._elem.attrib[key]) for key in keys]
)
return """<Element '%s' %s>""" % (tag, attrs_str)
else:
return """<Element '%s'>""" % tag

return """<Element '%s'>""" % tag

def append(self, sub_elem):
"""Adds the element subelement to the end of this elements internal
Expand Down Expand Up @@ -408,12 +404,12 @@ def __iadd__(self, other):
self.add_testsuite(suite)
self.update_statistics()
return self
else:
result = JUnitXml()
result.filepath = self.filepath
result.add_testsuite(self)
result.add_testsuite(other)
return result

result = JUnitXml()
result.filepath = self.filepath
result.add_testsuite(self)
result.add_testsuite(other)
return result

def remove_testcase(self, testcase):
"""Removes a test case from the suite."""
Expand All @@ -428,14 +424,15 @@ def update_statistics(self):
time = 0
for case in self:
tests += 1
if isinstance(case.result, Failure):
failures += 1
elif isinstance(case.result, Error):
errors += 1
elif isinstance(case.result, Skipped):
skipped += 1
if case.time is not None:
time += case.time
for entry in case.result:
if isinstance(entry, Failure):
failures += 1
elif isinstance(entry, Error):
errors += 1
elif isinstance(entry, Skipped):
skipped += 1
self.tests = tests
self.errors = errors
self.failures = failures
Expand Down Expand Up @@ -583,6 +580,13 @@ def __eq__(self, other):
and self.message == other.message
)

@property
def text(self):
return self._elem.text

@text.setter
def text(self, value):
self._elem.text = value

class Skipped(Result):
"""Test result when the case is skipped."""
Expand Down Expand Up @@ -633,46 +637,48 @@ class TestCase(Element):
classname = Attr()
time = FloatAttr()

def __init__(self, name=None):
def __init__(self, name=None, classname=None, time=None):
super(TestCase, self).__init__(self._tag)
self.name = name
if name is not None:
self.name = name
if classname is not None:
self.classname = classname
if time is not None:
self.time = float(time)

def __hash__(self):
return super(TestCase, self).__hash__()

def __iter__(self):
all_types = set.union(POSSIBLE_RESULTS, {SystemOut}, {SystemErr})
for elem in self._elem.iter():
for entry_type in all_types:
if elem.tag == entry_type._tag:
yield entry_type.fromelem(elem)

def __eq__(self, other):
# TODO: May not work correctly if unreliable hash method is used.
return hash(self) == hash(other)

@property
def result(self):
"""One of the Failure, Skipped, or Error objects."""
"""A list of Failure, Skipped, or Error objects."""
results = []
for res in POSSIBLE_RESULTS:
result = self.child(res)
if result is not None:
results.append(result)
if len(results) > 1:
raise JUnitXmlError("Only one result allowed per test case.")
elif len(results) == 0:
return None
else:
return results[0]
for entry in self:
if isinstance(entry, tuple(POSSIBLE_RESULTS)):
results.append(entry)

return results

@result.setter
def result(self, value):
# First remove all existing results
for res in POSSIBLE_RESULTS:
result = self.child(res)
if result is not None:
self.remove(result)
# Then add current result
if (
isinstance(value, Skipped)
or isinstance(value, Failure)
or isinstance(value, Error)
):
self.append(value)
for entry in self:
if any(isinstance(entry, r) for r in POSSIBLE_RESULTS ):
self.remove(entry)
for entry in value:
if any(isinstance(entry, r) for r in POSSIBLE_RESULTS ):
self.append(entry)

@property
def system_out(self):
Expand Down
23 changes: 12 additions & 11 deletions tests/test_fromfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ def test_fromfile(self):
self.assertEqual(len(suite2), 3)
self.assertEqual(suite2.name, "JUnitXmlReporter.constructor")
self.assertEqual(suite2.tests, 3)
case_results = [Failure, Skipped, type(None)]
for case, result in zip(suite2, case_results):
self.assertIsInstance(case.result, result)
cases = list(suite2.iterchildren(TestCase))
self.assertIsInstance(cases[0].result[0], Failure)
self.assertIsInstance(cases[1].result[0], Skipped)
self.assertEqual(len(cases[2].result), 0)

@unittest.skipUnless(has_lxml, "lxml required to run the case")
def test_fromfile_with_parser(self):
Expand All @@ -75,9 +76,10 @@ def parse_func(file_path):
self.assertEqual(len(suite2), 3)
self.assertEqual(suite2.name, "JUnitXmlReporter.constructor")
self.assertEqual(suite2.tests, 3)
case_results = [Failure, Skipped, type(None)]
for case, result in zip(suite2, case_results):
self.assertIsInstance(case.result, result)
cases = list(suite2.iterchildren(TestCase))
self.assertIsInstance(cases[0].result[0], Failure)
self.assertIsInstance(cases[1].result[0], Skipped)
self.assertEqual(len(cases[2].result), 0)

def test_fromfile_without_testsuites_tag(self):
xml = JUnitXml.fromfile(
Expand All @@ -89,9 +91,9 @@ def test_fromfile_without_testsuites_tag(self):
self.assertEqual(len(cases), 3)
self.assertEqual(xml.name, "JUnitXmlReporter.constructor")
self.assertEqual(xml.tests, 3)
case_results = [Failure, Skipped, type(None)]
for case, result in zip(xml, case_results):
self.assertIsInstance(case.result, result)
self.assertIsInstance(cases[0].result[0], Failure)
self.assertIsInstance(cases[1].result[0], Skipped)
self.assertEqual(len(cases[2].result), 0)

def test_write_xml_withouth_testsuite_tag(self):
suite = TestSuite()
Expand Down Expand Up @@ -187,8 +189,7 @@ def test_multi_results_in_case(self):
xml = JUnitXml.fromstring(text)
suite = next(iter(xml))
case = next(iter(suite))
with self.assertRaises(JUnitXmlError):
result = case.result
self.assertEqual(len(case.result), 2)

def test_write_pretty(self):
suite1 = TestSuite()
Expand Down

0 comments on commit e1270eb

Please sign in to comment.