pax_global_header00006660000000000000000000000064152125631330014513gustar00rootroot0000000000000052 comment=c69ae31315cb751dae730448272661cc7c73264c junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/000077500000000000000000000000001521256313300213005ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/.gitignore000066400000000000000000000001741521256313300232720ustar00rootroot00000000000000*.pyc build/ dist/ .idea/ .cache/ .venv/ venv/ MANIFEST .coverage *.xml *.xml.html *.egg-info *.code-workspace *.egg-info/ junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/.gitlab-ci.yml000066400000000000000000000023471521256313300237420ustar00rootroot00000000000000.common: before_script: - pip install pytest wheel build script: - python -m pip install -e . - python -m pytest -v . --junitxml=junit2html-job-${CI_JOB_NAME}.xml - python -m junit2htmlreport junit2html-job-${CI_JOB_NAME}.xml - python -m junit2htmlreport --merge junit2html-merged-example.xml tests/junit-unicode.xml tests/junit-unicode2.xml tests/junit-cute2.xml - python -m junit2htmlreport -m tests/large-matrix.html tests/test_large_random1.xml tests/test_large_random2.xml tests/test_large_random3.xml tests/test_large_random4.xml tests/test_large_random5.xml tests/test_large_random6.xml tests/test_large_random7.xml - python -m junit2htmlreport junit2html-merged-example.xml - python -m build -w - python -m junit2htmlreport --summary-matrix - < junit2html-job-${CI_JOB_NAME}.xml artifacts: paths: - junit2html*.xml* - tests/*.html - dist/*.whl reports: junit: - junit2html-job-*.xml python-3.8: image: python:3.8 extends: .common python-3.13: image: python:3.13 extends: .common coverage-3.13: image: python:3.13 script: - pip install pytest pytest-cov - pip install -e . - python3 -m pytest --cov-fail-under=87 --cov=junit2htmlreport .junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/.travis.yml000066400000000000000000000001641521256313300234120ustar00rootroot00000000000000language: python python: - "2.7" install: - python setup.py install script: python -m pytest junit2htmlreport junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/LICENSE000066400000000000000000000020531521256313300223050ustar00rootroot00000000000000MIT License Copyright (c) 2016 Ian Norton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/MANIFEST.in000066400000000000000000000001111521256313300230270ustar00rootroot00000000000000include junit2html README recursive-include junit2htmlreport/* junit2htmljunit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/Pipfile000066400000000000000000000002121521256313300226060ustar00rootroot00000000000000[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.8" junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/README.md000066400000000000000000000044551521256313300225670ustar00rootroot00000000000000junit2html by Ian Norton ------------------------------------------------------------- Hosted at https://gitlab.com/inorton/junit2html This is a simple self-contained python tool to produce a single html file from a single junit xml file. ## Basic Usage: ``` $ junit2html JUNIT_XML_FILE [NEW_HTML_FILE] ``` or ``` $ python -m junit2htmlreport JUNIT_XML_FILE [NEW_HTML_FILE] ``` eg: ``` $ junit2html pytest-results.xml testrun.html ``` or ``` $ python -m junit2htmlreport pytest-results.xml ``` ## Advanced Usage: Render Text summary of results ``` junit2html mytest-results.xml --summary-matrix ``` Render Text sumamry of results and exit non-zero on failures ``` junit2html --summary-matrix ./tests/junit-unicode.xml --max-failures 1 ``` # Installation junit2html is installable via a variety of tools including python virtualenvs, pipx, poetry and others. ## Install via pipx ``` $ pipx install junit2html ``` ## Install from source ``` $ pip install . ``` ## Install from pypi ``` $ sudo pip install junit2html ``` ## Example Outputs You can see junit2html's own test report output content at: https://gitlab.com/inorton/junit2html/-/jobs/artifacts/master/browse?job=python-3.13 An an example of the "matrix" report output can be found at: https://gitlab.com/inorton/junit2html/-/jobs/artifacts/master/file/tests/matrix-example.html?job=python-3.13 About Junit ----------- Junit is a widely used java test framework, it happens to produce a fairly generic formatted test report and many non-java things produce the same files (eg py.test) or can be converted quite easily to junit xml (cunit reports via xslt). The report files are understood by many things like Jenkins and various IDEs. The format of junit files is described here: http://llg.cubic.org/docs/junit/ Source and Releases ------------------- Junit2html is maintained on gitlab at https://gitlab.com/inorton/junit2html The current master build status of junit2html is: [![pipeline status](https://gitlab.com/inorton/junit2html/badges/master/pipeline.svg)](https://gitlab.com/inorton/junit2html/commits/master) The current coverage status is: [![coverage report](https://gitlab.com/inorton/junit2html/badges/master/coverage.svg)](https://gitlab.com/inorton/junit2html/commits/master) Releases are availible via Pypi using pip junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2html000077500000000000000000000001121521256313300233200ustar00rootroot00000000000000#!/usr/bin/env python from junit2htmlreport import runner runner.start() junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/000077500000000000000000000000001521256313300246345ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/__init__.py000066400000000000000000000000001521256313300267330ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/__main__.py000066400000000000000000000001521521256313300267240ustar00rootroot00000000000000import sys if sys.version_info >= (3, 0): from . import runner else: import runner runner.start() junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/case_result.py000066400000000000000000000007321521256313300275210ustar00rootroot00000000000000from enum import Enum class CaseResult(str, Enum): UNTESTED = "untested" PARTIAL_PASS = "partial pass" PARTIAL_FAIL = "partial failure" TOTAL_FAIL = "total failure" FAILED = "failed" # the test failed SKIPPED = "skipped" # the test was skipped PASSED = "passed" # the test completed successfully ABSENT = "absent" # the test was known but not run/failed/skipped UNKNOWN = "" def __str__(self) -> str: return self.value junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/matrix.py000066400000000000000000000253451521256313300265230ustar00rootroot00000000000000""" Handle multiple parsed junit reports """ from __future__ import unicode_literals import os from typing import TYPE_CHECKING from jinja2 import Environment, select_autoescape from . import parser from .case_result import CaseResult from .render import ReportContainer UNTESTED = CaseResult.UNTESTED PARTIAL_PASS = CaseResult.PARTIAL_PASS PARTIAL_FAIL = CaseResult.PARTIAL_FAIL TOTAL_FAIL = CaseResult.TOTAL_FAIL if TYPE_CHECKING: # pragma: no cover from .parser import Case, Class from typing import Dict, List, Optional, Literal class ReportMatrix(ReportContainer): """ Load and handle several report files """ cases: "Dict[str, Dict[str, Dict[str, Case]]]" classes: "Dict[str, Dict[str, Class]]" casenames: "Dict[str, List[str]]" result_stats: "Dict[CaseResult, int]" case_results: "Dict[str, Dict[str, List[CaseResult]]]" def __init__(self): super(ReportMatrix, self).__init__() self.cases = {} self.classes = {} self.casenames = {} self.result_stats = {} self.case_results = {} self.class_timings = {} self.class_anchors = {} def add_case_result(self, case: "Case"): if case.testclass is None or case.testclass.name is None: testclass = "" else: testclass = case.testclass.name casename = "" if case.name is None else case.name if testclass not in self.case_results: self.case_results[testclass] = {} if casename not in self.case_results[testclass]: self.case_results[testclass][casename] = [] self.case_results[testclass][casename].append(case.outcome()) def report_order(self): return sorted(self.reports.keys()) def short_outcome(self, outcome: CaseResult) -> "Literal['/', 's', 'f', 'F', '%', 'X', 'U', '?']": if outcome == CaseResult.PASSED: return "/" elif outcome == CaseResult.SKIPPED: # pragma: no cover return "s" # currently unused because SKIPPED returns UNTESTED elif outcome == CaseResult.FAILED: return "f" elif outcome == CaseResult.TOTAL_FAIL: return "F" elif outcome == CaseResult.PARTIAL_PASS: return "%" elif outcome == CaseResult.PARTIAL_FAIL: return "X" elif outcome == CaseResult.UNTESTED: return "U" return "?" def add_report(self, filename: str): """ Load a report into the matrix :param filename: :return: """ parsed = parser.Junit(filename=filename) filename = os.path.basename(filename) self.reports[filename] = parsed for suite in parsed.suites: for testclass in suite.classes: if testclass not in self.classes: self.classes[testclass] = {} self.class_timings[testclass] = 0 self.class_anchors[testclass] = f"cls_{len(self.class_anchors)}" self.class_timings[testclass] += suite.classes[testclass].duration if testclass not in self.casenames: self.casenames[testclass] = list() self.classes[testclass][filename] = suite.classes[testclass] for testcase in self.classes[testclass][filename].cases: name = "" if testcase.name is None else testcase.name.strip() if name not in self.casenames[testclass]: self.casenames[testclass].append(name) if testclass not in self.cases: self.cases[testclass] = {} if name not in self.cases[testclass]: self.cases[testclass][name] = {} self.cases[testclass][name][filename] = testcase outcome = testcase.outcome() self.add_case_result(testcase) self.result_stats[outcome] = 1 + self.result_stats.get( outcome, 0) def summary(self) -> str: """ Render a summary of the matrix :return: """ raise NotImplementedError() def combined_result_list(self, classname: str, casename: str): """ Combine the result of all instances of the given case :param classname: :param casename: :return: """ if classname in self.case_results: if casename in self.case_results[classname]: results = self.case_results[classname][casename] return self.combined_result(results) return " ", "" def combined_result(self, results: "List[CaseResult]"): """ Given a list of results, produce a "combined" overall result :param results: :return: """ if CaseResult.FAILED in results: return self.short_outcome(CaseResult.FAILED), CaseResult.FAILED.title() if CaseResult.PARTIAL_PASS in results: return self.short_outcome(CaseResult.PARTIAL_PASS), CaseResult.PARTIAL_PASS.title() if CaseResult.TOTAL_FAIL in results: return self.short_outcome(CaseResult.TOTAL_FAIL), CaseResult.TOTAL_FAIL.title() if CaseResult.PASSED in results: return self.short_outcome(CaseResult.PASSED), CaseResult.PASSED.title() if CaseResult.SKIPPED in results: return self.short_outcome(CaseResult.UNTESTED), CaseResult.UNTESTED.title() return " ", "" def class_time_breakdown(self) -> "Dict[str, float]": """Sort the matrix by duration of each class (largest first)""" total_time = sum(self.class_timings.values()) pct = {} total = 0 breaks = 0 for classname, classtime in sorted(self.class_timings.items(), key=lambda x: x[1], reverse=True): frac = classtime / total_time total += classtime breaks += 1 if classname not in pct: pct[classname] = 0 pct[classname] += frac return pct class HTMLMatrix: def __init__(self, matrix: "ReportMatrix"): self.matrix = matrix self.show_time_usage = True self.title: str = "JUnit Matrix" def __str__(self) -> str: loader = self.matrix.loader_factory.get_loader() env = Environment( loader=loader, autoescape=select_autoescape(["html"]) ) template = env.get_template("matrix.html") return template.render(matrix=self.matrix, show_time_usage=self.show_time_usage, title=self.title) class HtmlReportMatrix(ReportMatrix): """ Render a matrix report as html """ outdir: str def __init__(self, outdir: str): super(HtmlReportMatrix, self).__init__() self.outdir = outdir def add_report(self, filename: str, show_toc: bool=True): """ Load a report """ super(HtmlReportMatrix, self).add_report(filename) basename = os.path.basename(filename) # Propagate the theme so individual reports use the same loader. self.reports[basename].loader_factory = self.loader_factory # make the individual report too report = self.reports[basename].html(show_toc=show_toc) if self.outdir != "" and not os.path.exists(self.outdir): os.makedirs(self.outdir) with open( os.path.join(self.outdir, basename) + ".html", "wb") as filehandle: filehandle.write(report.encode("utf-8")) def short_outcome(self, outcome: CaseResult) -> "Literal['/', 's', 'f', 'F', '%', 'X', 'U', '?']": if outcome == CaseResult.PASSED: return "/" return super(HtmlReportMatrix, self).short_outcome(outcome) def short_axis(self, axis: str): if axis.endswith(".xml"): return axis[:-4] return axis def summary(self): """ Render the html :return: """ html_matrix = HTMLMatrix(self) return str(html_matrix) class TextReportMatrix(ReportMatrix): """ Render a matrix report as text """ def summary(self): """ Render as a string :return: """ output = "\nMatrix Test Report\n" output += "===================\n" axis = list(self.reports.keys()) axis.sort() # find the longest classname or test case name left_indent = 0 for classname in self.classes: left_indent = max(len(classname), left_indent) for casename in self.casenames[classname]: left_indent = max(len(casename), left_indent) # render the axis headings in a stepped tree treelines = "" for filename in self.report_order(): output += "{} {}{}\n".format(" " * left_indent, treelines, filename) treelines += "| " output += "{} {}\n".format(" " * left_indent, treelines) # render in groups of the same class for classname in self.classes: # new class output += "{} \n".format(classname) # print the case name for casename in sorted(set(self.casenames[classname])): output += "- {}{} ".format(casename, " " * (left_indent - len(casename))) # print each test and its result for each axis case_data = "" testcase: "Optional[Case]" = None for axis in self.report_order(): if axis not in self.cases[classname][casename]: case_data += " " else: testcase = self.cases[classname][casename][axis] if testcase.skipped: case_data += "s " elif testcase.failure: case_data += "f " else: case_data += "/ " if testcase is None or testcase.name is None: testcase_name = "" else: testcase_name = testcase.name combined, combined_name = self.combined_result( self.case_results[classname][testcase_name]) output += case_data output += " {} {}\n".format(combined, combined_name) # print the result stats output += "\n" output += "-" * 79 output += "\n" output += "Test Results:\n" for outcome in sorted(self.result_stats): output += " {:<12} : {:>6}\n".format( outcome.title(), self.result_stats[outcome]) return output junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/merge.py000066400000000000000000000057141521256313300263140ustar00rootroot00000000000000""" Classes for merging several reports into one """ from __future__ import unicode_literals from typing import TYPE_CHECKING import os import xml.etree.ElementTree as ET from io import BytesIO from . import parser from .render import ReportContainer from .textutils import unicode_str if TYPE_CHECKING: # pragma: no cover from typing import List def is_junit_file(filepath: str): """ Return True if this might be a junit xml file. :return: """ try: with open(filepath, "r", encoding="utf-8") as fh: start = fh.read(512) for fragment in ["' + u"\n" + unicode_str(buf.getvalue()) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/parser.py000066400000000000000000000370241521256313300265100ustar00rootroot00000000000000""" Parse a junit report file into a family of objects """ from __future__ import unicode_literals import xml.dom from typing import TYPE_CHECKING import os import sys import xml.etree.ElementTree as ET import collections import uuid from .case_result import CaseResult from .render import HTMLReport, JunitLoaderBase from .textutils import unicode_str if TYPE_CHECKING: from typing import Dict, List, Optional, Union, Any, OrderedDict NO_CLASSNAME = "no-testclass" PASSED = CaseResult.PASSED FAILED = CaseResult.FAILED SKIPPED = CaseResult.SKIPPED ABSENT = CaseResult.ABSENT UNKNOWN = CaseResult.UNKNOWN def clean_xml_attribute(element: "ET.Element", attribute: str, default: "Optional[str]"=None): """ Get an XML attribute value and ensure it is legal in XML :param element: :param attribute: :param default: :return: """ value = element.attrib.get(attribute, default) if value: value = value.encode("utf-8", errors="replace").decode("utf-8", errors="backslashreplace") value = value.replace(u"\ufffd", "?") # strip out the unicode replacement char return value class ParserError(Exception): """ We had a problem parsing a file """ def __init__(self, message: str): super(ParserError, self).__init__(message) class ToJunitXmlBase(object): """ Base class of all objects that can be serialized to Junit XML """ def tojunit(self) -> "ET.Element": """ Return an Element matching this object :return: """ raise NotImplementedError() def make_element(self, xmltag: str, text: "Optional[str]"=None, attribs: "Optional[Dict[str, Any]]"=None): """ Create an Element and put text and/or attribs into it :param xmltag: tag name :param text: :param attribs: dict of xml attributes :return: """ element = ET.Element(unicode_str(xmltag)) if text is not None: element.text = unicode_str(text) if attribs is not None: for item in attribs: element.set(unicode_str(item), unicode_str(attribs[item])) return element class AnchorBase(object): """ Base class that can generate a unique anchor name. """ def __init__(self): self._anchor = None def id(self): return self.anchor() def anchor(self): """ Generate a html anchor name :return: """ if not self._anchor: self._anchor = str(uuid.uuid4())[-12:] return self._anchor class Class(AnchorBase): """ A namespace for a test """ name: "Optional[str]" = None cases: "list[Case]" def __init__(self): super(Class, self).__init__() self.cases = list() @property def duration(self) -> float: return sum((x.duration for x in self.cases)) class Property(AnchorBase, ToJunitXmlBase): """ Test Properties """ def __init__(self): super(Property, self).__init__() self.name: "Optional[str]" = None self.value: "Optional[str]" = None def tojunit(self): """ Return the xml element for this property :return: """ prop = self.make_element("property") prop.set(u"name", unicode_str(self.name)) prop.set(u"value", unicode_str(self.value)) return prop class Case(AnchorBase, ToJunitXmlBase): """ Test cases """ failure: "Optional[str]" = None failure_msg: "Optional[str]" = None skipped: "Optional[str]" = None skipped_msg: "Optional[str]" = None stderr: "Optional[Union[str,Any]]" = None stdout: "Optional[Union[str,Any]]" = None duration: float = 0 name: "Optional[str]" = None testclass: "Optional[Class]" = None properties: "List[Property]" def __init__(self): super(Case, self).__init__() self.properties = list() @property def display_suffix(self): if self.skipped: return "[s]" return "" def outcome(self) -> CaseResult: """ Return the result of this test case :return: """ if self.skipped: return CaseResult.SKIPPED elif self.failed(): return CaseResult.FAILED return CaseResult.PASSED def prefix(self): if self.skipped: return "[S]" if self.failed(): return "[F]" return "" def tojunit(self): """ Turn this test case back into junit xml :note: this may not be the exact input we loaded :return: """ if self.testclass is None or self.testclass.name is None: testclass_name = "" else: testclass_name = self.testclass.name testcase = self.make_element("testcase") testcase.set(u"name", unicode_str(self.name)) testcase.set(u"classname", unicode_str(testclass_name)) testcase.set(u"time", unicode_str(self.duration)) if self.stderr is not None: testcase.append(self.make_element("system-err", self.stderr)) if self.stdout is not None: testcase.append(self.make_element("system-out", self.stdout)) if self.failure is not None: testcase.append(self.make_element( "failure", self.failure, { "message": self.failure_msg })) if self.skipped: testcase.append(self.make_element( "skipped", self.skipped, { "message": self.skipped_msg })) if self.properties: props = self.make_element("properties") for prop in self.properties: props.append(prop.tojunit()) testcase.append(props) return testcase def fullname(self): """ Get the full name of a test case :return: """ if self.testclass is None or self.testclass.name is None: testclass_name = "" else: testclass_name = self.testclass.name return "{} : {}".format(testclass_name, self.name) def basename(self): """ Get a short name for this case :return: """ if ( self.name is None or self.testclass is None or self.testclass.name is None ): return None if self.name.startswith(self.testclass.name): return self.name[len(self.testclass.name):] return self.name def failed(self): """ Return True if this test failed :return: """ return self.failure is not None class Suite(AnchorBase, ToJunitXmlBase): """ Contains test cases (usually only one suite per report) """ name: "Optional[str]" = None properties: "List[Property]" classes: "OrderedDict[str, Class]" _duration: float = 0 package: "Optional[str]" = None errors: "List[Dict[str, Optional[Union[str,Any]]]]" stdout: "Optional[Union[str,Any]]" = None stderr: "Optional[Union[str,Any]]" = None def __init__(self): super(Suite, self).__init__() self.classes = collections.OrderedDict() self.properties = [] self.errors = [] @property def duration(self) -> float: total = sum((x.duration for x in self.classes.values())) return max(total, self._duration) @duration.setter def duration(self, value: float) -> None: self._duration = value def tojunit(self): """ Return an element for this whole suite and all it's cases :return: """ suite = self.make_element("testsuite") suite.set(u"name", unicode_str(self.name)) suite.set(u"time", unicode_str(self.duration)) if self.properties: props = self.make_element("properties") for prop in self.properties: props.append(prop.tojunit()) suite.append(props) for testcase in self.all(): suite.append(testcase.tojunit()) return suite def __contains__(self, item: str): """ Return True if the given test classname is part of this test suite :param item: :return: """ return item in self.classes def __getitem__(self, item: str): """ Return the given test class object :param item: :return: """ return self.classes[item] def __setitem__(self, key: str, value: "Class"): """ Add a test class :param key: :param value: :return: """ self.classes[key] = value def all(self): """ Return all testcases :return: """ tests: "List[Case]" = list() for testclass in self.classes: tests.extend(self.classes[testclass].cases) return tests def failed(self): """ Return all the failed testcases :return: """ return [test for test in self.all() if test.failed()] def skipped(self): """ Return all skipped testcases :return: """ return [test for test in self.all() if test.skipped] def passed(self): """ Return all the passing testcases :return: """ return [test for test in self.all() if not test.failed() and not test.skipped] class Junit(JunitLoaderBase): """ Parse a single junit xml report """ def __init__(self, filename: "Optional[str]"=None, xmlstring: "Optional[str]"=None): """ Parse the file :param filename: :return: """ super().__init__() self.suites: "List[Suite]" = [] self.tree: "Optional[Union[ET.ElementTree,ET.Element]]" = None self.filename: "Optional[str]" = filename if filename == "-": # read the xml from stdin stdin = sys.stdin.read() xmlstring = stdin self.filename = None if self.filename is not None: self.tree = ET.parse(self.filename) elif xmlstring is not None: self._read(xmlstring) else: raise ValueError("Missing any filename or xmlstring") self.process() def __iter__(self): return self.suites.__iter__() def _read(self, xmlstring: str): """ Populate the junit xml document tree from a string :param xmlstring: :return: """ self.tree = ET.fromstring(xmlstring) def process(self): """ populate the report from the xml :return: """ testrun = False suites: "Optional[list[ET.Element]]" = None root: "ET.Element" if isinstance(self.tree, ET.ElementTree): root = self.tree.getroot() else: root = self.tree if root.tag == "testrun": testrun = True root: "ET.Element" = root[0] if root.tag == "testsuite": suites = [root] if root.tag == "testsuites" or testrun: suites = [x for x in root] if suites is None: raise ParserError("could not find test suites in results xml") suitecount = 0 for suite in suites: suitecount += 1 cursuite = Suite() self.suites.append(cursuite) cursuite.name = clean_xml_attribute(suite, "name", default="suite-" + str(suitecount)) cursuite.package = clean_xml_attribute(suite, "package") cursuite.duration = float(suite.attrib.get("time", '0').replace(',', '') or '0') for element in suite: if element.tag == "error": # top level error? errtag = { "message": element.attrib.get("message", ""), "type": element.attrib.get("type", ""), "text": element.text } cursuite.errors.append(errtag) if element.tag == "system-out": cursuite.stdout = element.text if element.tag == "system-err": cursuite.stderr = element.text if element.tag == "properties": for prop in element: if prop.tag == "property": newproperty = Property() newproperty.name = prop.attrib["name"] newproperty.value = prop.attrib["value"] cursuite.properties.append(newproperty) if element.tag == "testcase": testcase = element if not testcase.attrib.get("classname", None): testcase.attrib["classname"] = NO_CLASSNAME if testcase.attrib["classname"] not in cursuite: testclass = Class() testclass.name = testcase.attrib["classname"] cursuite[testclass.name] = testclass testclass: "Class" = cursuite[testcase.attrib["classname"]] newcase = Case() newcase.name = clean_xml_attribute(testcase, "name") newcase.testclass = testclass newcase.duration = float(testcase.attrib.get("time", '0').replace(',', '') or '0') testclass.cases.append(newcase) # does this test case have any children? for child in testcase: if child.tag == "skipped": newcase.skipped = child.text if "message" in child.attrib: newcase.skipped_msg = child.attrib["message"] if not newcase.skipped: newcase.skipped = "skipped" elif child.tag == "system-out": newcase.stdout = child.text elif child.tag == "system-err": newcase.stderr = child.text elif child.tag == "failure": newcase.failure = child.text if "message" in child.attrib: newcase.failure_msg = child.attrib["message"] if not newcase.failure: newcase.failure = "failed" elif child.tag == "error": newcase.failure = child.text if "message" in child.attrib: newcase.failure_msg = child.attrib["message"] if not newcase.failure: newcase.failure = "error" elif child.tag == "properties": for prop in child: newproperty = Property() newproperty.name = prop.attrib["name"] newproperty.value = prop.attrib["value"] newcase.properties.append(newproperty) def html(self, show_toc: bool=True): """ Render the test suite as a HTML report with links to errors first. :return: """ doc = HTMLReport(show_toc=show_toc) title = "Test Results" if self.filename: if os.path.exists(self.filename): title = os.path.basename(self.filename) doc.load(self, title=title) return str(doc) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/parserimpl.py000066400000000000000000000004521521256313300273650ustar00rootroot00000000000000""" Parse results files """ from .parser import Junit def load_report(filename: str) -> Junit: """ Load a report from disjk """ return Junit(filename=filename) def load_string(text: str) -> Junit: """ Load a report from a string """ return Junit(xmlstring=text) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/render.py000066400000000000000000000106201521256313300264640ustar00rootroot00000000000000""" Render junit reports as HTML """ import os import importlib.util from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover from os import PathLike from typing import Optional, List, Dict from jinja2 import Environment, PackageLoader, select_autoescape, FileSystemLoader, BaseLoader class LoaderFactory: """Obtain jinja template loaders""" DEFAULT = "default" pkgname = "junit2htmlreport" THEME_DIR_ENV = "JUNIT2HTML_THEMES" THEME_FILES = ["base.html", "matrix.html", "report.html", "styles.css"] def __init__(self): self.baseloaders: "Dict[str, BaseLoader]" = { self.DEFAULT: PackageLoader(self.pkgname, os.path.join("templates", self.DEFAULT)), } spec = importlib.util.find_spec(self.pkgname) if spec and spec.origin: pkgdir = os.path.dirname(spec.origin) templates_dir = os.path.join(pkgdir, "templates") for name in os.listdir(templates_dir): if name not in self.baseloaders.keys(): self.baseloaders[name] = PackageLoader(self.pkgname, os.path.join("templates", name)) extra_themes = os.getenv(self.THEME_DIR_ENV, None) if extra_themes and os.path.isdir(extra_themes): for root, folders, filenames in os.walk(os.path.abspath(extra_themes)): if filenames: theme = True for item in self.THEME_FILES: if item not in filenames: theme = False if theme: theme_name = os.path.basename(root) self.baseloaders[theme_name] = FileSystemLoader(root) self.selected = self.DEFAULT def add_filesystem_loader(self, name: str, path: "PathLike"): self.baseloaders[name] = FileSystemLoader(path) def select_loader(self, name: str) -> None: if name in self.baseloaders.keys(): self.selected = name def get_loader(self) -> BaseLoader: return self.baseloaders[self.selected] @property def styles(self) -> list: return sorted(list(self.baseloaders.keys())) class JunitLoaderBase: """Contains a jinja template BaseLoader""" def __init__(self): self._loader_factory: "Optional[LoaderFactory]" = None @property def loader_factory(self) -> "LoaderFactory": if self._loader_factory is None: return LoaderFactory() return self._loader_factory @loader_factory.setter def loader_factory(self, value): self._loader_factory = value class ReportContainer(JunitLoaderBase): """ Hold one or more reports """ reports: "Dict[str, Junit]" def __init__(self): super().__init__() self.reports = {} def add_report(self, filename: str) -> None: raise NotImplementedError() def failures(self): """ Return all the failed test cases :return: """ found: "List[Case]" = [] for report in self.reports: for suite in self.reports[report].suites: found.extend(suite.failed()) return found def skips(self): """ Return all the skipped test cases :return: """ found: "List[Case]" = [] for report in self.reports: for suite in self.reports[report].suites: found.extend(suite.skipped()) return found class HTMLReport: def __init__(self, show_toc: bool=True, show_time_usage: bool=True): self.show_toc = show_toc self.show_time_usage = show_time_usage self.title: str = "" self.report: "Optional[Junit]" = None def load(self, report: "Junit", title: str="JUnit2HTML Report"): self.report = report self.title = title def __iter__(self): if self.report is None: raise Exception("A report must be loaded through `load(...)` first.") return self.report.__iter__() def __str__(self) -> str: loader = self.report.loader_factory.get_loader() env = Environment(loader=loader, autoescape=select_autoescape(["html"])) template = env.get_template("report.html") return template.render(report=self, title=self.title, show_toc=self.show_toc, show_time_usage=self.show_time_usage) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/report.css000066400000000000000000000030321521256313300266570ustar00rootroot00000000000000h1 { font-family: sans-serif; } h2 { font-family: sans-serif; } li > a { font-family: monospace; } body { margin-bottom: 3em; } .testclass > td { border-bottom: 1px solid silver; color: #222222; padding: 3px; font-weight: bold; } .testclass > div { padding-left: 1em; } .testclass > a > div { font-size: 1.3em; font-family: sans-serif; } .testcase > div { border-left: 3px solid black; padding-left: 1em; } .testcase pre { display: block; padding: 4px; margin-right: 1em; background-color: #121212; color: #dedede; white-space: pre-wrap; } .testcase .details b { font-size: 1.2em; } .stdout, .stderr, .property { margin-left: 1em; } .testcase-link { text-decoration: none; } .testcase-cell { height: 1.8em; width: 1.8em; vertical-align: middle; text-align: center; } .testcase-cell a { display: inline-block; margin: -1em; padding: 1em; } .testcase-combined { white-space: pre; padding-left: 1em; } .skipped { background-color: gold; } .failed { background-color: lightcoral; } .passed { background-color: limegreen; } .tooltip { visibility: hidden; padding: 2px; z-index: 1; position: absolute; background: cornsilk; border: 1px solid black; margin-top: 15px; margin-left: 10px; opacity: 0; } .tooltip-parent:hover .tooltip { visibility: visible; opacity: 1; } .testcase:hover { background-color: gray; color: white; } .result-stats { margin: auto; } junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/runner.py000066400000000000000000000112151521256313300265170ustar00rootroot00000000000000""" Small command line tool to generate a html version of a junit report file """ from typing import TYPE_CHECKING import os import sys from argparse import ArgumentParser from . import matrix, merge, parser, render if TYPE_CHECKING: from typing import List loader_factory = render.LoaderFactory() installed_themes = sorted(list(loader_factory.styles)) PARSER = ArgumentParser(prog="junit2html") PARSER.add_argument("--summary-matrix", "-s", dest="text_matrix", action="store_true", default=False, help="Render multiple result files to the console") PARSER.add_argument("--report-matrix", "-m", dest="html_matrix", type=str, metavar="REPORT", help="Generate an HTML report matrix") PARSER.add_argument("--max-failures", dest="fail", type=int, default=0, metavar="FAILURES", help="Exit non-zero if FAILURES or more test cases are failures (has no effect with --merge)") PARSER.add_argument("--max-skipped", dest="skip", type=int, default=0, metavar="SKIPPED", help="Exit non-zero if SKIPPED or more test cases are skipped (has no effect with --merged)") PARSER.add_argument("--merge", dest="merge_output", type=str, metavar="NEWREPORT", help="Merge multiple test results into one file") THEME_MUTEX = PARSER.add_mutually_exclusive_group() THEME_MUTEX.add_argument("--theme", type=str, dest="theme", choices=installed_themes, help="Use an installed junit2html theme") THEME_MUTEX.add_argument("--theme-dir", type=str, dest="template_folder", help="Use a folder containing custom html and CSS.") PARSER.add_argument("--reports-template-folder", dest="template_folder", type=str, help="Render reports with these templates (deprecated, use '--theme-dir' instead") PARSER.add_argument("--hide-toc", dest="hide_toc", action="store_true", default=False, help="Don't include a table-of-contents in the HTML report") PARSER.add_argument("REPORTS", metavar="REPORT", type=str, nargs="+", help="Test file to read") PARSER.add_argument("OUTPUT", type=str, nargs="?", help="Filename to save the html as") def run(args: "List[str]"): """ Run this tool :param args: :return: """ opts = PARSER.parse_args(args) if args else PARSER.parse_args() if opts.template_folder and os.path.isdir(opts.template_folder): loader_factory.add_filesystem_loader(loader_factory.DEFAULT, os.path.abspath(opts.template_folder)) if opts.theme: loader_factory.select_loader(opts.theme) inputs = opts.REPORTS util = None if opts.merge_output: util = merge.Merger() for inputfile in inputs: util.add_report(inputfile) xmltext = util.toxmlstring() with open(opts.merge_output, "w") as outfile: outfile.write(xmltext) elif opts.text_matrix: util = matrix.TextReportMatrix() for filename in inputs: util.add_report(filename) print(util.summary()) elif opts.html_matrix: util = matrix.HtmlReportMatrix(os.path.dirname(opts.html_matrix)) util.loader_factory = loader_factory for filename in inputs: util.add_report(filename, show_toc=not opts.hide_toc) with open(opts.html_matrix, "w") as outfile: outfile.write(util.summary()) if util: if opts.fail: failed = util.failures() if len(failed) >= opts.fail: sys.exit(len(failed)) if opts.skip: skipped = util.skips() if len(skipped) >= opts.fail: sys.exit(len(skipped)) if not util: # legacy interface that we need to preserve # no options, one or two args, first is input file, optional second is output if len(opts.REPORTS) > 2: PARSER.print_usage() sys.exit(1) infilename = opts.REPORTS[0] if len(opts.REPORTS) == 2: outfilename = opts.REPORTS[1] else: outfilename = infilename + ".html" report = parser.Junit(infilename) report.loader_factory = loader_factory html = report.html(show_toc=not opts.hide_toc) if report.filename is not None: with open(outfilename, "wb") as outfile: outfile.write(html.encode('utf-8')) else: print(html.encode('utf-8')) def start(): """ Run using the current sys.argv """ run(sys.argv[1:]) if __name__ == "__main__": start() junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/000077500000000000000000000000001521256313300266325ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/dark/000077500000000000000000000000001521256313300275535ustar00rootroot00000000000000base.html000066400000000000000000000004511521256313300312740ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/dark {{title}} {% block content %} {% endblock %} matrix.html000066400000000000000000000065161521256313300316760ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/dark{% extends "base.html" %} {% block content %}

Reports Matrix : {{ title }}

{%- set report_names = matrix.report_order() %} {%- set n_reports = report_names.__len__() %} {%- if n_reports > 0 %} {%- for n in range(n_reports) %} {%- endfor %} {%- endif %} {%- for classname in matrix.classes.keys()|sort %} {%- for n in range(n_reports) %} {%- endfor %} {%- for casename in matrix.casenames[classname] %} {%- set xcasedisplay = casename %} {%- if xcasedisplay|length > 80 %} {%- set xcasedisplay = casename|truncate(60) ~ casename[-16:] %} {%- endif %} {%- set xcase = matrix.cases[classname][casename] %} {%- for n in range(n_reports) %} {%- set axis = report_names[n] %} {%- set line_class="mx-ax-line" %} {%- if axis in xcase %} {%- set line_class = line_class ~ " mx-res-" ~ xcase[axis].outcome() %} {%- endif %} {%- endfor %} {%- endfor %} {%- endfor %}
{%- for outcome in matrix.result_stats %} {%- endfor %}
{{outcome.title()}} {{matrix.result_stats[outcome]}}
{%- if show_time_usage %} Slowest suites: {%- set times = matrix.class_time_breakdown() %} {%- set total_time = matrix.class_timings.values()|sum %} {%- set breakdown_total = 0 %} {%- for classname, pct in (times|dictsort(False, by="value", reverse=True))[:5] %} {%- set breakdown_total = breakdown_total + pct %} {%- endfor %}
CPU time (mins)Suite Name
{{ (100 * pct)|round }}% {{ ((pct * total_time) / 60)|round(1) }} {{ classname }}
{%- endif %}
{{ classname }} ({{ (matrix.class_timings[classname] / 60)|round(1) }} mins)
{{xcasedisplay}} {{ matrix.combined_result_list(classname, casename)[1] }} {%- if axis in xcase %}
{{ matrix.short_outcome(xcase[axis].outcome()) }}
{%- endif %}
{% endblock %} report.html000066400000000000000000000113731521256313300317020ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/dark{% extends "base.html" %} {% block content %}

Test Report : {{ report.title }}

{% if show_toc %}
    {% for suite in report %} {% for classname in suite.classes %}
  • {{classname}}
      {% for test in suite.classes[classname].cases %}
    • {{test.name}}{{test.display_suffix}}
    • {% endfor %}
  • {% endfor %} {% endfor %}
    {% for suite in report %} {% for classname in suite.classes %} {% for test in suite.classes[classname].cases %} {% if test.failed() %}
  • {{test.prefix()}} {{test.fullname()}}
  • {% endif %} {% endfor %} {% endfor %} {% endfor %}
{% endif %} {% for suite in report %}

Test Suite: {{ suite.name }}

{% if suite.package %} Package: {{suite.package}} {% endif %} {% if suite.properties %}

Suite Properties

{% for prop in suite.properties %} {% endfor %}
{{prop.name}}{{prop.value}}
{% endif %}

Results

Duration{{suite.duration |round(3)}} sec
Tests{{suite.all() |length}}
Failures{{suite.failed()| length}}

Tests

{% for classname in suite.classes %}

{{classname}}

{% for test in suite.classes[classname].cases %}
{% if test.failed() %} {% endif %} {% if test.skipped %} {% endif %}
Test case:{{test.name}}
Outcome:{{test.outcome().title()}}
Duration:{{test.duration|round(3)}} sec
Failed{{test.failure_msg}}
Skipped{{test.skipped_msg}}
{% if test.failed() %}
{{test.failure}}
{% endif %} {% if test.skipped %}
{{test.skipped}}
{% endif %} {% if test.properties %} {% for prop in test.properties %} {% endfor %}
{{prop.name}}{{prop.value}}
{% endif %} {% if test.stdout %}
Stdout
{{test.stdout}}
{% endif %} {% if test.stderr %}
Stderr
{{test.stderr}}
{% endif %}
{% endfor %}
{% endfor %}
{% if suite.stdout or suite.stderr %}

Suite stdout:

{{suite.stdout}}

Suite stderr:

{{suite.stderr}}
{% endif %} {% endfor %} {% endblock %} styles.css000066400000000000000000000073341521256313300315400ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/darkbody { background-color: #111111; color: silver; padding-bottom: 20em; margin: 0; min-height: 15cm; } a { color: #007acc; } h1, h2, h3, h4, h5, h6, h7 { font-family: sans-serif; } h1 { color: cadetblue; padding: 3mm; margin-top: 0; margin-bottom: 1mm; } .footer { font-style: italic; font-size: small; text-align: right; padding: 1em; } .testsuite { padding-bottom: 2em; margin-left: 1em; } .proplist { width: 100%; margin-bottom: 2em; border-collapse: collapse; border: 1px solid grey; } .proplist th { background-color: darkslategrey; width: 5em; padding: 2px; padding-right: 1em; text-align: left; font-family: monospace; } .proplist td { padding: 2px; word-break: break-all; } .index-table { width: 90%; margin-left: 1em; } .index-table td { vertical-align: top; max-width: 200px; word-wrap: break-word; } .failure-index { } .toc { margin-bottom: 2em; font-family: monospace; } .stdio, pre { min-height: 1em; background-color: #1e1e1e; color: silver; padding: 0.5em; } .tdpre { background-color: #1e1e1e; } .test { margin-left: 0.5cm; } .toc { list-style: none; } .toc li.outcome { list-style: none; } .toc li.outcome::before { content: "\2022"; color: black; font-weight: bold; display: inline-block; width: 1.27em; margin-left: -1.27em; font-size: 1.2em; } .toc li.outcome-failed { background-color: #440000; } .toc li.outcome-failed::before { color: red; } .toc li.outcome-passed::before { color: lightseagreen; } .toc li.outcome-skipped::before { color: orange; } .testcases .outcome { border-left: 1em; padding: 2px; } .testcases .outcome-failed { border-left: 1em solid palevioletred; } .outcome-passed { border-left: 1em solid seagreen; } .outcome-skipped { border-left: 1em dotted silver; } .testcases .outcome-passed { border-left: 1em solid seagreen; } .testcases .outcome-skipped { border-left: 1em solid grey; } .stats-table { } .stats-table td { min-width: 4em; text-align: right; } .stats-table .failed { background-color: darkred; } .stats-table .passed { background-color: seagreen; } .mx-table { table-layout: fixed; border-spacing: 0; width: available; margin-left: 1em; } .mx-table td { vertical-align: center; } .mx-table td:last-child { width: 0; } .mx-table tr:hover { background-color: darkgoldenrod; color: black; } .mx-ax-name { white-space: nowrap; padding-right: 0.5em; border-left: 1px solid black; border-top: 1px solid black; text-align: right; } .mx-ax-vertical { white-space: nowrap; transform: rotate(180deg); writing-mode: vertical-rl; text-align: right; max-height: 4cm; overflow: clip; } .mx-ax-vertical a { text-decoration: none; } .mx-ax-line { border-left: 1px solid black; width: 0.5em; } .mx-ax-line a { text-decoration: none; width: 1em; height: 1em; } .mx-cls { text-align: left; width: 100%; border-top: 2px solid grey; border-bottom: 1px solid silver; } .mx-name { text-align: left; font-weight: normal; font-style: italic; padding-left: 1em; border-bottom: 1px solid silver; } .mx-res { display: block; width: 1em; min-height: 1em; text-align: center; padding: 0; margin: 0; } .mx-res-combined { white-space: nowrap; padding-right: 0.2em; text-align: right; } .mx-res-failed { background-color: palevioletred; } .mx-res-passed { background-color: seagreen; } .mx-res-skipped { background-color: darkgoldenrod; } .mx-even { background-color: #222222; } junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/default/000077500000000000000000000000001521256313300302565ustar00rootroot00000000000000base.html000066400000000000000000000004511521256313300317770ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/default {{title}} {% block content %} {% endblock %} matrix.html000066400000000000000000000065161521256313300324010ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/default{% extends "base.html" %} {% block content %}

Reports Matrix : {{ title }}

{%- set report_names = matrix.report_order() %} {%- set n_reports = report_names.__len__() %} {%- if n_reports > 0 %} {%- for n in range(n_reports) %} {%- endfor %} {%- endif %} {%- for classname in matrix.classes.keys()|sort %} {%- for n in range(n_reports) %} {%- endfor %} {%- for casename in matrix.casenames[classname] %} {%- set xcasedisplay = casename %} {%- if xcasedisplay|length > 80 %} {%- set xcasedisplay = casename|truncate(60) ~ casename[-16:] %} {%- endif %} {%- set xcase = matrix.cases[classname][casename] %} {%- for n in range(n_reports) %} {%- set axis = report_names[n] %} {%- set line_class="mx-ax-line" %} {%- if axis in xcase %} {%- set line_class = line_class ~ " mx-res-" ~ xcase[axis].outcome() %} {%- endif %} {%- endfor %} {%- endfor %} {%- endfor %}
{%- for outcome in matrix.result_stats %} {%- endfor %}
{{outcome.title()}} {{matrix.result_stats[outcome]}}
{%- if show_time_usage %} Slowest suites: {%- set times = matrix.class_time_breakdown() %} {%- set total_time = matrix.class_timings.values()|sum %} {%- set breakdown_total = 0 %} {%- for classname, pct in (times|dictsort(False, by="value", reverse=True))[:5] %} {%- set breakdown_total = breakdown_total + pct %} {%- endfor %}
CPU time (mins)Suite Name
{{ (100 * pct)|round }}% {{ ((pct * total_time) / 60)|round(1) }} {{ classname }}
{%- endif %}
{{ classname }} ({{ (matrix.class_timings[classname] / 60)|round(1) }} mins)
{{xcasedisplay}} {{ matrix.combined_result_list(classname, casename)[1] }} {%- if axis in xcase %}
{{ matrix.short_outcome(xcase[axis].outcome()) }}
{%- endif %}
{% endblock %} report.html000066400000000000000000000113731521256313300324050ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/default{% extends "base.html" %} {% block content %}

Test Report : {{ report.title }}

{% if show_toc %}
    {% for suite in report %} {% for classname in suite.classes %}
  • {{classname}}
      {% for test in suite.classes[classname].cases %}
    • {{test.name}}{{test.display_suffix}}
    • {% endfor %}
  • {% endfor %} {% endfor %}
    {% for suite in report %} {% for classname in suite.classes %} {% for test in suite.classes[classname].cases %} {% if test.failed() %}
  • {{test.prefix()}} {{test.fullname()}}
  • {% endif %} {% endfor %} {% endfor %} {% endfor %}
{% endif %} {% for suite in report %}

Test Suite: {{ suite.name }}

{% if suite.package %} Package: {{suite.package}} {% endif %} {% if suite.properties %}

Suite Properties

{% for prop in suite.properties %} {% endfor %}
{{prop.name}}{{prop.value}}
{% endif %}

Results

Duration{{suite.duration |round(3)}} sec
Tests{{suite.all() |length}}
Failures{{suite.failed()| length}}

Tests

{% for classname in suite.classes %}

{{classname}}

{% for test in suite.classes[classname].cases %}
{% if test.failed() %} {% endif %} {% if test.skipped %} {% endif %}
Test case:{{test.name}}
Outcome:{{test.outcome().title()}}
Duration:{{test.duration|round(3)}} sec
Failed{{test.failure_msg}}
Skipped{{test.skipped_msg}}
{% if test.failed() %}
{{test.failure}}
{% endif %} {% if test.skipped %}
{{test.skipped}}
{% endif %} {% if test.properties %} {% for prop in test.properties %} {% endfor %}
{{prop.name}}{{prop.value}}
{% endif %} {% if test.stdout %}
Stdout
{{test.stdout}}
{% endif %} {% if test.stderr %}
Stderr
{{test.stderr}}
{% endif %}
{% endfor %}
{% endfor %}
{% if suite.stdout or suite.stderr %}

Suite stdout:

{{suite.stdout}}

Suite stderr:

{{suite.stderr}}
{% endif %} {% endfor %} {% endblock %} styles.css000066400000000000000000000073421521256313300322420ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/templates/defaultbody { background-color: white; padding-bottom: 20em; margin: 0; min-height: 15cm; } h1, h2, h3, h4, h5, h6, h7 { font-family: sans-serif; } h1 { background-color: #007acc; color: white; padding: 3mm; margin-top: 0; margin-bottom: 1mm; } .footer { font-style: italic; font-size: small; text-align: right; padding: 1em; } .testsuite { padding-bottom: 2em; margin-left: 1em; } .proplist { width: 100%; margin-bottom: 2em; border-collapse: collapse; border: 1px solid grey; } .proplist th { background-color: silver; width: 5em; padding: 2px; padding-right: 1em; text-align: left; font-family: monospace; } .proplist td { padding: 2px; word-break: break-all; } .index-table { width: 90%; margin-left: 1em; } .index-table td { vertical-align: top; max-width: 200px; word-wrap: break-word; } .failure-index { } .toc { margin-bottom: 2em; font-family: monospace; } .stdio, pre { min-height: 1em; background-color: #1e1e1e; color: silver; padding: 0.5em; } .tdpre { background-color: #1e1e1e; } .test { margin-left: 0.5cm; } .toc { list-style: none; } .toc li.outcome { list-style: none; } .toc li.outcome::before { content: "\2022"; color: black; font-weight: bold; display: inline-block; width: 1.27em; margin-left: -1.27em; font-size: 1.2em; } .toc li.outcome-failed { background-color: lightcoral; } .toc li.outcome-failed::before { color: red; } .toc li.outcome-passed::before { color: green; } .toc li.outcome-skipped::before { color: orange; } .testcases .outcome { border-left: 1em; padding: 2px; } .testcases .outcome-failed { border-left: 1em solid lightcoral; } .outcome-passed { border-left: 1em solid lightgreen; } .outcome-skipped { border-left: 1em dotted silver; } .testcases .outcome-passed { border-left: 1em solid lightgreen; } .testcases .outcome-skipped { border-left: 1em solid #FED8B1; } .stats-table { } .stats-table td { min-width: 4em; text-align: right; } .stats-table .failed { background-color: lightcoral; } .stats-table .passed { background-color: lightgreen; } .mx-table { table-layout: fixed; border-spacing: 0; width: available; margin-left: 1em; } .mx-table td { vertical-align: center; } .mx-table td:last-child { width: 0; } .mx-table tr:hover { background-color: yellow; } .mx-ax-name { white-space: nowrap; padding-right: 0.5em; border-left: 1px solid black; border-top: 1px solid black; text-align: right; } .mx-ax-vertical { white-space: nowrap; transform: rotate(180deg); writing-mode: vertical-rl; text-align: right; max-height: 4cm; overflow: clip; } .mx-ax-vertical a { text-decoration: none; } .mx-ax-line { border-left: 1px solid black; width: 0.5em; } .mx-ax-line a { text-decoration: none; width: 1em; height: 1em; } .mx-cls { text-align: left; font-family: monospace; width: 100%; border-top: 2px solid grey; border-bottom: 1px solid silver; } .mx-name { text-align: left; font-weight: normal; font-style: italic; padding-left: 1em; font-family: monospace; border-bottom: 1px solid silver; } .mx-res { display: block; width: 1em; min-height: 1em; text-align: center; padding: 0; margin: 0; } .mx-res-combined { white-space: nowrap; padding-right: 0.2em; text-align: right; } .mx-res-failed { background-color: lightcoral; } .mx-res-passed { background-color: lightgreen; } .mx-res-skipped { background-color: lightyellow; } .mx-even { background-color: lightgray; } junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/junit2htmlreport/textutils.py000066400000000000000000000004561521256313300272600ustar00rootroot00000000000000""" Stringify to unicode """ from typing import Any, Optional def unicode_str(text: "Optional[Any]"): """ Convert text to unicode :param text: :return: """ if isinstance(text, bytes): return text.decode("utf-8", "strict") return "" if text is None else str(text) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/pyproject.toml000066400000000000000000000001311521256313300242070ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta"junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/pytest.ini000066400000000000000000000001071521256313300233270ustar00rootroot00000000000000[pytest] addopts = --junit-xml pytest-results.xml junit_family = xunit2junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/setup-venv.sh000066400000000000000000000001301521256313300237420ustar00rootroot00000000000000#!/bin/bash set -e rm -rf venv python3 -m venv venv . venv/bin/activate pip install -e .junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/setup.cfg000066400000000000000000000012421521256313300231200ustar00rootroot00000000000000[metadata] name = junit2html version = 31.1.4 description = Generate HTML reports from Junit results long_description = Generate a single file HTML report from a Junit or XUnit XML results file author = Ian Norton author_email = inorton@gmail.com url = https://gitlab.com/inorton/junit2html license = License :: OSI Approved :: MIT License [options] platforms = any python_requires = >= 3.8 install_requires = jinja2>=3.0 include_package_data = True packages = junit2htmlreport [options.package_data] junit2htmlreport = templates/**/*.css templates/**/*.html [options.entry_points] console_scripts = junit2html = junit2htmlreport.runner:start junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/000077500000000000000000000000001521256313300224425ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/.gitignore000066400000000000000000000000071521256313300244270ustar00rootroot00000000000000*.html junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/__init__.py000066400000000000000000000000001521256313300245410ustar00rootroot00000000000000junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/fake_report_generator.py000066400000000000000000000075611521256313300273740ustar00rootroot00000000000000"""Generate junit test reports""" import random import xml.etree.ElementTree as ET from io import BytesIO from typing import List from pathlib import Path from argparse import ArgumentParser from junit2htmlreport.parser import Suite, Class, Case from junit2htmlreport.textutils import unicode_str parser = ArgumentParser(description=__doc__) parser.add_argument("NAME", type=Path, help="Filename to write junit xml to") parser.add_argument("--suites", type=int, default=5, help="number of test suites") parser.add_argument("--size", type=int, default=50, help="number of tests in each suite") NAMES = [ "apple", "orange", "toast", "cat", "bat", "potato", "nemesis", "vulcan", "hytac", "duessa", "odracir", "baccus", "terminus", "sodor", "tokamak", "charissa", "genetix", "socrates", "magma", "igneous", "elwood", "avernus", "acheron", "beacon", "storm", "isolus", "vattland", "granite", "manta", "walrus", "albatross", "seal", "bear", "petrel", "razorbill", "mule", "swordfish", "needlefish", "butterfly", "corgette", "teapot", "moose", "acetone", "yorkshire tea", "smartcard", "bobble", "grno", "null", "panther", "courier", "alamo", "martham", "winterton", ] class NameGenerator: def __init__(self): self.used_suites = set() def get_namespace_name(self) -> str: words = random.randrange(1, 6) parts = [random.choice(NAMES) for _ in range(words)] name = ".".join(parts) return name def get_suite_name(self) -> str: while True: name = self.get_namespace_name() if name not in self.used_suites: self.used_suites.add(name) return name def get_test_name(self) -> str: return f"test_{random.choice(NAMES)}_{random.randint(1, 4)}" NAMER = NameGenerator() def generate_random_suites(n_suites: int, suite_tests: int) -> List[Suite]: suites = [] for i in range(n_suites): s = Suite() s.name = NAMER.get_suite_name() while len(s.all()) < suite_tests: classname = NAMER.get_namespace_name() testclass = Class() testclass.name = classname s.classes[classname] = testclass newtests = random.randrange(1,6) for n in range(newtests): case = Case() case.name = NAMER.get_test_name() case.testclass = testclass testclass.cases.append(case) duration = random.randrange(1, 300) if duration > 200: duration *= random.randrange(10, 140) case.duration = duration if random.randint(1, 5) % 2 == 0: case.failure = f"Failed {random.choice(NAMES)}" elif random.randint(0, 14) % 2 == 0: case.skipped = "Skipped" case.skipped_msg = f"Skipped because {NAMER.get_namespace_name()}" suites.append(s) return suites def main(): args = parser.parse_args() suites = generate_random_suites(args.suites, args.size) duration = 0 for suite in suites: for testcase in suite.all(): duration += testcase.duration suite_list = ET.Element("testsuites") suite_list.set(u"duration", unicode_str(str(duration))) for suite in suites: suite_list.append(suite.tojunit()) tree = ET.ElementTree(suite_list) ET.indent(tree, space=" ", level=0) buf = BytesIO() tree.write(buf) with args.NAME.open("wb") as out: out.write(b'') out.write(b"\n") out.write(buf.getvalue()) if __name__ == "__main__": main()junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/helpers.py000066400000000000000000000015011521256313300244530ustar00rootroot00000000000000""" Helper funcs for tests """ import os from .inputfiles import get_filepath from junit2htmlreport import runner def run_runner(tmpdir, filename, *extra): """ Run the junit2html program against the given report and produce a html doc :param tmpdir: :param filename: :param extra: addtional arguments :return: """ testfile = get_filepath(filename=filename) if not len(extra): outfile = os.path.join(tmpdir.strpath, "report.html") runner.run([testfile, outfile]) assert os.path.exists(outfile) else: runner.run([testfile] + list(extra)) def test_runner_simple(tmpdir): """ Test the stand-alone app with a simple fairly empty junit file :param tmpdir: py.test tmpdir fixture :return: """ run_runner(tmpdir, "junit-simple_suites.xml") junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/inputfiles.py000066400000000000000000000004631521256313300252010ustar00rootroot00000000000000""" Helper for loading our test files """ import os HERE = os.path.dirname(os.path.abspath(__file__)) def get_reports(): return [x for x in os.listdir(HERE) if x.endswith(".xml")] def get_filepath(filename): """ :param filename: :return: """ return os.path.join(HERE, filename) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-axis-linux.xml000066400000000000000000000010101521256313300264040ustar00rootroot00000000000000 Assertion failed junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-axis-solaris.xml000066400000000000000000000010101521256313300267210ustar00rootroot00000000000000 Assertion failed junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-axis-windows.xml000066400000000000000000000007301521256313300267470ustar00rootroot00000000000000 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-complex_suites.xml000066400000000000000000002611301521256313300273610ustar00rootroot00000000000000 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-cute2.xml000066400000000000000000000061171521256313300253420ustar00rootroot00000000000000 SetLarger: your == T1 "Set value to -1 should throw" "Set value overflow" "Set use to 1 more than alloc" junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-jenkins-stdout.xml000066400000000000000000000124351521256313300273010ustar00rootroot00000000000000 expectedRet: 0 - realRet: 141 File "/home/admin/sikuli/sikuli-ide.jar/Lib/unittest.py", line 260, in run testMethod() File "/home/admin/workspace/main-POC/test/automation-rdpcore/./RDPTest.sikuli/RDPTest.py", line 75, in testRdp myRDP.verifyResult(self.tc["ExpectedReturnCode"], self.tc["ExpectedErrorMsg"]) File "/home/admin/workspace/main-POC/test/automation-rdpcore/lib/rdp.sikuli/rdp.py", line 388, in verifyResult assert self.ret == int(expectedRet), "expectedRet: %s - realRet: %s" % (str(expectedRet), str(self.ret)) Executing test case[99]: Stand-alone SH which disables NLA with default setting Test Case Action: ExitDisconnect HostOS= 2008r2 , CMD= 10.200.25.179 -u auto1 -p Test#123 -d dellrdp, Actions= ExitDisconnect In init(). IN getImage(). rdp(): title = rdpclient.png running rdp client... ./rdpclient 10.200.25.179 -u auto1 -p Test#123 -d dellrdp --no-color-log Searching: RDP initialization has succeeded and ready to start main rdp loop.' to check if RDP session is connected or done. until timeout in 20 seconds. 0:00:01.027507084: rdpclient:b514a700: GStreamer Element/CODEC ' bmmxvimagesink' not found 0:00:01.027605840: rdpclient:b514a700: rdp(): starting 0:00:01.027648135: __BASE__:b514a700: Client Name is 'ubuntu-12.04' 0:00:01.027753259: __BASE__:b514a700: mchannel_init(): thread id 0xb514a700 0:00:01.028589778: __BASE__:b514a700: Connect to the host 10.200.25.179 ... 0:00:01.028625424: __BASE__:b514a700: client_sock_connect(): fqdn=(null) tna=(null) tsgwEnable=0 0:00:01.036141354: __BASE__:b514a700: RDP: NLA CredSSP Authentication 0:00:01.044063727: __BASE__:b514a700: NLA_SSL_connect():300: SSL_get_verify_result=20: unable to get local issuer certificate 0:00:01.044377460: __BASE__:b514a700: NLA_SSL_connect():312: Peer common name (shost6.dellrdp.local) doesn't match host name (10.200.25.179) 0:00:01.138312578: __BASE__:b514a700: Start main send-to-RDP thread... 0:00:01.138400992: __BASE__:b514a700: RDP initialization has succeeded and ready to start main rdp loop. 1.138400992 Found: RDP initialization has succeeded and ready to start main rdp loop Wait for 10 seconds... Call performExit(). HostOD = 2008r2 In performExit(): 2008r2 rdpclient.png Wait for 15 secs. IN getImage(). IN getImage(). IN getImage(). IN getImage(). IN getImage(). IN getImage(). IN getImage(). IN getImage(). IN getImage(). window.png IN getImage(). Call verifyResult(). IN getImage(). IN getImage(). IN getImage(). Polling the rdpclient process and check if it exits properly in 120 seconds...... 141 IN getImage(). IN getImage(). IN getImage(). *******************************stdout******************************* *******************************stderr******************************* 0:00:01.027507084: rdpclient:b514a700: GStreamer Element/CODEC ' bmmxvimagesink' not found 0:00:01.027605840: rdpclient:b514a700: rdp(): starting 0:00:01.027648135: __BASE__:b514a700: Client Name is 'ubuntu-12.04' 0:00:01.027753259: __BASE__:b514a700: mchannel_init(): thread id 0xb514a700 0:00:01.028589778: __BASE__:b514a700: Connect to the host 10.200.25.179 ... 0:00:01.028625424: __BASE__:b514a700: client_sock_connect(): fqdn=(null) tna=(null) tsgwEnable=0 0:00:01.036141354: __BASE__:b514a700: RDP: NLA CredSSP Authentication 0:00:01.044063727: __BASE__:b514a700: NLA_SSL_connect():300: SSL_get_verify_result=20: unable to get local issuer certificate 0:00:01.044377460: __BASE__:b514a700: NLA_SSL_connect():312: Peer common name (shost6.dellrdp.local) doesn't match host name (10.200.25.179) 0:00:01.138312578: __BASE__:b514a700: Start main send-to-RDP thread... 0:00:01.138400992: __BASE__:b514a700: RDP initialization has succeeded and ready to start main rdp loop. 0:00:01.142691288: PDU READ:afb15b40: clt_plat_chal_res(): vchannel sec_license_pkt 0:00:01.206414711: PDU READ:afb15b40: WARNING: Error opening TS-CAL file to save license: error code 2 (No such file or directory) 0:00:01.322600645: PDU READ:afb15b40: RDP: Decompression type is RDP 6.1 Bulk 0:00:01.323062845: -MAIN-:b331cb40: Max gc count: 3 0:00:01.329853196: -MAIN-:b331cb40: Max gc count: 10 0:00:01.545970383: -MAIN-:b331cb40: Max gc count: 21 0:00:01.547702120: -MAIN-:b331cb40: Max gc count: 24 0:00:03.173001519: -MAIN-:b331cb40: Max gc count: 30 0:00:29.063941827: -MAIN-:b331cb40: Max gc count: 36 0:00:29.402013036: -MAIN-:b331cb40: Max gc count: 72 0:00:45.667702527: -MAIN-:b331cb40: Max gc count: 252 0:00:51.223839720: PDU READ:afb15b40: mcs_filter(): SERVER HAS TOLD US TO TERMINATE (32) ***ERROR: Unexpected error return code - expectedRet: 0 , realRet: 141 In tearDown(). ]]> junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-report-6700.xml000066400000000000000000000021031521256313300262140ustar00rootroot00000000000000 Etc etc java.lang.NoClassDefFoundError: gda/device/detector/DAServer at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:169) Caused by: java.lang.ClassNotFoundException: gda.device.detector.DAServer at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at java.lang.ClassLoader.loadClass(ClassLoader.java:307) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) at java.lang.ClassLoader.loadClass(ClassLoader.java:248) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-simple_suite.xml000066400000000000000000000011571521256313300270210ustar00rootroot00000000000000 Assertion failed junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-simple_suites.xml000066400000000000000000000012611521256313300272000ustar00rootroot00000000000000 Assertion failed junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-sonobouy.xml000066400000000000000000005434351521256313300262060ustar00rootroot00000000000000 /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/network/networking.go:61 Feb 14 17:55:59.209: Failed: unknown Body: k8s� � �v1��Status��� � �������Failure�jforbidden: User "system:serviceaccount:heptio-sonobuoy:sonobuoy-serviceaccount" cannot get path "/metrics"" Forbidden* �����(�2�0����"� /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/network/networking.go:82 [BeforeEach] [sig-network] Networking /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/framework/framework.go:134 STEP: Creating a kubernetes client Feb 14 17:55:58.883: INFO: >>> kubeConfig: /tmp/kubeconfig-461644004 STEP: Building a namespace api object STEP: Waiting for a default service account to be provisioned in namespace [BeforeEach] [sig-network] Networking /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/network/networking.go:34 STEP: Executing a successful http request from the external internet [It] should provide unchanging, static URL paths for kubernetes api services /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/network/networking.go:61 STEP: testing: /healthz STEP: testing: /api STEP: testing: /apis STEP: testing: /metrics Feb 14 17:55:59.209: INFO: Failed: unknown Body: k8s� � �v1��Status��� � �������Failure�jforbidden: User "system:serviceaccount:heptio-sonobuoy:sonobuoy-serviceaccount" cannot get path "/metrics"" Forbidden* �����(�2�0����"� [AfterEach] [sig-network] Networking /go/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/test/e2e/framework/framework.go:135 STEP: Collecting events from namespace "e2e-tests-nettest-bhmc8". STEP: Found 0 events. Feb 14 17:55:59.272: INFO: POD NODE PHASE GRACE CONDITIONS Feb 14 17:55:59.272: INFO: networking-bhsj4 minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 21:48:10 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:20 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 21:48:10 +0000 UTC }] Feb 14 17:55:59.278: INFO: sonobuoy minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:39:02 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:40:42 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:39:02 +0000 UTC }] Feb 14 17:55:59.278: INFO: sonobuoy-e2e-job-a38df139ec5b4f64 minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:40:42 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:41:25 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:40:42 +0000 UTC }] Feb 14 17:55:59.278: INFO: systemd-logs-x2q8h minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:40:42 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:41:24 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-14 00:41:24 +0000 UTC }] Feb 14 17:55:59.278: INFO: etcd-minikube minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:05 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-addon-manager-minikube minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:05 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-apiserver-minikube minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:07 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-controller-manager-minikube minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:01 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:05 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-dns-6f4fd4bdf-c8f6s minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:36 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:53:33 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:36 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-proxy-67cp6 minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:53:09 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:53:10 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:53:10 +0000 UTC }] Feb 14 17:55:59.279: INFO: kube-scheduler-minikube minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:08 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:52:04 +0000 UTC }] Feb 14 17:55:59.279: INFO: kubernetes-dashboard-77d8b98585-xmc9v minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:38 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:54:08 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:38 +0000 UTC }] Feb 14 17:55:59.280: INFO: storage-provisioner minikube Running [{Initialized True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:37 +0000 UTC } {Ready True 0001-01-01 00:00:00 +0000 UTC 2018-02-12 19:54:03 +0000 UTC } {PodScheduled True 0001-01-01 00:00:00 +0000 UTC 2018-02-01 18:32:37 +0000 UTC }] Feb 14 17:55:59.280: INFO: Feb 14 17:55:59.287: INFO: Logging node info for node minikube Feb 14 17:55:59.293: INFO: Node Info: &Node{ObjectMeta:k8s_io_apimachinery_pkg_apis_meta_v1.ObjectMeta{Name:minikube,GenerateName:,Namespace:,SelfLink:/api/v1/nodes/minikube,UID:3b2838ff-077e-11e8-9e7f-0800278e1a2c,ResourceVersion:523670,Generation:0,CreationTimestamp:2018-02-01 18:32:17 +0000 UTC,DeletionTimestamp:<nil>,DeletionGracePeriodSeconds:nil,Labels:map[string]string{beta.kubernetes.io/arch: amd64,beta.kubernetes.io/os: linux,kubernetes.io/hostname: minikube,node-role.kubernetes.io/master: ,},Annotations:map[string]string{node.alpha.kubernetes.io/ttl: 0,volumes.kubernetes.io/controller-managed-attach-detach: true,},OwnerReferences:[],Finalizers:[],ClusterName:,Initializers:nil,},Spec:NodeSpec{PodCIDR:,ExternalID:minikube,ProviderID:,Unschedulable:false,Taints:[],ConfigSource:nil,},Status:NodeStatus{Capacity:ResourceList{cpu: {{2 0} {<nil>} 2 DecimalSI},memory: {{2097205248 0} {<nil>} 2048052Ki BinarySI},pods: {{110 0} {<nil>} 110 DecimalSI},},Allocatable:ResourceList{cpu: {{2 0} {<nil>} 2 DecimalSI},memory: {{1992347648 0} {<nil>} 1945652Ki BinarySI},pods: {{110 0} {<nil>} 110 DecimalSI},},Phase:,Conditions:[{OutOfDisk False 2018-02-14 17:55:50 +0000 UTC 2018-02-01 18:32:08 +0000 UTC KubeletHasSufficientDisk kubelet has sufficient disk space available} {MemoryPressure False 2018-02-14 17:55:50 +0000 UTC 2018-02-01 18:32:08 +0000 UTC KubeletHasSufficientMemory kubelet has sufficient memory available} {DiskPressure False 2018-02-14 17:55:50 +0000 UTC 2018-02-01 18:32:08 +0000 UTC KubeletHasNoDiskPressure kubelet has no disk pressure} {Ready True 2018-02-14 17:55:50 +0000 UTC 2018-02-01 18:32:08 +0000 UTC KubeletReady kubelet is posting ready status}],Addresses:[{InternalIP 10.0.2.15} {Hostname minikube}],DaemonEndpoints:NodeDaemonEndpoints{KubeletEndpoint:DaemonEndpoint{Port:10250,},},NodeInfo:NodeSystemInfo{MachineID:daea3ce95e0a494e92db53185faa7529,SystemUUID:C3DDD6BB-788A-44DB-AA88-D11D9B2ADA0D,BootID:9567033d-7576-4302-b199-d3d63fdd5494,KernelVersion:4.9.64,OSImage:Buildroot 2017.11,ContainerRuntimeVersion:docker://17.9.0,KubeletVersion:v1.9.3,KubeProxyVersion:v1.9.3,OperatingSystem:linux,Architecture:amd64,},Images:[{[perl@sha256:602d654765ebdd0881c435ddfa09c0a8e2ce81581c5415bc608183cd388ba555 perl:latest] 879156863} {[gcr.io/heptio-images/kube-conformance@sha256:5f86dd7095dca30132c49ee529177c813fb026e4cf8eab63800a56dd407860b6 gcr.io/heptio-images/kube-conformance:latest] 520680302} {[gcr.io/heptio-images/sonobuoy@sha256:9f2a352b44143c8c4dc72ea2df07d1b3c9d37e45a2ebcfa72c048cca17b9d6eb gcr.io/heptio-images/sonobuoy:v0.10.0] 480168142} {[gcr.io/heptio-images/sonobuoy-plugin-systemd-logs@sha256:58e077ba773ff5d7b6bba90771fad72217f0d10b913bb66bb0afb387c9552788 gcr.io/heptio-images/sonobuoy-plugin-systemd-logs:latest] 290890605} {[gcr.io/google_containers/kube-apiserver-amd64@sha256:a5382344aa373a90bc87d3baa4eda5402507e8df5b8bfbbad392c4fff715f043 gcr.io/google_containers/kube-apiserver-amd64:v1.9.3 k8s.gcr.io/kube-apiserver-amd64:v1.9.3] 210453276} {[gcr.io/google_containers/kube-apiserver-amd64@sha256:eec4329de0892f4a960b7f1202272f93880d3071a9b40d8407585125b37d527d gcr.io/google_containers/kube-apiserver-amd64:v1.9.2 k8s.gcr.io/kube-apiserver-amd64:v1.9.2] 210430863} {[gcr.io/google_containers/etcd-amd64@sha256:54889c08665d241e321ca5ce976b2df0f766794b698d53faf6b7dacb95316680 gcr.io/google_containers/etcd-amd64:3.1.11] 193862870} {[gcr.io/google_containers/kube-controller-manager-amd64@sha256:3ac295ae3e78af5c9f88164ae95097c2d7af03caddf067cb35599769d0b7251e gcr.io/google_containers/kube-controller-manager-amd64:v1.9.3 k8s.gcr.io/kube-controller-manager-amd64:v1.9.3] 137826990} {[gcr.io/google_containers/kube-controller-manager-amd64@sha256:10daf65c6e8d0ff032323931f3869cd30af23feab90345265ea405b6104a41c7 gcr.io/google_containers/kube-controller-manager-amd64:v1.9.2 k8s.gcr.io/kube-controller-manager-amd64:v1.9.2] 137820233} {[docker@sha256:8f644f21455eebfec99595779f90e9b3727789f5a627a7488e6ff90fb9aaa05c docker:stable-dind] 125589157} {[k8s.gcr.io/kubernetes-dashboard-amd64@sha256:3861695e962972965a4c611bcabc2032f885d8cbdb0bccc9bf513ef16335fe33 k8s.gcr.io/kubernetes-dashboard-amd64:v1.8.1] 120712806} {[ceridwen/networking@sha256:0c3b205434e290d372e946ff31c0e100f92a7db074bf76daf57b471b15664eae ceridwen/networking:v1] 109535621} {[gcr.io/google_containers/kube-proxy-amd64@sha256:fe00a6c576afba09255438f5ff01eff6d63ee302294c5450cb19b38404ef62ac gcr.io/google_containers/kube-proxy-amd64:v1.9.2 k8s.gcr.io/kube-proxy-amd64:v1.9.2] 109095227} {[k8s.gcr.io/kube-proxy-amd64:v1.9.3] 109095127} {[gcr.io/k8s-minikube/storage-provisioner@sha256:088daa9fcbccf04c3f415d77d5a6360d2803922190b675cb7fc88a9d2d91985a gcr.io/k8s-minikube/storage-provisioner:v1.8.1] 80815640} {[gcr.io/google-containers/kube-addon-manager@sha256:90d80609f044c121b0110d71ac02432cf4c0415e8204f25025666179cbb72387 gcr.io/google-containers/kube-addon-manager:v6.5] 79532475} {[gcr.io/google_containers/kube-scheduler-amd64@sha256:2c17e637c8e4f9202300bd5fc26bc98a7099f49559ca0a8921cf692ffd4a1675 gcr.io/google_containers/kube-scheduler-amd64:v1.9.3 k8s.gcr.io/kube-scheduler-amd64:v1.9.3] 62713240} {[gcr.io/google_containers/kube-scheduler-amd64@sha256:082520e24e697f3228046ca13cddf46e4e01ae2982685b4ccc7df8f8e9145abc gcr.io/google_containers/kube-scheduler-amd64:v1.9.2 k8s.gcr.io/kube-scheduler-amd64:v1.9.2] 62710076} {[gcr.io/google_containers/k8s-dns-kube-dns-amd64@sha256:f5bddc71efe905f4e4b96f3ca346414be6d733610c1525b98fff808f93966680 gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.7] 50274864} {[gcr.io/google_containers/k8s-dns-sidecar-amd64@sha256:f80f5f9328107dc516d67f7b70054354b9367d31d4946a3bffd3383d83d7efe8 gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.7] 42033070} {[gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64@sha256:6cfb9f9c2756979013dbd3074e852c2d8ac99652570c5d17d152e0c0eb3321d6 gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.7] 40951808} {[gcr.io/kubernetes-e2e-test-images/hostexec-amd64@sha256:066848e78b757074c932cbafd33958685a9dc0627a5061cdf98a6143db055fbd gcr.io/kubernetes-e2e-test-images/hostexec-amd64:1.0] 8374783} {[gcr.io/kubernetes-e2e-test-images/netexec-amd64@sha256:2edfad424a541b9e024f26368d3a5b7dcc1d7cd27a4ee8c1d8c3f81d9209ab2e gcr.io/kubernetes-e2e-test-images/netexec-amd64:1.0] 6227659} {[ceridwen/netcat@sha256:a2b50e9a0e1dc22c217b426947f1162d80a893567888437be31fa79cd77658d6 ceridwen/netcat:v1] 4048780} {[busybox@sha256:1669a6aa7350e1cdd28f972ddad5aceba2912f589f19a090ac75b7083da748db busybox:latest] 1146369} {[gcr.io/google_containers/pause-amd64@sha256:163ac025575b775d1c0f9bf0bdd0f086883171eb475b5068e7defa4ca9e76516 gcr.io/google_containers/pause-amd64:3.0 k8s.gcr.io/pause-amd64:3.0] 746888}],VolumesInUse:[],VolumesAttached:[],},} Feb 14 17:55:59.304: INFO: Logging kubelet events for node minikube Feb 14 17:55:59.310: INFO: Logging pods the kubelet thinks is on node minikube Feb 14 17:55:59.418: INFO: networking-bhsj4 started at 2018-02-01 21:48:10 +0000 UTC (0+1 container statuses recorded) Feb 14 17:55:59.418: INFO: Container networking ready: true, restart count 1 Feb 14 17:55:59.418: INFO: etcd-minikube started at <nil> (0+0 container statuses recorded) Feb 14 17:55:59.419: INFO: storage-provisioner started at 2018-02-01 18:32:37 +0000 UTC (0+1 container statuses recorded) Feb 14 17:55:59.419: INFO: Container storage-provisioner ready: true, restart count 3 Feb 14 17:55:59.419: INFO: sonobuoy started at 2018-02-14 00:39:02 +0000 UTC (0+1 container statuses recorded) Feb 14 17:55:59.419: INFO: Container kube-sonobuoy ready: true, restart count 0 Feb 14 17:55:59.419: INFO: sonobuoy-e2e-job-a38df139ec5b4f64 started at 2018-02-14 00:40:42 +0000 UTC (0+2 container statuses recorded) Feb 14 17:55:59.419: INFO: Container e2e ready: true, restart count 0 Feb 14 17:55:59.419: INFO: Container sonobuoy-worker ready: true, restart count 0 Feb 14 17:55:59.419: INFO: kube-controller-manager-minikube started at <nil> (0+0 container statuses recorded) Feb 14 17:55:59.419: INFO: kube-addon-manager-minikube started at <nil> (0+0 container statuses recorded) Feb 14 17:55:59.419: INFO: kube-proxy-67cp6 started at 2018-02-12 19:53:09 +0000 UTC (0+1 container statuses recorded) Feb 14 17:55:59.419: INFO: Container kube-proxy ready: true, restart count 0 Feb 14 17:55:59.419: INFO: kube-dns-6f4fd4bdf-c8f6s started at 2018-02-01 18:32:36 +0000 UTC (0+3 container statuses recorded) Feb 14 17:55:59.419: INFO: Container dnsmasq ready: true, restart count 1 Feb 14 17:55:59.419: INFO: Container kubedns ready: true, restart count 2 Feb 14 17:55:59.419: INFO: Container sidecar ready: true, restart count 1 Feb 14 17:55:59.419: INFO: kubernetes-dashboard-77d8b98585-xmc9v started at 2018-02-01 18:32:38 +0000 UTC (0+1 container statuses recorded) Feb 14 17:55:59.419: INFO: Container kubernetes-dashboard ready: true, restart count 3 Feb 14 17:55:59.419: INFO: kube-apiserver-minikube started at <nil> (0+0 container statuses recorded) Feb 14 17:55:59.419: INFO: kube-scheduler-minikube started at <nil> (0+0 container statuses recorded) Feb 14 17:55:59.419: INFO: systemd-logs-x2q8h started at 2018-02-14 00:40:42 +0000 UTC (0+2 container statuses recorded) Feb 14 17:55:59.419: INFO: Container sonobuoy-systemd-logs-config-675ad0e348484fde ready: true, restart count 0 Feb 14 17:55:59.419: INFO: Container sonobuoy-worker ready: true, restart count 0 Feb 14 17:55:59.600: INFO: Latency metrics for node minikube Feb 14 17:55:59.600: INFO: {Operation: Method:pod_start_latency_microseconds Quantile:0.99 Latency:42.923117s} Feb 14 17:55:59.601: INFO: {Operation:pull_image Method:docker_operations_latency_microseconds Quantile:0.99 Latency:24.190615s} Feb 14 17:55:59.601: INFO: {Operation:pull_image Method:docker_operations_latency_microseconds Quantile:0.9 Latency:24.190615s} Feb 14 17:55:59.601: INFO: {Operation:create Method:pod_worker_latency_microseconds Quantile:0.99 Latency:15.548466s} Feb 14 17:55:59.601: INFO: {Operation:create Method:pod_worker_latency_microseconds Quantile:0.9 Latency:15.548466s} Feb 14 17:55:59.601: INFO: {Operation:create Method:pod_worker_latency_microseconds Quantile:0.5 Latency:15.548466s} Feb 14 17:55:59.601: INFO: {Operation:sync Method:pod_worker_latency_microseconds Quantile:0.99 Latency:14.558798s} Feb 14 17:55:59.601: INFO: {Operation:sync Method:pod_worker_latency_microseconds Quantile:0.9 Latency:14.558798s} STEP: Dumping a list of prepulled images on each node to file /tmp/results/image-puller.txt Feb 14 17:55:59.624: INFO: Waiting up to 3m0s for all (but 0) nodes to be ready STEP: Destroying namespace "e2e-tests-nettest-bhmc8" for this suite. Feb 14 17:56:05.673: INFO: Waiting up to 30s for server preferred namespaced resources to be successfully discovered Feb 14 17:56:05.745: INFO: namespace: e2e-tests-nettest-bhmc8, resource: bindings, ignored listing per whitelist Feb 14 17:56:05.938: INFO: namespace e2e-tests-nettest-bhmc8 deletion completed in 6.291513655s junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-testrun.xml000066400000000000000000000027731521256313300260300ustar00rootroot00000000000000 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-unicode.xml000066400000000000000000000013051521256313300257400ustar00rootroot00000000000000 Expected <anonymous>({ 3: 1, Hi: 1, hi: 1, constructor: 1, _: 1, 10€: 2 }) to equal <anonymous>({ 3: 1, hi: 2, constructor: 1, _: 1, 10€: 2 }). /home/gereon/Schreibtisch/evalTest/ex-7_9-moodle-karma-test-execution/test-classes/test_ex4.js:40:50 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/junit-unicode2.xml000066400000000000000000000005161521256313300260250ustar00rootroot00000000000000 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/pytest-binary-names.xml000066400000000000000000000003431521256313300270770ustar00rootroot00000000000000 junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_example_output.py000066400000000000000000000015751521256313300271360ustar00rootroot00000000000000""" A series of tests that produce different example output """ import sys def test_stderr_only(): """ Print some text to stderr :return: """ sys.stderr.write(""" Hello Standard Error ===================================== This is some formatted stderr """) def test_stdout_only(): """ Print some text to stderr :return: """ sys.stdout.write(""" Hello Standard Out ===================================== This is some formatted stdout """) def test_stdoe(): """ Print some stuff to stderr and stdout :return: """ def err(msg): sys.stderr.write(msg) sys.stderr.write("\n") def out(msg): sys.stdout.write(msg) sys.stdout.write("\n") for _ in range(3): for word in ["Hello", "World"]: err("Err " + word) out("Out " + word) junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_junit2html.py000066400000000000000000000110221521256313300261470ustar00rootroot00000000000000""" Test that does nothing other than import """ import os import pytest from .inputfiles import get_filepath from .helpers import run_runner from junit2htmlreport import parser, runner def test_runner_sonobouy(tmpdir): """ Test the stand-alone app with report produced by sonobouy :param tmpdir: py.test tmpdir fixture :return: """ run_runner(tmpdir, "junit-sonobouy.xml") def test_runner_complex(tmpdir): """ Test the stand-alone app with a large fairly complex junit file :param tmpdir: py.test tmpdir fixture :return: """ run_runner(tmpdir, "junit-complex_suites.xml") def test_runner_6700(tmpdir): """ Test the 6700 report I can't remember what is special about this file! :param tmpdir: :return: """ run_runner(tmpdir, "junit-report-6700.xml") def test_runner_unicode(tmpdir): """ Test the stand-alone app with a unicode file (contains a euro symbol) :param tmpdir: :return: """ run_runner(tmpdir, "junit-unicode.xml") def test_runner_testrun(tmpdir): """ Test the stand-alone app with a file rooted at :param tmpdir: :return: """ run_runner(tmpdir, "junit-testrun.xml") def test_runner_merge(tmpdir): """ Test merging multiple files :param tmpdir: :return: """ filenames = ["junit-complex_suites.xml", "junit-cute2.xml", "junit-unicode.xml"] filepaths = [] for filename in filenames: filepaths.append( os.path.join(tmpdir.strpath, get_filepath(filename))) newfile = os.path.join(tmpdir.strpath, "merged.xml") args = ["--merge", newfile] args.extend(filepaths) runner.run(args) assert os.path.exists(newfile) def test_emit_stdio(): """ Test the stand-alone app can generate a page from a report containing stdio text But also save the result in the current folder :return: """ folder = os.path.dirname(__file__) reportfile = os.path.join(folder, "junit-jenkins-stdout.xml") runner.run([reportfile]) htmlfile = os.path.join(folder, "junit-jenkins-stdout.xml.html") assert os.path.exists(htmlfile) with open(htmlfile, "r") as readfile: content = readfile.read() assert "===> Executing test case" in content def test_parser(): """ Test the junit parser directly :return: """ junit = parser.Junit(filename=get_filepath("junit-simple_suite.xml")) assert len(junit.suites) == 1 assert len(junit.suites[0].properties) == 3 junit = parser.Junit(filename=get_filepath("junit-simple_suites.xml")) assert len(junit.suites) == 1 assert len(junit.suites[0].properties) == 3 junit = parser.Junit(filename=get_filepath("junit-complex_suites.xml")) assert len(junit.suites) == 66 junit = parser.Junit(filename=get_filepath("junit-cute2.xml")) assert len(junit.suites) == 6 junit = parser.Junit(filename=get_filepath("junit-unicode.xml")) assert len(junit.suites) == 1 assert len(junit.suites[0].classes) == 2 # different report structure, both files contain unicode symbols junit = parser.Junit(filename=get_filepath("junit-unicode2.xml")) assert len(junit.suites) == 1 assert len(junit.suites[0].classes) == 1 def test_binary_names(): # a test with nonsense binary in the case names junit = parser.Junit(filename=get_filepath("pytest-binary-names.xml")) assert junit def test_parser_stringreader(): """ Test the junit parser when reading strings :return: """ with open(get_filepath("junit-complex_suites.xml"), "r") as data: junit = parser.Junit(xmlstring=data.read()) assert len(junit.suites) == 66 assert junit.suites[0].name == "Untitled suite in /Users/niko/Sites/casperjs/tests/suites/casper/agent.js" assert junit.suites[0].package == "tests/suites/casper/agent" assert junit.suites[0].classes["tests/suites/casper/agent"].cases[1].name == "Default user agent matches /plop/" def test_fail_exit(tmpdir): """ Test the tool exits with an error for --max-failures :return: """ with pytest.raises(SystemExit) as err: run_runner(tmpdir, "junit-unicode.xml", "--summary-matrix", "--max-failures", "1") assert err.value.code != 0 def test_skip_exit(tmpdir): """ Test the tool exits with an error for --max-skipped :return: """ with pytest.raises(SystemExit) as err: run_runner(tmpdir, "junit-axis-windows.xml", "--summary-matrix", "--max-skipped", "1") assert err.value.code != 0junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_junitparser_render.py000066400000000000000000000006751521256313300277700ustar00rootroot00000000000000""" Test that we can load all our input files without error """ import pytest from . import inputfiles from junit2htmlreport import render, parserimpl @pytest.mark.parametrize("filename", inputfiles.get_reports()) def test_load(filename): report = parserimpl.load_report(inputfiles.get_filepath(filename)) assert report is not None doc = render.HTMLReport() doc.load(report, filename) output = str(doc) assert output junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random1.xml000066400000000000000000001035311521256313300267410ustar00rootroot00000000000000 Skipped Failed albatross Failed toast Failed acetone Failed magma Failed hytac Failed socrates Skipped Skipped Failed duessa Skipped Failed avernus Failed tokamak Failed seal Failed teapot Failed bobble Skipped Skipped Skipped Failed terminus Failed yorkshire tea Failed teapot Failed alamo Failed acetone Skipped Failed baccus Failed alamo Failed bobble Failed martham Failed butterfly Failed vattland Skipped Failed storm Failed null Skipped Skipped Skipped Failed storm Failed butterfly Skipped Skipped Skipped Skipped Skipped Failed grno Failed apple Failed moose Failed avernus Failed petrel Skipped Skipped Failed duessa Failed swordfish Skipped Skipped Failed orange Skipped Skipped Skipped Skipped Failed yorkshire tea Skipped Failed granite Skipped Failed albatross Failed albatross Failed null Failed petrel Skipped Skipped Failed panther Skipped Skipped Skipped Failed teapot Failed alamo Failed seal Failed tokamak Skipped Failed razorbill Failed baccus Failed grno Failed vattland Failed walrus Skipped Failed toast Failed martham Skipped Failed mule Skipped Failed butterfly Skipped Failed apple Skipped Skipped Skipped Skipped Failed hytac Failed smartcard Skipped Failed null Failed elwood Skipped Skipped Skipped Failed acetone Skipped Failed sodor Failed storm Skipped Failed acheron Failed moose Failed hytac Failed acetone Failed avernus Skipped Failed beacon Skipped Skipped Failed odracir Failed albatross Failed alamo Skipped Failed smartcard Skipped Failed baccus Skipped Failed null Skipped Failed hytac Skipped Failed orange Failed vulcan Failed winterton Failed corgette Failed granite Failed tokamak Failed walrus Failed potato Failed vulcan Skipped Failed duessa Skipped Failed albatross Skipped Failed bear Failed odracir Skipped Failed albatross Failed apple Skipped Skipped Failed seal Skipped Skipped Failed terminus Failed courier Failed seal Skipped Failed granite Failed albatross Failed bat Failed storm Failed nemesis Skipped Failed vattland Skipped Failed sodor Failed seal Skipped Failed acetone Skipped Skipped Skipped Skipped Failed walrus Failed mule Failed magma Skipped Skipped Skipped Failed razorbill Skipped Skipped Skipped junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random2.xml000066400000000000000000001040111521256313300267340ustar00rootroot00000000000000 Failed yorkshire tea Skipped Skipped Failed toast Skipped Skipped Skipped Failed swordfish Failed odracir Failed potato Skipped Skipped Failed grno Failed odracir Skipped Failed courier Failed apple Failed orange Failed cat Skipped Skipped Failed toast Skipped Failed walrus Skipped Skipped Skipped Failed mule Failed odracir Skipped Failed bat Failed mule Failed sodor Skipped Failed igneous Failed charissa Failed potato Skipped Failed odracir Skipped Skipped Failed bat Failed corgette Failed vulcan Failed moose Failed seal Failed petrel Failed grno Skipped Skipped Skipped Failed seal Skipped Skipped Skipped Failed magma Skipped Skipped Skipped Failed tokamak Skipped Failed butterfly Failed elwood Failed acheron Skipped Skipped Failed albatross Skipped Skipped Skipped Skipped Failed nemesis Failed grno Skipped Skipped Failed winterton Skipped Skipped Skipped Failed acheron Skipped Skipped Skipped Failed albatross Failed manta Failed teapot Failed bobble Skipped Failed acetone Skipped Skipped Skipped Skipped Failed tokamak Skipped Failed genetix Skipped Failed toast Skipped Failed granite Skipped Skipped Failed corgette Failed granite Skipped Failed hytac Skipped Skipped Failed acheron Skipped Failed storm Failed nemesis Failed mule Skipped Skipped Skipped Failed odracir Skipped Failed butterfly Failed manta Failed avernus Failed razorbill Skipped Skipped Skipped Failed smartcard Failed null Failed storm Skipped Skipped Failed genetix Failed nemesis Failed alamo Failed acheron Skipped Skipped Skipped Failed teapot Failed martham Failed teapot Failed martham Failed seal Skipped Skipped Skipped Failed avernus Skipped Failed cat Skipped Failed winterton Failed manta Failed avernus Failed bat Skipped Skipped Skipped Skipped Skipped Skipped Failed genetix Skipped Failed corgette Failed bat Skipped Failed grno Skipped Skipped Skipped Skipped Failed isolus Failed walrus Failed winterton Skipped Skipped Failed nemesis Skipped Skipped Skipped Failed petrel Skipped Skipped Failed smartcard junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random3.xml000066400000000000000000000270431521256313300267460ustar00rootroot00000000000000 Skipped Skipped Failed swordfish Failed seal Failed bobble Failed courier Failed vulcan Skipped Skipped Skipped Failed alamo Failed seal Failed null Skipped Skipped Failed yorkshire tea Failed genetix Failed vulcan Failed sodor Skipped Failed vattland Skipped Failed needlefish Skipped Failed avernus Failed socrates Failed sodor Failed cat Skipped Skipped Failed grno Skipped Skipped Failed manta Skipped Skipped Failed petrel Skipped Failed butterfly Failed alamo Failed panther Failed seal Skipped Failed charissa Failed albatross Skipped Failed martham Failed smartcard Failed elwood Failed grno Skipped Skipped Failed socrates Skipped Skipped Failed genetix Failed corgette Failed corgette Failed magma Failed yorkshire tea Skipped Skipped Skipped Skipped Failed nemesis junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random4.xml000066400000000000000000000370341521256313300267500ustar00rootroot00000000000000 Failed genetix Failed duessa Skipped Failed petrel Skipped Skipped Skipped Skipped Failed apple Failed moose Skipped Failed socrates Failed magma Skipped Failed teapot Failed razorbill Skipped Skipped Failed teapot Skipped Failed potato Failed storm Skipped Failed avernus Failed albatross Skipped Skipped Failed isolus Skipped Failed potato Failed isolus Failed nemesis Skipped Failed toast Failed grno Failed isolus Skipped Skipped Skipped Failed cat Skipped Failed sodor Failed null Failed panther Skipped Failed duessa Failed hytac Failed mule Skipped Failed seal Skipped Failed null Failed martham Skipped Failed bobble Skipped Failed sodor Skipped Skipped Failed walrus Skipped Skipped Skipped Failed bat Skipped Failed tokamak Skipped Failed terminus Failed corgette Skipped Failed vulcan Failed null Failed panther Failed petrel Skipped Failed igneous Skipped Skipped Skipped Failed null Skipped Skipped Failed duessa junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random5.xml000066400000000000000000000246431521256313300267530ustar00rootroot00000000000000 Skipped Skipped Failed needlefish Skipped Skipped Failed moose Failed baccus Failed seal Failed razorbill Failed winterton Failed sodor Failed panther Skipped Skipped Failed elwood Skipped Failed duessa Failed vattland Failed avernus Failed moose Skipped Failed avernus Skipped Failed duessa Failed sodor Failed moose Failed acetone Skipped Skipped Skipped Failed acheron Skipped Skipped Skipped Failed sodor Failed grno Skipped Failed yorkshire tea Failed needlefish Failed igneous Skipped Failed granite Failed charissa Failed walrus Failed apple Skipped Failed butterfly Failed acheron Skipped Skipped Failed swordfish Failed swordfish Skipped Skipped Skipped junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random6.xml000066400000000000000000000240451521256313300267500ustar00rootroot00000000000000 Failed terminus Failed teapot Failed teapot Failed baccus Failed toast Failed magma Skipped Skipped Failed bobble Skipped Skipped Failed elwood Failed elwood Failed cat Failed toast Failed potato Skipped Skipped Failed terminus Skipped Skipped Failed alamo Skipped Failed grno Failed acetone Failed avernus Failed acheron Failed toast Failed isolus Failed beacon Skipped Failed petrel Skipped Failed teapot Skipped Failed smartcard Skipped Failed martham Failed sodor Failed duessa Skipped Skipped Skipped Failed needlefish Failed acetone Failed igneous Skipped Failed walrus Failed bobble Failed panther Skipped Skipped Failed genetix Skipped Failed butterfly Skipped Failed nemesis junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_large_random7.xml000066400000000000000000000236151521256313300267530ustar00rootroot00000000000000 Failed grno Failed mule Failed tokamak Failed corgette Failed bear Failed toast Skipped Failed mule Skipped Skipped Skipped Skipped Failed panther Failed storm Skipped Failed elwood Failed socrates Skipped Failed albatross Failed moose Failed bat Failed terminus Skipped Failed tokamak Failed alamo Failed bear Skipped Skipped Skipped Skipped Skipped Skipped Failed elwood Failed alamo Failed igneous Failed charissa Failed potato Skipped Failed martham Skipped Failed winterton Skipped Failed null Skipped Skipped Skipped Skipped Failed vulcan Skipped Failed granite Skipped junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_matrix.py000066400000000000000000000054031521256313300253610ustar00rootroot00000000000000""" Test the matrix functionality """ from junit2htmlreport import matrix from junit2htmlreport.matrix import PARTIAL_PASS, PARTIAL_FAIL, TOTAL_FAIL, UNTESTED from junit2htmlreport.parser import PASSED, SKIPPED, FAILED, UNKNOWN from .inputfiles import get_filepath def test_combined_result(): """ Test that the combined result string and short result value are correct :return: """ textmatrix = matrix.TextReportMatrix() short, result = textmatrix.combined_result([PASSED, SKIPPED]) assert short == textmatrix.short_outcome(PASSED) assert result == PASSED.title() short, result = textmatrix.combined_result([PASSED, FAILED]) assert short == textmatrix.short_outcome(FAILED) assert result == FAILED.title() short, result = textmatrix.combined_result([PARTIAL_PASS]) assert short == textmatrix.short_outcome(PARTIAL_PASS) assert result == PARTIAL_PASS.title() short, result = textmatrix.combined_result([TOTAL_FAIL]) assert short == textmatrix.short_outcome(TOTAL_FAIL) assert result == TOTAL_FAIL.title() short, result = textmatrix.combined_result([FAILED, FAILED]) assert short == textmatrix.short_outcome(FAILED) assert result == FAILED.title() short, result = textmatrix.combined_result([PASSED]) assert short == textmatrix.short_outcome(PASSED) assert result == PASSED.title() short, result = textmatrix.combined_result([SKIPPED, SKIPPED]) assert short == textmatrix.short_outcome(UNTESTED) assert result == UNTESTED.title() short, result = textmatrix.combined_result([]) assert '?' == textmatrix.short_outcome(None) # type: ignore assert result == UNKNOWN.title() def test_matrix_load(tmpdir): """ Test loading multiple reports :return: """ textmatrix = matrix.TextReportMatrix() textmatrix.add_report(get_filepath("junit-simple_suite.xml")) textmatrix.add_report(get_filepath("junit-simple_suites.xml")) textmatrix.add_report(get_filepath("junit-unicode.xml")) textmatrix.add_report(get_filepath("junit-unicode2.xml")) textmatrix.add_report(get_filepath("junit-cute2.xml")) textmatrix.add_report(get_filepath("junit-jenkins-stdout.xml")) assert len(textmatrix.reports) == 6 result = textmatrix.summary() print(result) def test_matrix_html(tmpdir): """ Test loading multiple reports :return: """ htmatrix = matrix.HtmlReportMatrix(str(tmpdir)) htmatrix.add_report(get_filepath("junit-simple_suite.xml")) htmatrix.add_report(get_filepath("junit-simple_suites.xml")) htmatrix.add_report(get_filepath("junit-unicode.xml")) htmatrix.add_report(get_filepath("junit-axis-linux.xml")) assert len(htmatrix.reports) == 4 result = htmatrix.summary() assert result.endswith("") junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_matrix_stdout.py000066400000000000000000000005031521256313300267570ustar00rootroot00000000000000# -*- coding: utf-8 -*- from junit2htmlreport import runner from .inputfiles import get_filepath def test_matrix_stdout(capsys): runner.run(["--summary-matrix", get_filepath("junit-unicode.xml")]) out, err = capsys.readouterr() assert u"A Class with a cent ¢" in out assert u"Euro € Test Case" in outjunit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_merge.py000066400000000000000000000037361521256313300251630ustar00rootroot00000000000000"""Test the merge utility""" import os from junit2htmlreport import render, runner from pathlib import Path TESTS = Path(__file__).parent def test_merge_folder(tmp_path): outfile = os.path.join(tmp_path, "test-merged.xml") outmatrix = os.path.join(tmp_path, "matrix.html") runner.run(["--merge", outfile, str(TESTS)]) runner.run(["--theme", "dark", outfile]) assert os.path.exists(f"{outfile}.html") tests = list([str(x) for x in TESTS.glob("*.xml")]) runner.run(["--report-matrix", outmatrix] + tests) assert os.path.exists(outmatrix) def test_report_matrix_theme_dir_applies_to_individual_reports(tmp_path, monkeypatch): monkeypatch.setattr(runner, "loader_factory", render.LoaderFactory()) theme_dir = tmp_path / "theme" theme_dir.mkdir() (theme_dir / "base.html").write_text( """ custom theme marker {% block content %}{% endblock %} """.strip(), encoding="utf-8", ) (theme_dir / "report.html").write_text( """ {% extends "base.html" %} {% block content %} custom report marker {% endblock %} """.strip(), encoding="utf-8", ) (theme_dir / "matrix.html").write_text( """ {% extends "base.html" %} {% block content %} custom matrix marker {% endblock %} """.strip(), encoding="utf-8", ) (theme_dir / "styles.css").write_text("", encoding="utf-8") outmatrix = tmp_path / "matrix.html" report = TESTS / "junit-simple_suite.xml" runner.run(["--theme-dir", str(theme_dir), "--report-matrix", str(outmatrix), str(report)]) report_html = tmp_path / "junit-simple_suite.xml.html" matrix_content = outmatrix.read_text(encoding="utf-8") report_content = report_html.read_text(encoding="utf-8") assert "custom matrix marker" in matrix_content assert "custom report marker" in report_content assert "custom theme marker" in matrix_content assert "custom theme marker" in report_content junit2html-v31.1.4-c69ae31315cb751dae730448272661cc7c73264c/tests/test_parser_api.py000066400000000000000000000026271521256313300262070ustar00rootroot00000000000000from xml.etree import ElementTree from junit2htmlreport import parser as j2h def test_public_api(): container = j2h.Junit(xmlstring=""" """) container.filename = "test_results.xml" document = j2h.Suite() container.suites = [document] document.name = "test report" document.duration = 0.1 document.package = "com.tests" first = j2h.Class() first.name = "myclass" document.classes[first.name] = first test1 = j2h.Case() test1.name = "test_one" test1.duration = 1.1 test1.testclass = first first.cases.append(test1) test2 = j2h.Case() test2.name = "test_two" test2.duration = 1.2 test2.testclass = first first.cases.append(test2) skipped1 = j2h.Case() skipped1.name = "test_skippy" skipped1.duration = 1.3 skipped1.testclass = first skipped1.skipped = "test skipped" skipped1.skipped_msg = "test was skipped at runtime" first.cases.append(skipped1) failed1 = j2h.Case() failed1.name = "test_bad" failed1.duration = 1.4 failed1.testclass = first failed1.failure = "test failed" failed1.failure_msg = "an exception happened" first.cases.append(failed1) html = container.html() assert html assert "