pax_global_header00006660000000000000000000000064150747642470014532gustar00rootroot0000000000000052 comment=dbefdd95c14bf34d22cf6db91d77d6452058dc2f junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/000077500000000000000000000000001507476424700220535ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/.gitignore000066400000000000000000000001741507476424700240450ustar00rootroot00000000000000*.pyc build/ dist/ .idea/ .cache/ .venv/ venv/ MANIFEST .coverage *.xml *.xml.html *.egg-info *.code-workspace *.egg-info/ junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/.gitlab-ci.yml000066400000000000000000000022001507476424700245010ustar00rootroot00000000000000.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 --report-matrix tests/matrix-example.html tests/junit-axis-linux.xml tests/junit-axis-solaris.xml tests/junit-axis-windows.xml - python -m junit2htmlreport --merge junit2html-merged-example.xml tests/junit-unicode.xml tests/junit-unicode2.xml tests/junit-cute2.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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/.travis.yml000066400000000000000000000001641507476424700241650ustar00rootroot00000000000000language: python python: - "2.7" install: - python setup.py install script: python -m pytest junit2htmlreport junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/LICENSE000066400000000000000000000020531507476424700230600ustar00rootroot00000000000000MIT 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/MANIFEST.in000066400000000000000000000001111507476424700236020ustar00rootroot00000000000000include junit2html README recursive-include junit2htmlreport/* junit2htmljunit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/Pipfile000066400000000000000000000002121507476424700233610ustar00rootroot00000000000000[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.8" junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/README.md000066400000000000000000000044471507476424700233430ustar00rootroot00000000000000junit2html 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=python36 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=python39 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2html000077500000000000000000000001121507476424700240730ustar00rootroot00000000000000#!/usr/bin/env python from junit2htmlreport import runner runner.start() junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/000077500000000000000000000000001507476424700254075ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/__init__.py000066400000000000000000000000001507476424700275060ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/__main__.py000066400000000000000000000001521507476424700274770ustar00rootroot00000000000000import sys if sys.version_info >= (3, 0): from . import runner else: import runner runner.start() junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/case_result.py000066400000000000000000000007321507476424700302740ustar00rootroot00000000000000from 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/matrix.py000066400000000000000000000235341507476424700272740ustar00rootroot00000000000000""" 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 = {} 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['ok', '/', '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] = {} 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): """ Combone 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 results: if CaseResult.PASSED in results: if CaseResult.FAILED in results: return self.short_outcome(CaseResult.PARTIAL_FAIL), CaseResult.PARTIAL_FAIL.title() return self.short_outcome(CaseResult.PASSED), CaseResult.PASSED.title() if CaseResult.FAILED in results: return self.short_outcome(CaseResult.FAILED), CaseResult.FAILED.title() if CaseResult.SKIPPED in results: return self.short_outcome(CaseResult.UNTESTED), CaseResult.UNTESTED.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() return " ", "" class HTMLMatrix: def __init__(self, matrix: "ReportMatrix"): self.matrix = matrix 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, 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) # 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['ok', '/', 's', 'f', 'F', '%', 'X', 'U', '?']": if outcome == CaseResult.PASSED: return "ok" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/merge.py000066400000000000000000000057141507476424700270670ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/parser.py000066400000000000000000000362331507476424700272640ustar00rootroot00000000000000""" Parse a junit report file into a family of objects """ from __future__ import unicode_literals 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()) 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() 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 = [] 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/parserimpl.py000066400000000000000000000004521507476424700301400ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/render.py000066400000000000000000000103021507476424700272340ustar00rootroot00000000000000""" 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): self.show_toc = show_toc 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) junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/report.css000066400000000000000000000030321507476424700274320ustar00rootroot00000000000000h1 { 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/runner.py000066400000000000000000000112011507476424700272650ustar00rootroot00000000000000""" 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", dest="text_matrix", action="store_true", default=False, help="Render multiple result files to the console") PARSER.add_argument("--report-matrix", 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/000077500000000000000000000000001507476424700274055ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/dark/000077500000000000000000000000001507476424700303265ustar00rootroot00000000000000base.html000066400000000000000000000004511507476424700320470ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/dark {{title}} {% block content %} {% endblock %} matrix.html000066400000000000000000000053531507476424700324470ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/dark{% extends "base.html" %} {% block content %}

Reports Matrix

{% set report_names = matrix.report_order() %} {% set n_reports = report_names.__len__() %} {% if n_reports > 0 %} {% endif %} {% for i in range(n_reports) %} {% for n in range(i) %} {% endfor %} {% endfor %} {% for n in range(n_reports + 1) %} {% endfor %} {% for classname in matrix.classes %} {% for n in range(n_reports) %} {% endfor %} {% for casename in matrix.casenames[classname] %} {% set xcase = matrix.cases[classname][casename] %} {% for n in range(n_reports) %} {% set axis = report_names[n] %} {% endfor %} {% endfor %} {% endfor %}
{% for outcome in matrix.result_stats %} {% endfor %}
{{outcome.title()}} {{matrix.result_stats[outcome]}}
{{matrix.short_axis(report_names[n_reports - 1 - i])}}
{{classname}}
{{casename}} {{ matrix.combined_result_list(classname, casename)[1] }} {% if axis in xcase %} {{ matrix.short_outcome(xcase[axis].outcome()) }} {% else %}   {% endif %}
{% endblock %} report.html000066400000000000000000000113731507476424700324550ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/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.css000066400000000000000000000067371507476424700323210ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/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; } .matrix-table { table-layout: fixed; border-spacing: 0; width: available; margin-left: 1em; } .matrix-table td { vertical-align: center; } .matrix-table td:last-child { width: 0; } .matrix-table tr:hover { background-color: darkgoldenrod; } .matrix-axis-name { white-space: nowrap; padding-right: 0.5em; border-left: 1px solid black; border-top: 1px solid black; text-align: right; } .matrix-axis-line { border-left: 1px solid black; width: 0.5em; } .matrix-classname { text-align: left; width: 100%; border-top: 2px solid grey; border-bottom: 1px solid silver; } .matrix-casename { text-align: left; font-weight: normal; font-style: italic; padding-left: 1em; border-bottom: 1px solid silver; } .matrix-result { display: block; width: 1em; text-align: center; padding: 1mm; margin: 0; } .matrix-result-combined { white-space: nowrap; padding-right: 0.2em; text-align: right; } .matrix-result-failed { background-color: palevioletred; } .matrix-result-passed { background-color: seagreen; } .matrix-result-skipped { background-color: darkgoldenrod; } .matrix-even { background-color: dimgrey; } junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/default/000077500000000000000000000000001507476424700310315ustar00rootroot00000000000000base.html000066400000000000000000000004511507476424700325520ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/default {{title}} {% block content %} {% endblock %} matrix.html000066400000000000000000000053531507476424700331520ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/templates/default{% extends "base.html" %} {% block content %}

Reports Matrix

{% set report_names = matrix.report_order() %} {% set n_reports = report_names.__len__() %} {% if n_reports > 0 %} {% endif %} {% for i in range(n_reports) %} {% for n in range(i) %} {% endfor %} {% endfor %} {% for n in range(n_reports + 1) %} {% endfor %} {% for classname in matrix.classes %} {% for n in range(n_reports) %} {% endfor %} {% for casename in matrix.casenames[classname] %} {% set xcase = matrix.cases[classname][casename] %} {% for n in range(n_reports) %} {% set axis = report_names[n] %} {% endfor %} {% endfor %} {% endfor %}
{% for outcome in matrix.result_stats %} {% endfor %}
{{outcome.title()}} {{matrix.result_stats[outcome]}}
{{matrix.short_axis(report_names[n_reports - 1 - i])}}
{{classname}}
{{casename}} {{ matrix.combined_result_list(classname, casename)[1] }} {% if axis in xcase %} {{ matrix.short_outcome(xcase[axis].outcome()) }} {% else %}   {% endif %}
{% endblock %} report.html000066400000000000000000000113731507476424700331600ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/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.css000066400000000000000000000066431507476424700330200ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/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; } .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; } .matrix-table { table-layout: fixed; border-spacing: 0; width: available; margin-left: 1em; } .matrix-table td { vertical-align: center; } .matrix-table td:last-child { width: 0; } .matrix-table tr:hover { background-color: yellow; } .matrix-axis-name { white-space: nowrap; padding-right: 0.5em; border-left: 1px solid black; border-top: 1px solid black; text-align: right; } .matrix-axis-line { border-left: 1px solid black; width: 0.5em; } .matrix-classname { text-align: left; width: 100%; border-top: 2px solid grey; border-bottom: 1px solid silver; } .matrix-casename { text-align: left; font-weight: normal; font-style: italic; padding-left: 1em; border-bottom: 1px solid silver; } .matrix-result { display: block; width: 1em; text-align: center; padding: 1mm; margin: 0; } .matrix-result-combined { white-space: nowrap; padding-right: 0.2em; text-align: right; } .matrix-result-failed { background-color: lightcoral; } .matrix-result-passed { background-color: lightgreen; } .matrix-result-skipped { background-color: lightyellow; } .matrix-even { background-color: lightgray; } junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/junit2htmlreport/textutils.py000066400000000000000000000004561507476424700300330ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/pyproject.toml000066400000000000000000000001311507476424700247620ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta"junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/pytest.ini000066400000000000000000000001071507476424700241020ustar00rootroot00000000000000[pytest] addopts = --junit-xml pytest-results.xml junit_family = xunit2junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/setup-venv.sh000066400000000000000000000001301507476424700245150ustar00rootroot00000000000000#!/bin/bash set -e rm -rf venv python3 -m venv venv . venv/bin/activate pip install -e .junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/setup.cfg000066400000000000000000000012431507476424700236740ustar00rootroot00000000000000[metadata] name = junit2html version = 31.0.5 description = Generate HTML reports from Junit results long_description = Genearate 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/000077500000000000000000000000001507476424700232155ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/.gitignore000066400000000000000000000000071507476424700252020ustar00rootroot00000000000000*.html junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/__init__.py000066400000000000000000000000001507476424700253140ustar00rootroot00000000000000junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/helpers.py000066400000000000000000000015011507476424700252260ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/inputfiles.py000066400000000000000000000004631507476424700257540ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-axis-linux.xml000066400000000000000000000010101507476424700271570ustar00rootroot00000000000000 Assertion failed junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-axis-solaris.xml000066400000000000000000000010101507476424700274740ustar00rootroot00000000000000 Assertion failed junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-axis-windows.xml000066400000000000000000000007301507476424700275220ustar00rootroot00000000000000 junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-complex_suites.xml000066400000000000000000002611301507476424700301340ustar00rootroot00000000000000 junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-cute2.xml000066400000000000000000000061171507476424700261150ustar00rootroot00000000000000 SetLarger: your == T1 "Set value to -1 should throw" "Set value overflow" "Set use to 1 more than alloc" junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-jenkins-stdout.xml000066400000000000000000000124351507476424700300540ustar00rootroot00000000000000 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-report-6700.xml000066400000000000000000000021031507476424700267670ustar00rootroot00000000000000 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-simple_suite.xml000066400000000000000000000011571507476424700275740ustar00rootroot00000000000000 Assertion failed junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-simple_suites.xml000066400000000000000000000012611507476424700277530ustar00rootroot00000000000000 Assertion failed junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-sonobouy.xml000066400000000000000000005434351507476424700267610ustar00rootroot00000000000000 /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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-testrun.xml000066400000000000000000000027731507476424700266030ustar00rootroot00000000000000 junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-unicode.xml000066400000000000000000000013051507476424700265130ustar00rootroot00000000000000 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/junit-unicode2.xml000066400000000000000000000005161507476424700266000ustar00rootroot00000000000000 junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/pytest-binary-names.xml000066400000000000000000000003431507476424700276520ustar00rootroot00000000000000 junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_example_output.py000066400000000000000000000015751507476424700277110ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_junit2html.py000066400000000000000000000110221507476424700267220ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_junitparser_render.py000066400000000000000000000006751507476424700305430ustar00rootroot00000000000000""" 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_matrix.py000066400000000000000000000054171507476424700261410ustar00rootroot00000000000000""" 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(PARTIAL_FAIL) assert result == PARTIAL_FAIL.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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_matrix_stdout.py000066400000000000000000000005031507476424700275320ustar00rootroot00000000000000# -*- 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.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_merge.py000066400000000000000000000010631507476424700257250ustar00rootroot00000000000000"""Test the merge utility""" import os from junit2htmlreport import 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) junit2html-v31.0.5-dbefdd95c14bf34d22cf6db91d77d6452058dc2f/tests/test_parser_api.py000066400000000000000000000026271507476424700267620ustar00rootroot00000000000000from 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 "