pax_global_header00006660000000000000000000000064144215203770014517gustar00rootroot0000000000000052 comment=56480050fdc487590386909eec8d37af2c4497c8 junitparser-3.1.0/000077500000000000000000000000001442152037700140665ustar00rootroot00000000000000junitparser-3.1.0/.coveragerc000066400000000000000000000004431442152037700162100ustar00rootroot00000000000000[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-3.1.0/.github/000077500000000000000000000000001442152037700154265ustar00rootroot00000000000000junitparser-3.1.0/.github/dependabot.yml000066400000000000000000000007651442152037700202660ustar00rootroot00000000000000# 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-3.1.0/.github/workflows/000077500000000000000000000000001442152037700174635ustar00rootroot00000000000000junitparser-3.1.0/.github/workflows/build.yml000066400000000000000000000051231442152037700213060ustar00rootroot00000000000000name: build on: push: branches: ["master"] tags: - "*" pull_request: branches: ["master"] workflow_dispatch: jobs: build-test: # Python version < 3.7 requires ubuntu-20.04 runs-on: ubuntu-20.04 strategy: matrix: python-version: ["3.6", "3.x"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 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 - name: Test with lxml run: | pip install lxml pytest coverage-lint: needs: build-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.x uses: actions/setup-python@v4 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) package-publish: needs: coverage-lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python 3.x uses: actions/setup-python@v4 with: python-version: 3.x - name: Build packages run: | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 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@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} junitparser-3.1.0/.gitignore000066400000000000000000000014511442152037700160570ustar00rootroot00000000000000# 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-3.1.0/CHANGELOG.md000066400000000000000000000072421442152037700157040ustar00rootroot00000000000000# Changelog ## [3.1.0] - 2023-04-22 ### Added - Support for different schemas. - xunit2 flavor support - Type hints ## [3.0.0] - 2023-04-20 ### Breaking Python 2 is no longer supported. Version 2.x will keep supporting py2 and it will be maintained as long as possible, though no new features will be added. ## [2.8.0] - 2022-08-19 ### Added - `--suite-name` parameter for merging xmls with cli. Thanks to @yusijs ## [2.7.0] - 2022-06-25 ### Added - `fromroot` class method to create object from a root element. Thanks to @EnricoMi ## [2.6.0] - 2022-05-31 ### Added - `verify` subcommand. Thanks to @teake ## [2.5.0] - 2022-03-14 ### Added - Bulk add test cases. Thanks to @Goblenus - ## [2.4.3] - 2022-03-14 ### Fixed - Custom element not properly initiated according to the readme example. ## [2.4.2] - 2022-01-08 ### Fixed - Fix the package build for a specific install method ## [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-3.1.0/LICENSE000066400000000000000000000011001442152037700150630ustar00rootroot00000000000000Copyright 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-3.1.0/README.rst000066400000000000000000000204321442152037700155560ustar00rootroot00000000000000junitparser -- 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) #Bulk add cases to suite case3 = TestCase('case3') case4 = TestCase('case4') suite.add_testcases([case3, case4]) # 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. Schema Support ~~~~~~~~~~~~~~~ By default junitparser supports the schema of windyroad_, which is a relatively simple schema. .. _windyroad: https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd Junitparser also support extra schemas: .. code-block:: python from junitparser.xunit2 import JUnitParser, TestCase, TestSuite, \ RerunFailure # These classes are redefined to support extra properties and attributes # of the xunit2 schema. suite = TestSuite("mySuite") suite.system_err = "System err" # xunit2 specific property case = TestCase("myCase") rerun_failure = RerunFailure("Not found", "404") # case property rerun_failure.stack_trace = "Stack" rerun_failure.system_err = "E404" rerun_failure.system_out = "NOT FOUND" case.add_rerun_result(rerun_failure) Currently supported schemas including: - xunit2_, supported by pytest, Erlang/OTP, Maven Surefire, CppTest, etc. .. _xunit2: https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd PRs are welcome to support more schemas. 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. verify Return a non-zero exit code if one of the testcases failed or errored. 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). --suite-name SUITE_NAME Name added to . .. code-block:: shell $ junitparser verify --help usage: junitparser verify [-h] [--glob] paths [paths ...] positional arguments: paths XML path(s) of reports to verify. 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-3.1.0/docs/000077500000000000000000000000001442152037700150165ustar00rootroot00000000000000junitparser-3.1.0/docs/Makefile000066400000000000000000000011041442152037700164520ustar00rootroot00000000000000# 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-3.1.0/docs/conf.py000066400000000000000000000122711442152037700163200ustar00rootroot00000000000000# -*- 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-3.1.0/docs/index.rst000066400000000000000000000006731442152037700166650ustar00rootroot00000000000000.. 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-3.1.0/docs/make.bat000066400000000000000000000014231442152037700164230ustar00rootroot00000000000000@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-3.1.0/junitparser/000077500000000000000000000000001442152037700164345ustar00rootroot00000000000000junitparser-3.1.0/junitparser/__init__.py000066400000000000000000000003441442152037700205460ustar00rootroot00000000000000from .junitparser import ( JUnitXmlError, Attr, Element, JUnitXml, TestSuite, Property, Skipped, Failure, Error, TestCase, Properties, IntAttr, FloatAttr, ) version = "3.1.0" junitparser-3.1.0/junitparser/__main__.py000066400000000000000000000001111442152037700205170ustar00rootroot00000000000000import sys from .cli import main sys.exit(main(prog_name=__package__)) junitparser-3.1.0/junitparser/cli.py000066400000000000000000000060341442152037700175600ustar00rootroot00000000000000from argparse import ArgumentParser from glob import iglob from itertools import chain from . import JUnitXml, version def merge(paths, output, suite_name): """Merge xml report.""" result = JUnitXml() for path in paths: result += JUnitXml.fromfile(path) result.update_statistics() if suite_name: result.name = suite_name result.write(output, to_console=output == "-") return 0 def verify(paths): """Verify if none of the testcases failed or errored.""" # We could grab the number of failures and errors from the statistics of the root element # or from the test suites elements, but those attributes are not guaranteed to be present # or correct. So we'll just loop over all the testcases. for path in paths: xml = JUnitXml.fromfile(path) for suite in xml: for case in suite: if not case.is_passed and not case.is_skipped: return 1 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' ) merge_parser.add_argument( "--suite-name", help="Name added to .", ) # command: verify merge_parser = command_parser.add_parser( "verify", help="Return a non-zero exit code if one of the testcases failed or errored.", ) 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="XML path(s) of reports to verify." ) 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, args.suite_name, ) if args.command == "verify": return verify( chain.from_iterable(iglob(path) for path in args.paths) if args.paths_are_globs else args.paths ) return 255 junitparser-3.1.0/junitparser/junitparser.py000066400000000000000000000521631442152037700213630ustar00rootroot00000000000000""" 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. Reference schema: https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd This according to the document is Apache Ant's JUnit output. See documentation for other supported schemas. """ import itertools from copy import deepcopy try: from lxml import etree except ImportError: from xml.etree import ElementTree as etree 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: str = 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: str): """Sets XML element attribute.""" if value is not None: instance._elem.attrib[self.name] = str(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().__get__(instance, cls) if result is None and isinstance(instance, (JUnitXml, TestSuite)): instance.update_statistics() result = super().__get__(instance, cls) return int(result) if result else None def __set__(self, instance, value: int): if not isinstance(value, int): raise TypeError("Expected integer value.") super().__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().__get__(instance, cls) if result is None and isinstance(instance, (JUnitXml, TestSuite)): instance.update_statistics() result = super().__get__(instance, cls) return float(result.replace(",", "")) if result else None def __set__(self, instance, value: float): if not (isinstance(value, float) or isinstance(value, int)): raise TypeError("Expected float value.") super().__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(metaclass=junitxml): """Base class for all Junit XML elements.""" def __init__(self, name: str = None): if not name: name = self._tag 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) def extend(self, sub_elems): """Adds elements subelement to the end of this elements internal list of subelements. """ self._elem.extend((sub_elem._elem for sub_elem in sub_elems)) @classmethod def fromstring(cls, text: str): """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 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: str = None, type_: str = 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: str): self._elem.text = value class Skipped(Result): """Test result when the case is skipped.""" _tag = "skipped" def __eq__(self, other): return super().__eq__(other) class Failure(Result): """Test result when the case failed.""" _tag = "failure" def __eq__(self, other): return super().__eq__(other) class Error(Result): """Test result when the case has errors during execution.""" _tag = "error" def __eq__(self, other): return super().__eq__(other) POSSIBLE_RESULTS = {Failure, Error, Skipped} class System(Element): """Parent class for SystemOut and SystemErr. Attributes: text: the output message """ _tag = "" def __init__(self, content: str = None): super().__init__(self._tag) self.text = content @property def text(self): return self._elem.text @text.setter def text(self, value: str): self._elem.text = value class SystemOut(System): _tag = "system-out" class SystemErr(System): _tag = "system-err" 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: str = None, classname: str = None, time: float = None): super().__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().__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 is_passed(self): """Whether this testcase was a success (i.e. if it isn't skipped, failed, or errored).""" return not self.result @property def is_skipped(self): """Whether this testcase was skipped.""" for r in self.result: if isinstance(r, Skipped): return True return False @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: Result): # First remove all existing results for entry in self.result: 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: str): 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: str): err = self.child(SystemErr) if err is not None: err.text = value else: err = SystemErr(value) self.append(err) 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: str = None, value: str = None): super().__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 Properties(Element): """A list of properties inside a test suite. See :class:`Property` """ _tag = "properties" def __init__(self): super().__init__(self._tag) def add_property(self, property_: Property): self.append(property_) def __iter__(self): return super().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 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().__init__(self._tag) self.name = name self.filepath = None def __iter__(self): return itertools.chain( super().iterchildren(TestCase), (case for suite in super().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: TestCase): """Removes a test case from the suite.""" for case in self: if case == testcase: super().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_testcases(self, testcases): """Adds test cases to the suite.""" self.extend(testcases) 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_: 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: str = None, pretty=False): write_xml(self, filepath=filepath, pretty=pretty) 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().__init__(self._tag) self.filepath = None self.name = name def __iter__(self): return super().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: TestSuite): """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 fromroot(cls, root_elem: Element): """Constructs Junit objects from an elementTree root element.""" 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 fromstring(cls, text: str): """Construct Junit objects from a XML string.""" root_elem = etree.fromstring(text) # nosec return cls.fromroot(root_elem) @classmethod def fromfile(cls, filepath: str, 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() instance = cls.fromroot(root_elem) instance.filepath = filepath return instance def write(self, filepath: str = 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) junitparser-3.1.0/junitparser/xunit2.py000066400000000000000000000121321442152037700202360ustar00rootroot00000000000000""" The flavor based on Jenkins xunit plugin: https://github.com/jenkinsci/xunit-plugin/blob/xunit-2.3.2/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd According to the internet, The schema is compatible with: - pytest (as default, though it also supports a "legacy" xunit1 flavor) - Erlang/OTP - Maven Surefire - CppTest There may be many others that I'm not aware of. """ import itertools from typing import List, TypeVar from . import junitparser T = TypeVar("T") class JUnitXml(junitparser.JUnitXml): # Pytest and xunit schema doesn't have skipped in testsuites skipped = None def update_statistics(self): """Update test count, time, etc.""" time = 0 tests = failures = errors = 0 for suite in self: suite.update_statistics() tests += suite.tests failures += suite.failures errors += suite.errors time += suite.time self.tests = tests self.failures = failures self.errors = errors self.time = round(time, 3) class TestSuite(junitparser.TestSuite): """TestSuit for Pytest, with some different attributes.""" group = junitparser.Attr() id = junitparser.Attr() package = junitparser.Attr() file = junitparser.Attr() log = junitparser.Attr() url = junitparser.Attr() version = junitparser.Attr() def __iter__(self): return itertools.chain( super().iterchildren(TestCase), (case for suite in super().iterchildren(TestSuite) for case in suite), ) @property def system_out(self): """""" elem = self.child(junitparser.SystemOut) if elem is not None: return elem.text return None @system_out.setter def system_out(self, value: str): """""" out = self.child(junitparser.SystemOut) if out is not None: out.text = value else: out = junitparser.SystemOut(value) self.append(out) @property def system_err(self): """""" elem = self.child(junitparser.SystemErr) if elem is not None: return elem.text return None @system_err.setter def system_err(self, value: str): """""" err = self.child(junitparser.SystemErr) if err is not None: err.text = value else: err = junitparser.SystemErr(value) self.append(err) class StackTrace(junitparser.System): _tag = "stackTrace" class RerunType(junitparser.Result): _tag = "rerunType" @property def stack_trace(self): """""" elem = self.child(StackTrace) if elem is not None: return elem.text return None @stack_trace.setter def stack_trace(self, value: str): """""" trace = self.child(StackTrace) if trace is not None: trace.text = value else: trace = StackTrace(value) self.append(trace) @property def system_out(self): """""" elem = self.child(junitparser.SystemOut) if elem is not None: return elem.text return None @system_out.setter def system_out(self, value: str): """""" out = self.child(junitparser.SystemOut) if out is not None: out.text = value else: out = junitparser.SystemOut(value) self.append(out) @property def system_err(self): """""" elem = self.child(junitparser.SystemErr) if elem is not None: return elem.text return None @system_err.setter def system_err(self, value: str): """""" err = self.child(junitparser.SystemErr) if err is not None: err.text = value else: err = junitparser.SystemErr(value) self.append(err) class RerunFailure(RerunType): _tag = "rerunFailure" class RerunError(RerunType): _tag = "rerunError" class FlakyFailure(RerunType): _tag = "flakyFailure" class FlakyError(RerunType): _tag = "flakyError" class TestCase(junitparser.TestCase): group = junitparser.Attr() def _rerun_results(self, _type: T) -> List[T]: elems = self.iterchildren(_type) results = [] for elem in elems: results.append(_type.fromelem(elem)) return results def rerun_failures(self): """""" return self._rerun_results(RerunFailure) def rerun_errors(self): """""" return self._rerun_results(RerunError) def flaky_failures(self): """""" return self._rerun_results(FlakyFailure) def flaky_errors(self): """""" return self._rerun_results(FlakyError) def add_rerun_result(self, result: RerunType): """Append a rerun result to the test case. A case can have multiple rerun results""" self.append(result) junitparser-3.1.0/pyproject.toml000066400000000000000000000001631442152037700170020ustar00rootroot00000000000000[build-system] requires = ["setuptools >=44.0", "wheel >=0.37"] build-backend = "setuptools.build_meta:__legacy__" junitparser-3.1.0/requirements.txt000066400000000000000000000000011442152037700173410ustar00rootroot00000000000000 junitparser-3.1.0/setup.cfg000066400000000000000000000000311442152037700157010ustar00rootroot00000000000000[bdist_wheel] universal=1junitparser-3.1.0/setup.py000066400000000000000000000020121442152037700155730ustar00rootroot00000000000000from 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 :: 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-3.1.0/tests/000077500000000000000000000000001442152037700152305ustar00rootroot00000000000000junitparser-3.1.0/tests/__init__.py000066400000000000000000000000001442152037700173270ustar00rootroot00000000000000junitparser-3.1.0/tests/data/000077500000000000000000000000001442152037700161415ustar00rootroot00000000000000junitparser-3.1.0/tests/data/jenkins.xml000066400000000000000000000027201442152037700203250ustar00rootroot00000000000000 Assertion failed junitparser-3.1.0/tests/data/no_fails.xml000066400000000000000000000017011442152037700204540ustar00rootroot00000000000000 junitparser-3.1.0/tests/data/no_suites_tag.xml000066400000000000000000000015561442152037700215350ustar00rootroot00000000000000 Assertion failed junitparser-3.1.0/tests/data/normal.xml000066400000000000000000000020261442152037700201530ustar00rootroot00000000000000 Assertion failed junitparser-3.1.0/tests/test_cli.py000066400000000000000000000007671442152037700174220ustar00rootroot00000000000000# -*- coding: utf-8 -*- import os import unittest from junitparser.cli import verify class Test_Cli(unittest.TestCase): def test_verify(self): files_expectedexitcodes = { "data/jenkins.xml": 1, "data/no_fails.xml": 0, "data/normal.xml": 1, } for file, expected_exitcode in files_expectedexitcodes.items(): path = os.path.join(os.path.dirname(__file__), file) self.assertEqual(verify([path]), expected_exitcode) junitparser-3.1.0/tests/test_fromfile.py000066400000000000000000000161611442152037700204510ustar00rootroot00000000000000# -*- coding: utf-8 -*- 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 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-3.1.0/tests/test_general.py000066400000000000000000000661401442152037700202650ustar00rootroot00000000000000# -*- coding: utf-8 -*- 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, Element, ) try: from lxml import etree as expected_lxml_etree has_lxml = True except ImportError: from xml.etree import ElementTree as expected_xml_etree has_lxml = False class Test_XmlPackage(unittest.TestCase): @unittest.skipIf(has_lxml, "xml package is used unless lxml is installed") def test_xml_etree(self): from junitparser.junitparser import etree as actual_etree self.assertEqual(actual_etree, expected_xml_etree) @unittest.skipUnless(has_lxml, "lxml package has to be installed") def test_lxml_etree(self): from junitparser.junitparser import etree as actual_etree self.assertEqual(actual_etree, expected_lxml_etree) 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.assertIsInstance(result, JUnitXml) 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_fromroot_testsuite(self): text = """ test_x.py:11: unconditional skip @pytest.fixture(scope="module") def compb(): yield > raise PermissionError E PermissionError test_x.py:6: PermissionError """ root_elemt = etree.fromstring(text) result = JUnitXml.fromroot(root_elemt) self.assertIsInstance(result, TestSuite) self.assertEqual(result.errors, 1) self.assertEqual(result.skipped, 1) cases = list(iter(result)) 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_fromroot_testsuites(self): text = """ test_x.py:11: unconditional skip @pytest.fixture(scope="module") def compb(): yield > raise PermissionError E PermissionError test_x.py:6: PermissionError """ root_elemt = etree.fromstring(text) result = JUnitXml.fromroot(root_elemt) 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) def test_add_cases(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_testcases([case1, case2, case3, case4]) suite.update_statistics() self.assertEqual(suite.tests, 4) self.assertEqual(suite.failures, 1) self.assertEqual(suite.errors, 1) self.assertEqual(suite.skipped, 1) 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_xml_multi_results(self): text = """ """ case = TestCase.fromstring(text) # no assertion raised self.assertEqual(case.name, "testname") self.assertEqual(len(case.result), 2) def test_multi_results(self): case = TestCase("testname", "testclassname") err = Error("err msg", "err_type") fail1 = Failure("fail msg 1", "fail_type") fail2 = Failure("fail msg 2", "fail_type") fail3 = Failure("fail msg 3", "fail_type") fail4 = Failure("fail msg 4", "fail_type") case.result += [err] self.assertEqual(len(case.result), 1) case.result += [fail1] self.assertEqual(len(case.result), 2) case.result += [fail2] self.assertEqual(len(case.result), 3) case.result += [fail3] self.assertEqual(len(case.result), 4) case.result += [fail4] self.assertEqual(len(case.result), 5) 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''] ) def test_add_child_element(self): class CustomElement(Element): _tag = "custom" foo = Attr() bar = Attr() testcase = TestCase() custom = CustomElement() testcase.append(custom) self.assertIn( testcase.tostring(), [b"", b""], ) def test_case_is_skipped(self): case = TestCase() case.result = [Skipped()] self.assertTrue(case.is_skipped) self.assertFalse(case.is_passed) def test_case_is_passed(self): case = TestCase() case.result = [] self.assertFalse(case.is_skipped) self.assertTrue(case.is_passed) def test_case_is_failed(self): case = TestCase() case.result = [Failure()] self.assertFalse(case.is_skipped) self.assertFalse(case.is_passed) 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) junitparser-3.1.0/tests/test_xunit2.py000066400000000000000000000100761442152037700200760ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest from junitparser.xunit2 import JUnitXml, TestSuite, TestCase, RerunFailure from junitparser import Failure from copy import deepcopy class Test_TestCase(unittest.TestCase): def test_case_fromstring(self): text = """ No ha encontrado Error del servidor Stacktrace 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") rerun_failures = case.rerun_failures() self.assertEqual(len(rerun_failures), 2) self.assertEqual(rerun_failures[0].message, "Not found") self.assertEqual(rerun_failures[0].stack_trace, None) self.assertEqual(rerun_failures[0].system_out, "No ha encontrado") self.assertEqual(rerun_failures[1].stack_trace, "Stacktrace") self.assertEqual(rerun_failures[1].system_err, "Error del servidor") self.assertEqual(len(case.rerun_errors()), 0) self.assertEqual(len(case.flaky_failures()), 0) self.assertEqual(len(case.flaky_errors()), 0) def test_rerun(self): case = TestCase("testname") rerun_failure = RerunFailure("Not found", "404") self.assertEqual(rerun_failure.system_out, None) self.assertEqual(rerun_failure.system_err, None) rerun_failure.stack_trace = "Stack" rerun_failure.system_err = "E404" rerun_failure.system_out = "NOT FOUND" case.add_rerun_result(rerun_failure) self.assertEqual(len(case.rerun_failures()), 1) # Interesting, same object is only added once by xml libs failure2 = deepcopy(rerun_failure) failure2.stack_trace = "Stack2" failure2.system_err = "E401" failure2.system_out = "401 Error" case.add_rerun_result(failure2) self.assertEqual(len(case.rerun_failures()), 2) class Test_TestSuite(unittest.TestCase): def test_properties(self): suite = TestSuite("mySuite") self.assertEqual(suite.system_out, None) self.assertEqual(suite.system_err, None) suite.system_err = "System err" suite.system_out = "System out" self.assertEqual(suite.system_out, "System out") self.assertEqual(suite.system_err, "System err") suite.system_err = "System err2" suite.system_out = "System out2" self.assertEqual(suite.system_out, "System out2") self.assertEqual(suite.system_err, "System err2") def test_iterate_case(self): suite = TestSuite("mySuite") suite.add_testcase(TestCase("test1")) case = next(iter(suite)) self.assertEqual(case.name, "test1") def test_iterate_suite(self): suite = TestSuite("mySuite") suite.add_testsuite(TestSuite("suite1")) suite = next(suite.testsuites()) self.assertEqual(suite.name, "suite1") def test_remove_case(self): suite = TestSuite("mySuite") test = TestCase("test1") suite.add_testcase(test) suite.remove_testcase(test) self.assertEqual(list(iter(suite)), []) class Test_JUnitXml(unittest.TestCase): def test_init(self): xml = JUnitXml("tests") self.assertEqual(xml.name, "tests") xml = JUnitXml("myname") xml.add_testsuite(TestSuite("suite1")) xml.update_statistics() self.assertEqual(xml.skipped, None) # errors="0" self.assertEqual(xml.tostring().count(b"errors"), 2) # "skipped" attribute doesn't exist self.assertEqual(xml.tostring().count(b"skipped"), 1)