pax_global_header00006660000000000000000000000064141662027150014516gustar00rootroot0000000000000052 comment=0a67e8a30aa89f035c3721944e2f9227dbfb70ba junitparser-2.4.2/000077500000000000000000000000001416620271500140715ustar00rootroot00000000000000junitparser-2.4.2/.coveragerc000066400000000000000000000004431416620271500162130ustar00rootroot00000000000000[run] omit = # Don't complain if non-runnable code isn't run: */__main__.py [report] exclude_lines = # Have to re-enable the standard pragma \#\s*pragma: no cover # Don't complain if non-runnable code isn't run: ^if __name__ == ['"]__main__['"]:$ ^\s*if False: junitparser-2.4.2/.github/000077500000000000000000000000001416620271500154315ustar00rootroot00000000000000junitparser-2.4.2/.github/dependabot.yml000066400000000000000000000007651416620271500202710ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" junitparser-2.4.2/.github/workflows/000077500000000000000000000000001416620271500174665ustar00rootroot00000000000000junitparser-2.4.2/.github/workflows/build.yml000066400000000000000000000042611416620271500213130ustar00rootroot00000000000000name: build on: push: branches: ["master"] tags: - "*" pull_request: branches: ["master"] workflow_dispatch: jobs: build-test: runs-on: ubuntu-latest strategy: matrix: python-version: [2.7, 3.5] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Setup locales run: | sudo locale-gen en_US.UTF-8 sudo locale-gen de_DE.UTF-8 sudo update-locale - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Test run: | pytest publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python 3.x uses: actions/setup-python@v2 with: python-version: 3.x - name: Setup locales run: | sudo locale-gen en_US.UTF-8 sudo locale-gen de_DE.UTF-8 sudo update-locale - name: Install dependencies run: | python -m pip install --upgrade pip pip install lxml flake8 pytest coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test and coverage run: | coverage run -m pytest bash <(curl -s https://codecov.io/bash) - name: Build packages if: startsWith(github.ref, 'refs/tags') run: | pip install setuptools wheel python setup.py sdist bdist_wheel - name: Publish to pypi if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_password }} junitparser-2.4.2/.gitignore000066400000000000000000000014511416620271500160620ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # IDE Specific .vs/ *.pyproj *.sln .vscode/ .idea/ # Virtual env venv/ .venv/ .DS_Store .pytest_cache/ junitparser-2.4.2/CHANGELOG.md000066400000000000000000000056111416620271500157050ustar00rootroot00000000000000# Changelog ## [2.4.1] - 2022-01-08 ### Fixed - #83 `pip install --only-binary :all:` not working. ## [2.4.1] - 2021-12-31 ### Fixed - Parameter typo in the cli. Thanks to @petterssonandreas - ## [2.4.0] - 2021-12-30 This release addresses issues and PRs by @markgras. ### Fixed - Parameter typo in function `write_xml()`. - Properly closes file in `setup.py`. ### Enhancement - Use generators in stead of lists in a few occasions. ## [2.3.0] - 2021-11-20 ### Possibly Breaking - The time value now has a precision of 3 (#72). Thanks to @bryan-hunt. ## [2.2.0] - 2021-11-20 ### Fixed - Unescaping attribute values (#71). ## [2.1.1] - 2021-05-31 ### Fixed - CLI broken due to a quotation mark. ## [2.1.0] - 2021-05-30 ### Fixed - Should not have used default sys locale to parse numbers. Thanks to @EnricoMi ### Added - Merge parameter enhancement: output to console if output file name is set to "-" - Support testcase tags inside testcase tags. Thanks to @EnricoMi ## [2.0.0] - 2020-11-28 ### Breaking - `TestCase.result` is now a list instead of a single item. `Failure`, `Skip`, etc. are all treated as results. ### Added - `TestCase` constructor supports `time` and `classname` as params. - `Result` object supports `text` attribute. - Handles localized timestamps. Thanks to @ppalucha ## [1.6.3] - 2020-11-24 ### Fixed - `JunitXML.fromstring()` now handles various inputs. ## [1.6.2] - 2020-10-29 ### Changed - Exclude test file from package. Thanks to @Ishinomori ## [1.6.1] - 2020-10-29 ### Changed - Update licence and readme ## [1.6.0] - 2020-10-28 ### Added - Custom parser option for `fromfile` ## [1.5.1] - 2020-10-28 ### Fixed - #47 result error when running merge in cli ## [1.5.0] - 2020-10-26 ### Added - Runs with `python -m junitparser ...` Thanks to @jkowalleck - `junitparser merge --glob` also by @jkowalleck ## [1.4.2] - 2020-10-21 ### Fixed - command line versioning ## [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-2.4.2/LICENSE000066400000000000000000000011001416620271500150660ustar00rootroot00000000000000Copyright 2020 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-2.4.2/README.rst000066400000000000000000000150731416620271500155660ustar00rootroot00000000000000junitparser -- Pythonic JUnit/xUnit Result XML Parser ====================================================== .. image:: https://github.com/weiwei/junitparser/workflows/build/badge.svg?branch=master :target: https://github.com/weiwei/junitparser/actions .. image:: https://codecov.io/gh/weiwei/junitparser/branch/master/graph/badge.svg?token=UotlfRXNnK :target: https://codecov.io/gh/weiwei/junitparser 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. 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) Note on version 2 ----------------- Version 2 improved support for pytest result xml files by fixing a few issues, notably that there could be multiple or 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 ------------- :: 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', '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')] # 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 existing 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 It is also possible to use a custom parser. For example lxml provides a plethora of parsing options_. We can use them this way: .. code-block:: python from lxml.etree import XMLParser, parse from junitparser import JUnitXml def parse_func(file_path): xml_parser = XMLParser(huge_tree=True) return parse(file_path, xml_parser) xml = JUnitXml.fromfile('/path/to/junit.xml', parse_func) # process xml... .. _options: https://lxml.de/api/lxml.etree.XMLParser-class.html 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 TestCase, Attr, JUnitXml # 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] [--glob] paths [paths ...] output positional arguments: paths Original XML path(s). output Merged XML Path, setting to "-" will output console optional arguments: -h, --help show this help message and exit --glob Treat original XML path(s) as glob(s). Test ---- The tests are written with python `unittest`, to run them, use pytest:: pytest test.py Contribute ---------- PRs are welcome! junitparser-2.4.2/docs/000077500000000000000000000000001416620271500150215ustar00rootroot00000000000000junitparser-2.4.2/docs/Makefile000066400000000000000000000011041416620271500164550ustar00rootroot00000000000000# 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-2.4.2/docs/conf.py000066400000000000000000000122261416620271500163230ustar00rootroot00000000000000# -*- 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-2.4.2/docs/index.rst000066400000000000000000000006731416620271500166700ustar00rootroot00000000000000.. 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-2.4.2/docs/make.bat000066400000000000000000000014231416620271500164260ustar00rootroot00000000000000@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-2.4.2/junitparser/000077500000000000000000000000001416620271500164375ustar00rootroot00000000000000junitparser-2.4.2/junitparser/__init__.py000066400000000000000000000003441416620271500205510ustar00rootroot00000000000000from .junitparser import ( JUnitXmlError, Attr, Element, JUnitXml, TestSuite, Property, Skipped, Failure, Error, TestCase, Properties, IntAttr, FloatAttr, ) version = "2.4.2" junitparser-2.4.2/junitparser/__main__.py000066400000000000000000000001111416620271500205220ustar00rootroot00000000000000import sys from .cli import main sys.exit(main(prog_name=__package__)) junitparser-2.4.2/junitparser/cli.py000066400000000000000000000032061416620271500175610ustar00rootroot00000000000000from argparse import ArgumentParser from glob import iglob from itertools import chain from . import JUnitXml, version def merge(paths, output): """Merge xml report.""" result = JUnitXml() for path in paths: result += JUnitXml.fromfile(path) result.update_statistics() result.write(output, to_console=output == "-") return 0 def _parser(prog_name=None): # pragma: no cover """Create the CLI arg parser.""" parser = ArgumentParser(description="Junitparser CLI helper.", prog=prog_name) parser.add_argument( "-v", "--version", action="version", version="%(prog)s " + version ) command_parser = parser.add_subparsers(dest="command", help="command") command_parser.required = True # command: merge merge_parser = command_parser.add_parser( "merge", help="Merge Junit XML format reports with junitparser." ) merge_parser.add_argument( "--glob", help="Treat original XML path(s) as glob(s).", dest="paths_are_globs", action="store_true", default=False, ) merge_parser.add_argument("paths", nargs="+", help="Original XML path(s).") merge_parser.add_argument( "output", help='Merged XML Path, setting to "-" will output console' ) return parser def main(args=None, prog_name=None): """CLI's main runner.""" args = args or _parser(prog_name=prog_name).parse_args() if args.command == "merge": return merge( chain.from_iterable(iglob(path) for path in args.paths) if args.paths_are_globs else args.paths, args.output, ) return 255 junitparser-2.4.2/junitparser/junitparser.py000066400000000000000000000514221416620271500213630ustar00rootroot00000000000000""" 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 import itertools 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, to_console=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) # nosec content = xml.toprettyxml(encoding="utf-8") if to_console: print(content) else: with open(filepath, "wb") as xmlfile: xmlfile.write(content) else: if to_console: print( etree.tostring( obj._elem, encoding="utf-8", xml_declaration=True ).decode("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.""" return instance._elem.attrib.get(self.name) 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, 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, TestSuite)): instance.update_statistics() result = super(FloatAttr, self).__get__(instance, cls) return float(result.replace(",", "")) 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) 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) # nosec 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.""" for existing_suite in self: if existing_suite == suite: for case in suite: existing_suite._add_testcase_no_update_stats(case) return 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 = round(time, 3) @classmethod def fromstring(cls, text): """Construct Junit objects from a XML string.""" root_elem = etree.fromstring(text) # nosec if root_elem.tag == "testsuites": instance = cls() elif root_elem.tag == "testsuite": instance = TestSuite() else: raise JUnitXmlError("Invalid format.") instance._elem = root_elem return instance @classmethod def fromfile(cls, filepath, parse_func=None): """Initiate the object from a report file.""" if parse_func: tree = parse_func(filepath) else: tree = etree.parse(filepath) # nosec 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, to_console=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, to_console=to_console) 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 itertools.chain( super(TestSuite, self).iterchildren(TestCase), ( case for suite in super(TestSuite, self).iterchildren(TestSuite) for case in suite ), ) 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 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 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 self.skipped = skipped self.time = round(time, 3) 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 ) @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.""" _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, classname=None, time=None): super(TestCase, self).__init__(self._tag) 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): """A list of Failure, Skipped, or Error objects.""" results = [] 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 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): """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-2.4.2/pyproject.toml000066400000000000000000000001751416620271500170100ustar00rootroot00000000000000[build-system] requires = ["setuptools >=44.0", "wheel >=0.37", "future"] build-backend = "setuptools.build_meta:__legacy__" junitparser-2.4.2/requirements.txt000066400000000000000000000000071416620271500173520ustar00rootroot00000000000000future junitparser-2.4.2/setup.cfg000066400000000000000000000000311416620271500157040ustar00rootroot00000000000000[bdist_wheel] universal=1junitparser-2.4.2/setup.py000066400000000000000000000022041416620271500156010ustar00rootroot00000000000000from setuptools import setup, find_packages import os from junitparser import version def read(fname): try: with open(os.path.join(os.path.dirname(__file__), fname)) as f: return f.read() except IOError: return '' setup(name='junitparser', version=version, 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 :: 3', ], url='https://github.com/weiwei/junitparser', author='Weiwei Wang', author_email='gastlygem@gmail.com', license='Apache 2.0', install_requires=['future'], keywords='junit xunit xml parser', packages=find_packages(exclude=['tests']), entry_points={ 'console_scripts': [ 'junitparser=junitparser.cli:main' ] }, zip_safe=False) junitparser-2.4.2/tests/000077500000000000000000000000001416620271500152335ustar00rootroot00000000000000junitparser-2.4.2/tests/__init__.py000066400000000000000000000000001416620271500173320ustar00rootroot00000000000000junitparser-2.4.2/tests/data/000077500000000000000000000000001416620271500161445ustar00rootroot00000000000000junitparser-2.4.2/tests/data/jenkins.xml000066400000000000000000000027201416620271500203300ustar00rootroot00000000000000 Assertion failed junitparser-2.4.2/tests/data/no_suites_tag.xml000066400000000000000000000015561416620271500215400ustar00rootroot00000000000000 Assertion failed junitparser-2.4.2/tests/data/normal.xml000066400000000000000000000020261416620271500201560ustar00rootroot00000000000000 Assertion failed junitparser-2.4.2/tests/test_fromfile.py000066400000000000000000000164521416620271500204570ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import with_statement from __future__ import absolute_import from __future__ import unicode_literals import os import unittest from junitparser import ( TestCase, TestSuite, Skipped, Failure, Error, Attr, JUnitXmlError, JUnitXml, Property, Properties, IntAttr, FloatAttr, ) try: from lxml.etree import XMLParser, parse has_lxml = True except ImportError: has_lxml = False from io import open try: import itertools.izip as zip except ImportError: pass 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): xml = JUnitXml.fromfile( os.path.join(os.path.dirname(__file__), "data/normal.xml") ) 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) 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): def parse_func(file_path): xml_parser = XMLParser(huge_tree=True) return parse(file_path, xml_parser) xml = JUnitXml.fromfile( os.path.join(os.path.dirname(__file__), "data/normal.xml"), parse_func=parse_func, ) 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) 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( os.path.join(os.path.dirname(__file__), "data/no_suites_tag.xml") ) 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) self.assertIsInstance(cases[0].result[0], Failure) self.assertIsInstance(cases[1].result[0], Skipped) self.assertEqual(len(cases[2].result), 0) def test_fromfile_with_testsuite_in_testsuite(self): xml = JUnitXml.fromfile( os.path.join(os.path.dirname(__file__), "data/jenkins.xml") ) 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) direct_cases = list(suite2.iterchildren(TestCase)) self.assertEqual(len(direct_cases), 1) self.assertIsInstance(direct_cases[0].result[0], Failure) all_cases = list(suite2) self.assertIsInstance(all_cases[0].result[0], Failure) self.assertIsInstance(all_cases[1].result[0], Skipped) self.assertEqual(len(all_cases[2].result), 0) 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)) self.assertEqual(len(case.result), 2) 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") junitparser-2.4.2/tests/test_general.py000066400000000000000000000537541416620271500202770ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals from __future__ import with_statement import locale import unittest from copy import deepcopy from xml.etree import ElementTree as etree from junitparser import ( TestCase, TestSuite, Skipped, Failure, Error, Attr, JUnitXmlError, JUnitXml, Property, Properties, IntAttr, FloatAttr, ) 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) def test_merge_same_suite(self): text1 = """ """ test_suite1 = TestSuite.fromstring(text1) text2 = """ """ test_suite2 = TestSuite.fromstring(text2) combined_suites = JUnitXml() combined_suites += test_suite1 combined_suites += test_suite2 suites = list(suite for suite in combined_suites) self.assertEqual(len(suites), 1) 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_fromstring_no_testsuites(self): text = """ """ result = JUnitXml.fromstring(text) self.assertEqual(result.time, 0) self.assertEqual(len(result), 1) def test_fromstring_numbers_locale_insensitive(self): "Case relies on that LC_ALL is set in the console." for loc in ['', 'en_US.UTF-8', 'de_DE.UTF-8']: old_locale = locale.getlocale(locale.LC_NUMERIC) try: locale.setlocale(locale.LC_NUMERIC, loc) text = """ """ result = JUnitXml.fromstring(text) suite = list(iter(result))[0] self.assertEqual(suite.time, 1000.125, msg=loc) cases = list(iter(suite)) self.assertEqual(cases[0].time, 1000.025, msg=loc) self.assertEqual(cases[1].time, 0.1, msg=loc) finally: locale.setlocale(locale.LC_NUMERIC, old_locale) def test_fromstring_multiple_fails(self): text = """ test_x.py:11: unconditional skip @pytest.fixture(scope="module") def compb(): yield > raise PermissionError E PermissionError test_x.py:6: PermissionError """ result = JUnitXml.fromstring(text) self.assertEqual(result.errors, 1) self.assertEqual(result.skipped, 1) suite = list(iter(result))[0] cases = list(iter(suite)) self.assertEqual(len(cases[0].result), 0) self.assertEqual(len(cases[1].result), 2) text = cases[1].result[1].text self.assertTrue("@pytest.fixture" in text) def test_fromstring_invalid(self): text = """""" with self.assertRaises(Exception) as context: JUnitXml.fromstring(text) self.assertTrue(isinstance(context.exception, JUnitXmlError)) def test_add_suite(self): suite1 = TestSuite("suite1") suite2 = TestSuite("suite2") 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("suite1") result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite("suite2") result2.add_testsuite(suite2) result3 = result1 + result2 self.assertEqual(len(result3), 2) def test_add_same_suite(self): result1 = JUnitXml() suite1 = TestSuite() result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite() result2.add_testsuite(suite2) result3 = result1 + result2 self.assertEqual(len(result3), 1) def test_iadd(self): result1 = JUnitXml() suite1 = TestSuite("suite1") result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite("suite2") result2.add_testsuite(suite2) result1 += result2 self.assertEqual(len(result1), 2) def test_iadd_same_suite(self): result1 = JUnitXml() suite1 = TestSuite() result1.add_testsuite(suite1) result2 = JUnitXml() suite2 = TestSuite() result2.add_testsuite(suite2) result1 += result2 self.assertEqual(len(result1), 1) 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() self.assertEqual(result3.tests, 0) 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_case_fromstring(self): text = """ System out System err """ case = TestCase.fromstring(text) self.assertEqual(case.name, "testname") self.assertIsInstance(case.result[0], 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()] case.result[0].text = "woah skipped" self.assertEqual(case.name, "testname") self.assertEqual(case.classname, "testclassname") self.assertEqual(case.time, 15.123) self.assertIsInstance(case.result[0], Skipped) self.assertEqual(case.result[0].text, "woah skipped") def test_case_init_with_attributes(self): case = TestCase("testname", "testclassname", 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[0], 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_update_results(self): case = TestCase() case.result = [Skipped()] case.result = [Failure(), Skipped()] self.assertEqual(len(case.result), 2) 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")) def test_result_attrs(self): res1 = Failure("A") # NOTE: lxml gives spaceless result self.assertIn( res1.tostring(), [b'', 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()