junitparser-1.4.1/0000755000175000017500000000000013601064053012437 5ustar bagebagejunitparser-1.4.1/bin/0000755000175000017500000000000013601064053013207 5ustar bagebagejunitparser-1.4.1/bin/junitparser0000755000175000017500000000202613601064053015503 0ustar bagebage#!/usr/bin/env python import argparse from junitparser import JUnitXml def merge(paths, output): """Merge xml report.""" result = JUnitXml() for path in args.paths: result += JUnitXml.fromfile(path) result.update_statistics() result.write(args.output) if __name__ == '__main__': parser = argparse.ArgumentParser( description='Junitparser CLI helper.') parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + argparse.__version__) command_parser = parser.add_subparsers( dest='command', help='command') # merge merge_parser = command_parser.add_parser( 'merge', help='Merge Junit XML format reports with junitparser.') merge_parser.add_argument( 'paths', nargs='+', help='Original XML path(s).') merge_parser.add_argument( 'output', help='Merged XML Path.') args = parser.parse_args() # entrypoint if args.command == 'merge': merge(args.paths, args.output) junitparser-1.4.1/docs/0000755000175000017500000000000013601064053013367 5ustar bagebagejunitparser-1.4.1/docs/index.rst0000644000175000017500000000067313601064053015236 0ustar bagebage.. junitparser documentation master file, created by sphinx-quickstart on Fri Mar 1 10:40:19 2019. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. toctree:: :maxdepth: 2 :caption: Contents: .. include:: ../README.rst .. .. automodule:: junitparser .. :members: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` junitparser-1.4.1/docs/conf.py0000644000175000017500000001222613601064053014671 0ustar bagebage# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('../junitparser')) # -- Project information ----------------------------------------------------- project = 'junitparser' copyright = '2019, Joel Wang' author = 'Joel Wang' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'junitparserdoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'junitparser.tex', 'junitparser Documentation', 'Joel Wang', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'junitparser', 'junitparser Documentation', [author], 1) ] # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'junitparser', 'junitparser Documentation', author, 'junitparser', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- junitparser-1.4.1/docs/Makefile0000644000175000017500000000110413601064053015023 0ustar bagebage# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)junitparser-1.4.1/docs/make.bat0000644000175000017500000000142313601064053014774 0ustar bagebage@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd junitparser-1.4.1/LICENSE0000644000175000017500000000110013601064053013434 0ustar bagebageCopyright 2016 Joel Wang Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.junitparser-1.4.1/README.rst0000644000175000017500000001537113601064053014135 0ustar bagebagejunitparser -- Pythonic JUnit/xUnit Result XML Parser ====================================================== .. image:: https://api.codacy.com/project/badge/Grade/00cfb3ab06ca4e419b787af69ff2a8c3 :alt: Codacy Badge :target: https://app.codacy.com/app/gastlygem/junitparser?utm_source=github.com&utm_medium=referral&utm_content=gastlygem/junitparser&utm_campaign=badger .. image:: https://travis-ci.org/gastlygem/junitparser.svg?branch=master .. image:: https://coveralls.io/repos/github/gastlygem/junitparser/badge.svg?branch=master :target: https://coveralls.io/github/gastlygem/junitparser?branch=master What does it do? ---------------- junitparser is a JUnit/xUnit Result XML Parser. 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. Why junitparser? ---------------- * 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. Installation ------------- :: pip install junitparser Usage ----- You should be relatively familiar with the Junit XML format. If not, run ``pydoc`` on the exposed classes and functions to see how it's structured. Create Junit XML format reports from scratch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You have some test result data, and you want to convert them into junit.xml format. .. code-block:: python from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error # Create cases case1 = TestCase('case1') case1.result = Skipped() case2 = TestCase('case2') case2.result = Error('Example error message', 'the_error_type') # Create suite and add cases suite = TestSuite('suite1') suite.add_property('build', '55') suite.add_testcase(case1) suite.add_testcase(case2) suite.remove_testcase(case2) # Add suite to JunitXml xml = JUnitXml() xml.add_testsuite(suite) xml.write('junit.xml') Read and manipulate exiting JUnit/xUnit XML files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You have some existing junit.xml files, and you want to modify the content. .. code-block:: python from junitparser import JUnitXml xml = JUnitXml.fromfile('/path/to/junit.xml') for suite in xml: # handle suites for case in suite: # handle cases xml.write() # Writes back to file Merge XML files ~~~~~~~~~~~~~~~ You have two or more XML files, and you want to merge them into one. .. code-block:: python from junitparser import JUnitXml xml1 = JUnitXml.fromfile('/path/to/junit1.xml') xml2 = JUnitXml.fromfile('/path/to/junit2.xml') newxml = xml1 + xml2 # Alternatively, merge in place xml1 += xml2 Note that it won't check for duplicate entries. You need to deal with them on your own. Create XML with custom attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You want to use an attribute that is not supported by default. .. code-block:: python from junitparser import TestCase, Attr, IntAttr, FloatAttr # Add the custom attribute TestCase.id = IntAttr('id') TestCase.rate = FloatAttr('rate') TestCase.custom = Attr('custom') case = TestCase() case.id = 123 case.rate = 0.95 case.custom = 'foobar' Handling XML with custom element ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There may be once in 1000 years you want to it this way, but anyways. Suppose you want to add element CustomElement to TestCase. .. code-block:: python from junitparser import Element, Attr, TestSuite # Create the new element by subclassing Element, # and add custom attributes to it. class CustomElement(Element): _tag = 'custom' foo = Attr() bar = Attr() testcase = TestCase() custom = CustomElement() testcase.append(custom) # To find a single sub-element: testcase.child(CustomElement) # To iterate over custom elements: for custom in testcase.iterchildren(CustomElement): ... # Do things with custom element Handling custom XML attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Say you have some data stored in the XML as custom attributes and you want to read them out: .. code-block:: python from junitparser import Element, Attr, TestSuite # Create the new element by subclassing Element or one of its child class, # and add custom attributes to it. class MyTestCase(TestCase): foo = Attr() xml = JUnitXml.fromfile('/path/to/junit.xml') for suite in xml: # handle suites for case in suite: my_case = MyTestCase.fromelem(case) print(my_case.foo) Command Line ------------ .. code-block:: shell $ junitparser --help usage: junitparser [-h] [-v] {merge} ... Junitparser CLI helper. positional arguments: {merge} command merge Merge Junit XML format reports with junitparser. optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit .. code-block:: shell $ junitparser merge --help usage: junitparser merge [-h] paths [paths ...] output positional arguments: paths Original XML path(s). output Merged XML Path. optional arguments: -h, --help show this help message and exit Test ---- You can run the cases directly:: python test.py Or 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! junitparser-1.4.1/.codacy.yml0000644000175000017500000000005313601064053014500 0ustar bagebageexclude_paths: - 'test.py' - 'setup.py'junitparser-1.4.1/CHANGELOG.md0000644000175000017500000000203413601064053014247 0ustar bagebage# Changelog ## [1.4.1] - 2019-12-26 ### Fixed - A conditional statement error. Thanks to @dries007 ## [1.4.0] - 2019-10-28 ### Fixed - Retain suite name when merging test suites. Thanks to @alde - Add skipped member to JUnitXml. Thanks to @arichardson ## [1.3.5] - 2019-09-23 ### Fixed - Prevented an exception when test result is None. Thanks to @patbro ## [1.3.4] - 2019-09-15 ### Fixed - Performance improvement for file merging. Thanks to @arichardson ## [1.3.3] - 2019-09-02 ### Fixed - Ensure htmlentities are used in attributes. Thanks to @alde ## [1.3.1] - 2019-02-11 ### Fixed - Install with --no-binary ## [1.3.0] - 2019-02-11 ### Fixed - Merging test files doesn't merge test counts. Thanks to @andydawkins ## [1.2.0] ### Added - Support for reading custom attributes and elements. Thanks to @arewm ## [1.1.0] ### Added - a command to merge xml files. Thanks to @imsuwj ## [1.0.0] ### Added - Python 2 support. Thanks to @SteinHeselmans ## [0.9.0] ### Changed * Supports xmls with ``testcase`` as root node. * First beta release.junitparser-1.4.1/junitparser/0000755000175000017500000000000013601064053015005 5ustar bagebagejunitparser-1.4.1/junitparser/junitparser.py0000644000175000017500000004653413601064053017741 0ustar bagebage""" junitparser is a JUnit/xUnit Result XML Parser. Use it to parse and manipulate existing Result XML files, or create new JUnit/xUnit result XMLs from scratch. :copyright: (c) 2019 by Joel Wang. :license: Apache2, see LICENSE for more details. """ from __future__ import with_statement from __future__ import absolute_import from __future__ import unicode_literals from future.utils import with_metaclass from builtins import object from io import open try: from html import escape # python 3.x except ImportError: from cgi import escape # python 2.x try: import itertools.izip as zip except ImportError: pass try: from lxml import etree except ImportError: from xml.etree import ElementTree as etree from copy import deepcopy try: type(unicode) except NameError: unicode = str def write_xml(obj, filepath=None, pretty=False): tree = etree.ElementTree(obj._elem) if filepath is None: filepath = obj.filepath if filepath is None: raise JUnitXmlError("Missing filepath argument.") if pretty: from xml.dom.minidom import parseString text = etree.tostring(obj._elem) xml = parseString(text) with open(filepath, "wb") as xmlfile: xmlfile.write(xml.toprettyxml(encoding="utf-8")) else: tree.write(filepath, encoding="utf-8", xml_declaration=True) class JUnitXmlError(Exception): """Exception for JUnit XML related errors.""" class Attr(object): """An attribute for an XML element. By default they are all string values. To support different value types, inherit this class and define your own methods. Also see: :class:`InitAttr`, :class:`FloatAttr`. """ def __init__(self, name=None): self.name = name def __get__(self, instance, cls): """Gets value from attribute, return None if attribute doesn't exist.""" value = instance._elem.attrib.get(self.name) if value is not None: return escape(value) return value def __set__(self, instance, value): """Sets XML element attribute.""" if value is not None: instance._elem.attrib[self.name] = unicode(value) class IntAttr(Attr): """An integer attribute for an XML element. This class is used internally for counting test cases, but you could use it for any specific purpose. """ def __get__(self, instance, cls): result = super(IntAttr, self).__get__(instance, cls) if result is None and ( isinstance(instance, JUnitXml) or isinstance(instance, TestSuite) ): instance.update_statistics() result = super(IntAttr, self).__get__(instance, cls) return int(result) if result else None def __set__(self, instance, value): if not isinstance(value, int): raise TypeError("Expected integer value.") super(IntAttr, self).__set__(instance, value) class FloatAttr(Attr): """A float attribute for an XML element. This class is used internally for counting test durations, but you could use it for any specific purpose. """ def __get__(self, instance, cls): result = super(FloatAttr, self).__get__(instance, cls) if result is None and ( isinstance(instance, JUnitXml) or isinstance(instance, TestSuite) ): instance.update_statistics() result = super(FloatAttr, self).__get__(instance, cls) return float(result) if result else None def __set__(self, instance, value): if not (isinstance(value, float) or isinstance(value, int)): raise TypeError("Expected float value.") super(FloatAttr, self).__set__(instance, value) def attributed(cls): """Decorator to read XML element attribute name from class attribute.""" for key, value in vars(cls).items(): if isinstance(value, Attr): value.name = key return cls class junitxml(type): """Metaclass to decorate the xml class""" def __new__(meta, name, bases, methods): cls = super(junitxml, meta).__new__(meta, name, bases, methods) cls = attributed(cls) return cls class Element(with_metaclass(junitxml, object)): """Base class for all Junit XML elements.""" def __init__(self, name=None): self._elem = etree.Element(name) def __hash__(self): return hash(etree.tostring(self._elem)) def __repr__(self): tag = self._elem.tag keys = sorted(self._elem.attrib.keys()) if keys: attrs_str = " ".join( ['%s="%s"' % (key, self._elem.attrib[key]) for key in keys] ) return """""" % (tag, attrs_str) else: return """""" % tag def append(self, sub_elem): """Adds the element subelement to the end of this elements internal list of subelements. """ self._elem.append(sub_elem._elem) @classmethod def fromstring(cls, text): """Construct Junit objects from a XML string.""" instance = cls() instance._elem = etree.fromstring(text) return instance @classmethod def fromelem(cls, elem): """Constructs Junit objects from an elementTree element.""" if elem is None: return instance = cls() if isinstance(elem, Element): instance._elem = elem._elem else: instance._elem = elem return instance def iterchildren(self, Child): """Iterate through specified Child type elements.""" elems = self._elem.iterfind(Child._tag) for elem in elems: yield Child.fromelem(elem) def child(self, Child): """Find a single child of specified Child type.""" elem = self._elem.find(Child._tag) return Child.fromelem(elem) def remove(self, sub_elem): """Remove a sub element.""" for elem in self._elem.iterfind(sub_elem._tag): child = sub_elem.__class__.fromelem(elem) if child == sub_elem: self._elem.remove(child._elem) def tostring(self): """Converts element to XML string.""" return etree.tostring(self._elem, encoding="utf-8") class JUnitXml(Element): """The JUnitXml root object. It may contains a :class:`TestSuites` or a :class:`TestSuite`. Attributes: name: test suite name if it only contains one test suite time: time consumed by the test suites tests: total number of tests failures: number of failed cases errors: number of cases with errors skipped: number of skipped cases """ _tag = "testsuites" name = Attr() time = FloatAttr() tests = IntAttr() failures = IntAttr() errors = IntAttr() skipped = IntAttr() def __init__(self, name=None): super(JUnitXml, self).__init__(self._tag) self.filepath = None self.name = name def __iter__(self): return super(JUnitXml, self).iterchildren(TestSuite) def __len__(self): return len(list(self.__iter__())) def __add__(self, other): result = JUnitXml() for suite in self: result.add_testsuite(suite) for suite in other: result.add_testsuite(suite) return result def __iadd__(self, other): if other._elem.tag == "testsuites": for suite in other: self.add_testsuite(suite) elif other._elem.tag == "testsuite": suite = TestSuite(name=other.name) for case in other: suite._add_testcase_no_update_stats(case) self.add_testsuite(suite) self.update_statistics() return self def add_testsuite(self, suite): """Add a test suite""" self.append(suite) def update_statistics(self): """Update test count, time, etc.""" time = 0 tests = failures = errors = skipped = 0 for suite in self: suite.update_statistics() tests += suite.tests failures += suite.failures errors += suite.errors skipped += suite.skipped time += suite.time self.tests = tests self.failures = failures self.errors = errors self.skipped = skipped self.time = time @classmethod def fromfile(cls, filepath): """Initiate the object from a report file.""" tree = etree.parse(filepath) root_elem = tree.getroot() if root_elem.tag == "testsuites": instance = cls() elif root_elem.tag == "testsuite": instance = TestSuite() else: raise JUnitXmlError("Invalid format.") instance._elem = root_elem instance.filepath = filepath return instance def write(self, filepath=None, pretty=False): """Write the object into a junit xml file. If `file_path` is not specified, it will write to the original file. If `pretty` is True, the result file will be more human friendly. """ write_xml(self, filepath=filepath, pretty=pretty) class TestSuite(Element): """The object. Attributes: name: test suite name hostname: name of the test machine time: time concumed by the test suite timestamp: when the test was run tests: total number of tests failures: number of failed tests errors: number of cases with errors skipped: number of skipped cases """ _tag = "testsuite" name = Attr() hostname = Attr() time = FloatAttr() timestamp = Attr() tests = IntAttr() failures = IntAttr() errors = IntAttr() skipped = IntAttr() def __init__(self, name=None): super(TestSuite, self).__init__(self._tag) self.name = name self.filepath = None def __iter__(self): return super(TestSuite, self).iterchildren(TestCase) def __len__(self): return len(list(self.__iter__())) def __eq__(self, other): def props_eq(props1, props2): props1 = list(props1) props2 = list(props2) if len(props1) != len(props2): return False props1.sort(key=lambda x: x.name) props2.sort(key=lambda x: x.name) zipped = zip(props1, props2) return all([x == y for x, y in zipped]) return ( self.name == other.name and self.hostname == other.hostname and self.timestamp == other.timestamp ) and props_eq(self.properties(), other.properties()) def __add__(self, other): if self == other: # Merge the two suites result = deepcopy(self) for case in other: result._add_testcase_no_update_stats(case) for suite in other.testsuites(): result.add_testsuite(suite) result.update_statistics() else: # Create a new test result containing two suites result = JUnitXml() result.add_testsuite(self) result.add_testsuite(other) return result def __iadd__(self, other): if self == other: for case in other: self._add_testcase_no_update_stats(case) for suite in other.testsuites(): 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 def remove_testcase(self, testcase): """Removes a test case from the suite.""" for case in self: if case == testcase: super(TestSuite, self).remove(case) self.update_statistics() def update_statistics(self): """Updates test count and test time.""" tests = errors = failures = skipped = 0 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 self.tests = tests self.errors = errors self.failures = failures self.skipped = skipped self.time = time def add_property(self, name, value): """Adds a property to the testsuite. See :class:`Property` and :class:`Properties` """ props = self.child(Properties) if props is None: props = Properties() self.append(props) prop = Property(name, value) props.add_property(prop) def add_testcase(self, testcase): """Adds a testcase to the suite.""" self.append(testcase) self.update_statistics() def _add_testcase_no_update_stats(self, testcase): """ Adds a testcase to the suite (without updating stats). For internal use only to avoid quadratic behaviour in merge. """ self.append(testcase) def add_testsuite(self, suite): """Adds a testsuite inside current testsuite.""" self.append(suite) def properties(self): """Iterates through all properties.""" props = self.child(Properties) if props is None: return for prop in props: yield prop def remove_property(self, property_): """Removes a property.""" props = self.child(Properties) if props is None: return for prop in props: if prop == property_: props.remove(property_) def testsuites(self): """Iterates through all testsuites.""" for suite in self.iterchildren(TestSuite): yield suite def write(self, filepath=None, pretty=False): write_xml(self, filepath=filepath, pretty=pretty) class Properties(Element): """A list of properties inside a test suite. See :class:`Property` """ _tag = "properties" def __init__(self): super(Properties, self).__init__(self._tag) def add_property(self, property_): self.append(property_) def __iter__(self): return super(Properties, self).iterchildren(Property) def __eq__(self, other): p1 = list(self) p2 = list(other) p1.sort() p2.sort() if len(p1) != len(p2): return False for e1, e2 in zip(p1, p2): if e1 != e2: return False return True class Property(Element): """A key/value pare that's stored in the test suite. Use it to store anything you find interesting or useful. Attributes: name: the property name value: the property value """ _tag = "property" name = Attr() value = Attr() def __init__(self, name=None, value=None): super(Property, self).__init__(self._tag) self.name = name self.value = value def __eq__(self, other): return self.name == other.name and self.value == other.value def __ne__(self, other): return not self == other def __lt__(self, other): """Supports sort() for properties.""" return self.name > other.name class Result(Element): """Base class for test result. Attributes: message: result as message string type: message type """ _tag = None message = Attr() type = Attr() def __init__(self, message=None, type_=None): super(Result, self).__init__(self._tag) if message: self.message = message if type_: self.type = type_ def __eq__(self, other): return ( self._tag == other._tag and self.type == other.type and self.message == other.message ) class Skipped(Result): """Test result when the case is skipped.""" _tag = "skipped" def __eq__(self, other): return super(Skipped, self).__eq__(other) class Failure(Result): """Test result when the case failed.""" _tag = "failure" def __eq__(self, other): return super(Failure, self).__eq__(other) class Error(Result): """Test result when the case has errors during execution.""" _tag = "error" def __eq__(self, other): return super(Error, self).__eq__(other) POSSIBLE_RESULTS = {Failure, Error, Skipped} class TestCase(Element): """Object to store a testcase and its result. Attributes: name: case name classname: the parent class of the case time: how much time is consumed by the test Properties: result: Failure, Skipped, or Error system_out: stdout system_err: stderr """ _tag = "testcase" name = Attr() classname = Attr() time = FloatAttr() def __init__(self, name=None): super(TestCase, self).__init__(self._tag) self.name = name def __hash__(self): return super(TestCase, self).__hash__() 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.""" 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] @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) @property def system_out(self): """stdout.""" elem = self.child(SystemOut) if elem is not None: return elem.text return None @system_out.setter def system_out(self, value): out = self.child(SystemOut) if out is not None: out.text = value else: out = SystemOut(value) self.append(out) @property def system_err(self): """stderr.""" elem = self.child(SystemErr) if elem is not None: return elem.text return None @system_err.setter def system_err(self, value): err = self.child(SystemErr) if err is not None: err.text = value else: err = SystemErr(value) self.append(err) class System(Element): """Parent class for SystemOut and SystemErr. Attributes: text: the output message """ _tag = "" def __init__(self, content=None): super(System, self).__init__(self._tag) self.text = content @property def text(self): return self._elem.text @text.setter def text(self, value): self._elem.text = value class SystemOut(System): _tag = "system-out" class SystemErr(System): _tag = "system-err" junitparser-1.4.1/junitparser/__init__.py0000644000175000017500000000032113601064053017112 0ustar bagebagefrom .junitparser import ( JUnitXmlError, Attr, Element, JUnitXml, TestSuite, Property, Skipped, Failure, Error, TestCase, Properties, IntAttr, FloatAttr, ) junitparser-1.4.1/pylintrc0000644000175000017500000000000013601064053014214 0ustar bagebagejunitparser-1.4.1/requirements.txt0000644000175000017500000000000713601064053015720 0ustar bagebagefuture junitparser-1.4.1/setup.cfg0000644000175000017500000000003113601064053014252 0ustar bagebage[bdist_wheel] universal=1junitparser-1.4.1/setup.py0000644000175000017500000000235513601064053014156 0ustar bagebagefrom setuptools import setup, find_packages import os import sys def read(fname): try: return open(os.path.join(os.path.dirname(__file__), fname)).read() except IOError: return '' setup(name='junitparser', version="1.4.1", description='Manipulates JUnit/xUnit Result XML files', long_description=read('README.rst'), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Topic :: Text Processing', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', ], url='https://github.com/gastlygem/junitparser', author='Joel Wang', author_email='gastlygem@gmail.com', license='Apache 2.0', install_requires=['future'], keywords='junit xunit xml parser', packages=find_packages(), scripts=['bin/junitparser'], zip_safe=False) junitparser-1.4.1/test.py0000644000175000017500000005732413601064053014003 0ustar bagebage#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import with_statement from __future__ import absolute_import from __future__ import unicode_literals import os import unittest from copy import deepcopy from junitparser import (TestCase, TestSuite, Skipped, Failure, Error, Attr, JUnitXmlError, JUnitXml, Property, Properties, IntAttr, FloatAttr) from xml.etree import ElementTree as etree from io import open try: import itertools.izip as zip except ImportError: pass class Test_MergeSuiteCounts(unittest.TestCase): def test_merge_test_count(self): text1 = """ """ test_suite1 = TestSuite.fromstring(text1) text2 = """ """ test_suite2 = TestSuite.fromstring(text2) combined_suites = JUnitXml() combined_suites += test_suite1 combined_suites += test_suite2 self.assertEqual(combined_suites.tests, 4) self.assertEqual(combined_suites.failures, 1) self.assertEqual(combined_suites.skipped, 1) class Test_JunitXml(unittest.TestCase): def test_fromstring(self): text = """ """ result = JUnitXml.fromstring(text) self.assertEqual(result.time, 0) self.assertEqual(len(result), 2) def test_add_suite(self): suite1 = TestSuite() suite2 = TestSuite() result = JUnitXml() result.add_testsuite(suite1) result.add_testsuite(suite2) self.assertEqual(len(result), 2) def test_construct_xml(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = 'case1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) self.assertEqual(result._elem.tag, 'testsuites') suite = result._elem.findall('testsuite') self.assertEqual(len(suite), 1) self.assertEqual(suite[0].attrib['name'], 'suite1') case = suite[0].findall('testcase') self.assertEqual(len(case), 1) self.assertEqual(case[0].attrib['name'], 'case1') def test_add(self): result1 = JUnitXml() suite1 = TestSuite() result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite() result2.add_testsuite(suite2) result3 = result1 + result2 self.assertEqual(len(result3), 2) def test_iadd(self): result1 = JUnitXml() suite1 = TestSuite() result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite() result2.add_testsuite(suite2) result1 += result2 self.assertEqual(len(result1), 2) def test_add_two_same_suites(self): suite1 = TestSuite() case1 = TestCase(name='case1') suite1.add_testcase(case1) suite2 = TestSuite() case2 = TestCase(name='case2') suite2.add_testcase(case2) suite3 = TestSuite() suite2.add_testsuite(suite3) result = suite1 + suite2 self.assertIsInstance(result, TestSuite) self.assertEqual(len(list(iter(result))), 2) self.assertEqual(len(list(iter(result.testsuites()))), 1) def test_iadd_two_same_suites(self): suite1 = TestSuite() case1 = TestCase(name='case1') suite1.add_testcase(case1) suite2 = TestSuite() case2 = TestCase(name='case2') suite2.add_testcase(case2) suite3 = TestSuite() suite2.add_testsuite(suite3) suite1 += suite2 self.assertIsInstance(suite1, TestSuite) self.assertEqual(len(list(iter(suite1))), 2) self.assertEqual(len(list(iter(suite1.testsuites()))), 1) def test_add_two_different_suites(self): suite1 = TestSuite(name='suite1') case1 = TestCase(name='case1') suite1.add_testcase(case1) suite2 = TestSuite(name='suite2') case2 = TestCase(name='case2') suite2.add_testcase(case2) result = suite1 + suite2 self.assertIsInstance(result, JUnitXml) self.assertEqual(len(list(iter(result))), 2) def test_iadd_two_different_suites(self): suite1 = TestSuite(name='suite1') case1 = TestCase(name='case1') suite1.add_testcase(case1) suite2 = TestSuite(name='suite2') case2 = TestCase(name='case2') suite2.add_testcase(case2) suite1 += suite2 self.assertIsInstance(suite1, JUnitXml) self.assertEqual(len(list(iter(suite1))), 2) def test_xml_statistics(self): result1 = JUnitXml() suite1 = TestSuite() result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite() result2.add_testsuite(suite2) result3 = result1 + result2 result3.update_statistics() class Test_RealFile(unittest.TestCase): def setUp(self): import tempfile self.tmp = tempfile.mktemp(suffix='.xml') def tearDown(self): if os.path.exists(self.tmp): os.remove(self.tmp) def test_fromfile(self): text = """ Assertion failed """ with open(self.tmp, 'w') as f: f.write(text) xml = JUnitXml.fromfile(self.tmp) suite1, suite2 = list(iter(xml)) self.assertEqual(len(list(suite1.properties())), 0) self.assertEqual(len(list(suite2.properties())), 3) 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) def test_fromfile_without_testsuites_tag(self): text = """ Assertion failed """ with open(self.tmp, 'w') as f: f.write(text) xml = JUnitXml.fromfile(self.tmp) cases = list(iter(xml)) properties = list(iter(xml.properties())) self.assertEqual(len(properties), 3) 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) def test_write_xml_withouth_testsuite_tag(self): suite = TestSuite() suite.name = 'suite1' case = TestCase() case.name = 'case1' suite.add_testcase(case) suite.write(self.tmp) with open(self.tmp) as f: text = f.read() self.assertIn('suite1', text) self.assertIn('case1', text) def test_file_is_not_xml(self): text = "Not really an xml file" with open(self.tmp, 'w') as f: f.write(text) with self.assertRaises(Exception): xml = JUnitXml.fromfile(self.tmp) # Raises lxml.etree.XMLSyntaxError def test_illegal_xml_file(self): text = "" with open(self.tmp, 'w') as f: f.write(text) with self.assertRaises(JUnitXmlError): xml = JUnitXml.fromfile(self.tmp) def test_write(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = 'case1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) result.write(self.tmp) with open(self.tmp) as f: text = f.read() self.assertIn('suite1', text) self.assertIn('case1', text) def test_write_noarg(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = 'case1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) with self.assertRaises(JUnitXmlError): result.write() def test_write_nonascii(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = '用例1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) result.write(self.tmp) with open(self.tmp, encoding='utf-8') as f: text = f.read() self.assertIn('suite1', text) self.assertIn('用例1', text) def test_read_written_xml(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = '用例1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) result.write(self.tmp) xml = JUnitXml.fromfile(self.tmp) suite = next(iter(xml)) case = next(iter(suite)) self.assertEqual(case.name, '用例1') def test_multi_results_in_case(self): # Has to be a binary string to include xml declarations. text = b""" Assertion failed """ xml = JUnitXml.fromstring(text) suite = next(iter(xml)) case = next(iter(suite)) with self.assertRaises(JUnitXmlError): result = case.result def test_write_pretty(self): suite1 = TestSuite() suite1.name = 'suite1' case1 = TestCase() case1.name = '用例1' suite1.add_testcase(case1) result = JUnitXml() result.add_testsuite(suite1) result.write(self.tmp, pretty=True) xml = JUnitXml.fromfile(self.tmp) suite = next(iter(xml)) case = next(iter(suite)) self.assertEqual(case.name, '用例1') class Test_TestSuite(unittest.TestCase): def test_fromstring(self): text = """ """ suite = TestSuite.fromstring(text) self.assertEqual(suite.time, 1.32) suite.update_statistics() self.assertEqual(suite.name, 'suitename') self.assertEqual(suite.tests, 1) def test_props_fromstring(self): text = """ """ suite = TestSuite.fromstring(text) for prop in suite.properties(): self.assertEqual(prop.name, 'name1') self.assertEqual(prop.value, 'value1') def test_quoted_attr(self): text = """ """ suite = TestSuite.fromstring(text) self.assertEqual(suite.name, 'suitename with "quotes"') def test_combining_testsuite_should_keep_name(self): text1 = """ """ test_suite1 = TestSuite.fromstring(text1) text2 = """ """ test_suite2 = TestSuite.fromstring(text2) combined_suites = JUnitXml() combined_suites += test_suite1 combined_suites += test_suite2 self.assertEqual([s.name for s in combined_suites], ["suitename1", "suitename2"]) def test_len(self): text = """ """ suite = TestSuite.fromstring(text) self.assertEqual(len(suite), 2) def test_add_case(self): suite = TestSuite() self.assertEqual(suite.tests, 0) case1 = TestCase() case2 = TestCase() case2.result = Failure() case3 = TestCase() case3.result = Error() case4 = TestCase() case4.result = Skipped() suite.add_testcase(case1) suite.add_testcase(case2) suite.add_testcase(case3) suite.add_testcase(case4) suite.update_statistics() self.assertEqual(suite.tests, 4) self.assertEqual(suite.failures, 1) self.assertEqual(suite.errors, 1) self.assertEqual(suite.skipped, 1) def test_case_count(self): suite = TestSuite() case1 = TestCase() suite.add_testcase(case1) self.assertEqual(suite.tests, 1) self.assertEqual(suite.failures, 0) def test_add_property(self): suite = TestSuite() suite.add_property('name1', 'value1') res_prop = next(suite.properties()) self.assertEqual(res_prop.name, 'name1') self.assertEqual(res_prop.value, 'value1') def test_remove_case(self): suite = TestSuite() case1 = TestCase() case1.name = 'test1' case2 = TestCase() case2.name = 'test2' suite.add_testcase(case1) suite.add_testcase(case2) suite.remove_testcase(case1) self.assertEqual(len(suite), 1) def test_remove_property(self): suite = TestSuite() suite.add_property('name1', 'value1') suite.add_property('name2', 'value2') suite.add_property('name3', 'value3') for prop in suite.properties(): if prop.name == 'name2': suite.remove_property(prop) self.assertEqual(len(list(suite.properties())), 2) def test_remove_property_from_none(self): suite = TestSuite() suite.remove_property(Property('key', 'value')) # Nothing should happen def test_suite_in_suite(self): suite = TestSuite('parent') childsuite = TestSuite('child') suite.add_testsuite(childsuite) self.assertEqual(len(list(suite.testsuites())), 1) def test_case_time(self): suite = TestSuite() case1 = TestCase() case1.name = 'test1' case1.time = 15 suite.add_testcase(case1) suite.update_statistics() self.assertEqual(suite.time, 15) def test_wrong_attr_type(self): suite = TestSuite() with self.assertRaises(TypeError): suite.time = 'abc' with self.assertRaises(TypeError): suite.tests = 10.5 def test_suite_eq(self): suite = TestSuite() suite.add_property('name1', 'value1') suite2 = deepcopy(suite) self.assertEqual(suite, suite2) def test_suite_ne(self): suite = TestSuite() suite.add_property('name1', 'value1') suite2 = deepcopy(suite) suite2.add_property('name2', 'value2') self.assertNotEqual(suite, suite2) class Test_TestCase(unittest.TestCase): def test_fromstring(self): text = """ System out System err """ case = TestCase.fromstring(text) self.assertEqual(case.name, "testname") self.assertIsInstance(case.result, Failure) self.assertEqual(case.system_out, "System out") self.assertEqual(case.system_err, "System err") def test_illegal_xml_multi_results(self): text = """ """ case = TestCase.fromstring(text) self.assertRaises(JUnitXmlError) def test_case_attributes(self): case = TestCase() case.name = 'testname' case.classname = 'testclassname' case.time = 15.123 case.result = Skipped() self.assertEqual(case.name, 'testname') self.assertEqual(case.classname, 'testclassname') self.assertEqual(case.time, 15.123) self.assertIsInstance(case.result, Skipped) def test_case_output(self): case = TestCase() case.system_err = 'error message' case.system_out = 'out message' self.assertEqual(case.system_err, 'error message') self.assertEqual(case.system_out, 'out message') case.system_err = 'error2' case.system_out = 'out2' self.assertEqual(case.system_err, 'error2') self.assertEqual(case.system_out, 'out2') def test_set_multiple_results(self): case = TestCase() case.result = Skipped() case.result = Failure() self.assertIsInstance(case.result, Failure) def test_monkypatch(self): TestCase.id = Attr('id') case = TestCase() case.id = "100" self.assertEqual(case.id, "100") def test_equal(self): case = TestCase() case.name = 'test1' case2 = TestCase() case2.name = 'test1' self.assertEqual(case, case2) def test_not_equal(self): case = TestCase() case.name = 'test1' case2 = TestCase() case2.name = 'test2' self.assertNotEqual(case, case2) def test_from_elem(self): elem = etree.Element('testcase', name='case1') case = TestCase.fromelem(elem) self.assertEqual(case.name, 'case1') def test_from_junit_elem(self): case = TestCase() case.name = 'test1' class TestOtherCase(TestCase): _tag = 'TestOtherCase' assertions = Attr() other_case = TestOtherCase.fromelem(case) self.assertEqual(case.name, other_case.name) self.assertRaises(AttributeError, lambda: case.assertions) other_case.assertions = 20 self.assertEqual(other_case.assertions, '20') def test_to_string(self): case = TestCase() case.name = 'test1' case_str = case.tostring() self.assertIn(b'test1', case_str) def test_to_nonascii_string(self): case = TestCase() case.name = '测试1' case.result = Failure('失败', '类型') case_str = case.tostring() self.assertIn('测试1', case_str.decode('utf-8')) self.assertIn('失败', case_str.decode('utf-8')) self.assertIn('类型', case_str.decode('utf-8')) def test_system_out(self): case = TestCase() case.name = 'case1' self.assertIsNone(case.system_out) case.system_out = "output" self.assertEqual(case.system_out, "output") def test_system_err(self): case = TestCase() case.name = 'case1' self.assertIsNone(case.system_err) case.system_err = "error" self.assertEqual(case.system_err, "error") def test_result_eq(self): # TODO: Weird, need to think of a better API self.assertEqual(Failure('A'), Failure('A')) self.assertNotEqual(Skipped('B'), Skipped('A')) self.assertNotEqual(Error('C'), Error('B')) class Test_Properties(unittest.TestCase): def test_property_repr1(self): prop1 = Property('prop1', '1') self.assertEqual(prop1.__repr__(), '') def test_property_repr2(self): prop1 = TestSuite() self.assertEqual(prop1.__repr__(), '') def test_property_eq(self): prop1 = Property('prop1', '1') prop2 = Property('prop1', '1') self.assertEqual(prop1, prop2) def test_property_ne(self): prop1 = Property('prop1', '1') prop2 = Property('prop1', '2') self.assertNotEqual(prop1, prop2) def test_properties_eq(self): prop1 = Property('prop1', '1') prop2 = Property('prop1', '2') # Note: an attribute can only be used at one place. prop3 = deepcopy(prop1) prop4 = deepcopy(prop2) props1 = Properties() props1.add_property(prop1) props1.add_property(prop2) props2 = Properties() props2.add_property(prop3) props2.add_property(prop4) self.assertEqual(props1, props2) def test_properties_ne(self): prop1 = Property('prop1', '1') prop2 = Property('prop1', '2') prop3 = deepcopy(prop1) prop4 = deepcopy(prop1) props1 = Properties() props1.add_property(prop1) props1.add_property(prop2) props2 = Properties() props2.add_property(prop3) props2.add_property(prop4) self.assertNotEqual(props1, props2) def test_properties_ne2(self): prop1 = Property('prop1', '1') prop2 = Property('prop1', '2') prop3 = deepcopy(prop1) props1 = Properties() props1.add_property(prop1) props1.add_property(prop2) props2 = Properties() props2.add_property(prop3) self.assertNotEqual(props1, props2) class Test_Attrs(unittest.TestCase): def test_attr(self): TestCase.text = Attr("text") TestCase.int = IntAttr("int") TestCase.float = FloatAttr("float") element = TestCase("foo") element.text = "foo" element.int = 10 element.float = 8.5 self.assertEqual(element.text, "foo") self.assertEqual(element.int, 10) self.assertEqual(element.float, 8.5) if __name__ == '__main__': unittest.main()