├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── README.md ├── junit2html ├── junit2htmlreport ├── __init__.py ├── __main__.py ├── case_result.py ├── common.py ├── matrix.py ├── merge.py ├── parser.py ├── parserimpl.py ├── render.py ├── report.css ├── runner.py ├── templates │ ├── base.html │ ├── matrix.html │ ├── report.html │ └── styles.css └── textutils.py ├── pytest.ini ├── setup-venv.sh ├── setup.py └── tests ├── .gitignore ├── __init__.py ├── helpers.py ├── inputfiles.py ├── junit-axis-linux.xml ├── junit-axis-solaris.xml ├── junit-axis-windows.xml ├── junit-complex_suites.xml ├── junit-cute2.xml ├── junit-jenkins-stdout.xml ├── junit-report-6700.xml ├── junit-simple_suite.xml ├── junit-simple_suites.xml ├── junit-sonobouy.xml ├── junit-testrun.xml ├── junit-unicode.xml ├── junit-unicode2.xml ├── pytest-binary-names.xml ├── test_example_output.py ├── test_junit2html.py ├── test_junitparser_render.py ├── test_matrix.py ├── test_matrix_stdout.py ├── test_parser_api.py └── test_tojunit.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | .idea/ 4 | .cache/ 5 | .venv/ 6 | venv/ 7 | MANIFEST 8 | .coverage 9 | *.xml 10 | *.xml.html 11 | *.egg-info 12 | *.code-workspace 13 | *.egg-info 14 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | .common: 2 | before_script: 3 | - pip install pytest wheel 4 | script: 5 | - python -m pip install -e . 6 | - python -m pytest -v . --junitxml=junit2html-job-${CI_JOB_NAME}.xml 7 | - python -m junit2htmlreport junit2html-job-${CI_JOB_NAME}.xml 8 | - python -m junit2htmlreport --report-matrix tests/matrix-example.html tests/junit-axis-linux.xml tests/junit-axis-solaris.xml tests/junit-axis-windows.xml 9 | - python -m junit2htmlreport --merge junit2html-merged-example.xml tests/junit-unicode.xml tests/junit-unicode2.xml tests/junit-cute2.xml 10 | - python -m junit2htmlreport junit2html-merged-example.xml 11 | - python setup.py bdist_wheel 12 | - python -m junit2htmlreport --summary-matrix - < junit2html-job-${CI_JOB_NAME}.xml 13 | artifacts: 14 | paths: 15 | - junit2html*.xml* 16 | - tests/*.html 17 | - dist/*.whl 18 | reports: 19 | junit: 20 | - junit2html-job-*.xml 21 | 22 | 23 | python36: 24 | image: python:3.6 25 | extends: .common 26 | 27 | python38: 28 | image: python:3.8 29 | extends: .common 30 | 31 | python39: 32 | image: python:3.9 33 | extends: .common 34 | 35 | coverage: 36 | image: python:3.9 37 | script: 38 | - pip install pytest pytest-cov 39 | - pip install -e . 40 | - python3 -m pytest --cov-fail-under=86 --cov=junit2htmlreport . -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | install: 6 | - python setup.py install 7 | 8 | script: python -m pytest junit2htmlreport 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ian Norton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include junit2html README 2 | recursive-include junit2htmlreport/* junit2html -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.8" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | junit2html by Ian Norton 2 | ------------------------------------------------------------- 3 | 4 | Hosted at https://gitlab.com/inorton/junit2html 5 | 6 | This is a simple self-contained python tool to 7 | produce a single html file from a single junit xml file. 8 | 9 | ## Basic Usage: 10 | 11 | ``` 12 | $ junit2html JUNIT_XML_FILE [NEW_HTML_FILE] 13 | ``` 14 | or 15 | ``` 16 | $ python -m junit2htmlreport JUNIT_XML_FILE [NEW_HTML_FILE] 17 | ``` 18 | 19 | eg: 20 | 21 | ``` 22 | $ junit2html pytest-results.xml testrun.html 23 | ``` 24 | or 25 | ``` 26 | $ python -m junit2htmlreport pytest-results.xml 27 | ``` 28 | 29 | ## Advanced Usage: 30 | 31 | Render Text summary of results 32 | 33 | ``` 34 | junit2html mytest-results.xml --summary-matrix 35 | ``` 36 | 37 | Render Text sumamry of results and exit non-zero on failures 38 | 39 | ``` 40 | junit2html --summary-matrix ./tests/junit-unicode.xml --max-failures 1 41 | ``` 42 | 43 | 44 | # Installation 45 | 46 | ``` 47 | $ sudo python setup.py install 48 | ``` 49 | or 50 | ``` 51 | $ sudo pip install junit2html 52 | ``` 53 | 54 | ## Example Outputs 55 | 56 | You can see junit2html's own test report output content at: 57 | https://gitlab.com/inorton/junit2html/-/jobs/artifacts/master/browse?job=python36 58 | 59 | An an example of the "matrix" report output can be found at: 60 | https://gitlab.com/inorton/junit2html/-/jobs/artifacts/master/file/tests/matrix-example.html?job=python39 61 | 62 | 63 | About Junit 64 | ----------- 65 | 66 | Junit is a widely used java test framework, it happens to produce a fairly 67 | generic formatted test report and many non-java things produce the same files 68 | (eg py.test) or can be converted quite easily to junit xml (cunit reports via 69 | xslt). The report files are understood by many things like Jenkins and various 70 | IDEs. 71 | 72 | The format of junit files is described here: http://llg.cubic.org/docs/junit/ 73 | 74 | Source and Releases 75 | ------------------- 76 | 77 | Junit2html is maintained on gitlab at https://gitlab.com/inorton/junit2html 78 | 79 | The current master build status of junit2html is: 80 | [![pipeline status](https://gitlab.com/inorton/junit2html/badges/master/pipeline.svg)](https://gitlab.com/inorton/junit2html/commits/master) 81 | 82 | The current coverage status is: 83 | [![coverage report](https://gitlab.com/inorton/junit2html/badges/master/coverage.svg)](https://gitlab.com/inorton/junit2html/commits/master) 84 | 85 | 86 | 87 | Releases are availible via Pypi using pip 88 | 89 | 90 | -------------------------------------------------------------------------------- /junit2html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from junit2htmlreport import runner 4 | runner.start() 5 | -------------------------------------------------------------------------------- /junit2htmlreport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inorton/junit2html/b821eb1a79f25a2efa9bd54d147e3101094ff5fb/junit2htmlreport/__init__.py -------------------------------------------------------------------------------- /junit2htmlreport/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if sys.version_info >= (3, 0): 3 | from . import runner 4 | else: 5 | import runner 6 | runner.start() 7 | -------------------------------------------------------------------------------- /junit2htmlreport/case_result.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class CaseResult(str, Enum): 5 | UNTESTED = "untested" 6 | PARTIAL_PASS = "partial pass" 7 | PARTIAL_FAIL = "partial failure" 8 | TOTAL_FAIL = "total failure" 9 | FAILED = "failed" # the test failed 10 | SKIPPED = "skipped" # the test was skipped 11 | PASSED = "passed" # the test completed successfully 12 | ABSENT = "absent" # the test was known but not run/failed/skipped 13 | UNKNOWN = "" 14 | 15 | def __str__(self) -> str: 16 | return self.value 17 | -------------------------------------------------------------------------------- /junit2htmlreport/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common code between JUnit2HTML matrix and merge classes 3 | """ 4 | from __future__ import print_function 5 | 6 | from typing import TYPE_CHECKING 7 | 8 | from .parser import Case, Junit 9 | 10 | if TYPE_CHECKING: # pragma: no cover 11 | from typing import Dict, List 12 | 13 | 14 | class ReportContainer(object): 15 | """ 16 | Hold one or more reports 17 | """ 18 | reports: "Dict[str, Junit]" 19 | 20 | def __init__(self): 21 | self.reports = {} 22 | 23 | def add_report(self, filename: str) -> None: 24 | raise NotImplementedError() 25 | 26 | def failures(self): 27 | """ 28 | Return all the failed test cases 29 | :return: 30 | """ 31 | found: "List[Case]" = [] 32 | for report in self.reports: 33 | for suite in self.reports[report].suites: 34 | found.extend(suite.failed()) 35 | 36 | return found 37 | 38 | def skips(self): 39 | """ 40 | Return all the skipped test cases 41 | :return: 42 | """ 43 | found: "List[Case]" = [] 44 | for report in self.reports: 45 | for suite in self.reports[report].suites: 46 | found.extend(suite.skipped()) 47 | return found 48 | -------------------------------------------------------------------------------- /junit2htmlreport/matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Handle multiple parsed junit reports 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | from typing import TYPE_CHECKING 8 | 9 | from . import parser 10 | from .case_result import CaseResult 11 | from .common import ReportContainer 12 | from .render import HTMLMatrix 13 | 14 | UNTESTED = CaseResult.UNTESTED 15 | PARTIAL_PASS = CaseResult.PARTIAL_PASS 16 | PARTIAL_FAIL = CaseResult.PARTIAL_FAIL 17 | TOTAL_FAIL = CaseResult.TOTAL_FAIL 18 | 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from .parser import Case, Class 22 | from typing import Dict, List, Optional, Any, Literal 23 | 24 | 25 | class ReportMatrix(ReportContainer): 26 | """ 27 | Load and handle several report files 28 | """ 29 | cases: "Dict[str, Dict[str, Dict[str, Case]]]" 30 | classes: "Dict[str, Dict[str, Class]]" 31 | casenames: "Dict[str, List[str]]" 32 | result_stats: "Dict[CaseResult, int]" 33 | case_results: "Dict[str, Dict[str, List[CaseResult]]]" 34 | 35 | def __init__(self): 36 | super(ReportMatrix, self).__init__() 37 | self.cases = {} 38 | self.classes = {} 39 | self.casenames = {} 40 | self.result_stats = {} 41 | self.case_results = {} 42 | 43 | def add_case_result(self, case: "Case"): 44 | if case.testclass is None or case.testclass.name is None: 45 | testclass = "" 46 | else: 47 | testclass = case.testclass.name 48 | casename = "" if case.name is None else case.name 49 | if testclass not in self.case_results: 50 | self.case_results[testclass] = {} 51 | if casename not in self.case_results[testclass]: 52 | self.case_results[testclass][casename] = [] 53 | self.case_results[testclass][casename].append(case.outcome()) 54 | 55 | def report_order(self): 56 | return sorted(self.reports.keys()) 57 | 58 | def short_outcome(self, outcome: CaseResult) -> "Literal['ok', '/', 's', 'f', 'F', '%', 'X', 'U', '?']": 59 | if outcome == CaseResult.PASSED: 60 | return "/" 61 | elif outcome == CaseResult.SKIPPED: # pragma: no cover 62 | return "s" # currently unused because SKIPPED returns UNTESTED 63 | elif outcome == CaseResult.FAILED: 64 | return "f" 65 | elif outcome == CaseResult.TOTAL_FAIL: 66 | return "F" 67 | elif outcome == CaseResult.PARTIAL_PASS: 68 | return "%" 69 | elif outcome == CaseResult.PARTIAL_FAIL: 70 | return "X" 71 | elif outcome == CaseResult.UNTESTED: 72 | return "U" 73 | 74 | return "?" 75 | 76 | def add_report(self, filename: str): 77 | """ 78 | Load a report into the matrix 79 | :param filename: 80 | :return: 81 | """ 82 | parsed = parser.Junit(filename=filename) 83 | filename = os.path.basename(filename) 84 | self.reports[filename] = parsed 85 | 86 | for suite in parsed.suites: 87 | for testclass in suite.classes: 88 | if testclass not in self.classes: 89 | self.classes[testclass] = {} 90 | if testclass not in self.casenames: 91 | self.casenames[testclass] = list() 92 | self.classes[testclass][filename] = suite.classes[testclass] 93 | 94 | for testcase in self.classes[testclass][filename].cases: 95 | name = "" if testcase.name is None else testcase.name.strip() 96 | if name not in self.casenames[testclass]: 97 | self.casenames[testclass].append(name) 98 | 99 | if testclass not in self.cases: 100 | self.cases[testclass] = {} 101 | if name not in self.cases[testclass]: 102 | self.cases[testclass][name] = {} 103 | self.cases[testclass][name][filename] = testcase 104 | 105 | outcome = testcase.outcome() 106 | self.add_case_result(testcase) 107 | 108 | self.result_stats[outcome] = 1 + self.result_stats.get( 109 | outcome, 0) 110 | 111 | def summary(self) -> str: 112 | """ 113 | Render a summary of the matrix 114 | :return: 115 | """ 116 | raise NotImplementedError() 117 | 118 | def combined_result_list(self, classname: str, casename: str): 119 | """ 120 | Combone the result of all instances of the given case 121 | :param classname: 122 | :param casename: 123 | :return: 124 | """ 125 | if classname in self.case_results: 126 | if casename in self.case_results[classname]: 127 | results = self.case_results[classname][casename] 128 | return self.combined_result(results) 129 | 130 | return " ", "" 131 | 132 | def combined_result(self, results: "List[CaseResult]"): 133 | """ 134 | Given a list of results, produce a "combined" overall result 135 | :param results: 136 | :return: 137 | """ 138 | if results: 139 | if CaseResult.PASSED in results: 140 | if CaseResult.FAILED in results: 141 | return self.short_outcome(CaseResult.PARTIAL_FAIL), CaseResult.PARTIAL_FAIL.title() 142 | return self.short_outcome(CaseResult.PASSED), CaseResult.PASSED.title() 143 | 144 | if CaseResult.FAILED in results: 145 | return self.short_outcome(CaseResult.FAILED), CaseResult.FAILED.title() 146 | if CaseResult.SKIPPED in results: 147 | return self.short_outcome(CaseResult.UNTESTED), CaseResult.UNTESTED.title() 148 | if CaseResult.PARTIAL_PASS in results: 149 | return self.short_outcome(CaseResult.PARTIAL_PASS), CaseResult.PARTIAL_PASS.title() 150 | if CaseResult.TOTAL_FAIL in results: 151 | return self.short_outcome(CaseResult.TOTAL_FAIL), CaseResult.TOTAL_FAIL.title() 152 | return " ", "" 153 | 154 | 155 | class HtmlReportMatrix(ReportMatrix): 156 | """ 157 | Render a matrix report as html 158 | """ 159 | 160 | outdir: str 161 | 162 | def __init__(self, outdir: str): 163 | super(HtmlReportMatrix, self).__init__() 164 | self.outdir = outdir 165 | 166 | def add_report(self, filename: str, show_toc: bool=True): 167 | """ 168 | Load a report 169 | """ 170 | super(HtmlReportMatrix, self).add_report(filename) 171 | basename = os.path.basename(filename) 172 | # make the individual report too 173 | report = self.reports[basename].html(show_toc=show_toc) 174 | if self.outdir != "" and not os.path.exists(self.outdir): 175 | os.makedirs(self.outdir) 176 | with open( 177 | os.path.join(self.outdir, basename) + ".html", "wb") as filehandle: 178 | filehandle.write(report.encode("utf-8")) 179 | 180 | def short_outcome(self, outcome: CaseResult) -> "Literal['ok', '/', 's', 'f', 'F', '%', 'X', 'U', '?']": 181 | if outcome == CaseResult.PASSED: 182 | return "ok" 183 | return super(HtmlReportMatrix, self).short_outcome(outcome) 184 | 185 | def short_axis(self, axis: str): 186 | if axis.endswith(".xml"): 187 | return axis[:-4] 188 | return axis 189 | 190 | def summary(self, template: "Optional[Any]"=None): 191 | """ 192 | Render the html 193 | :return: 194 | """ 195 | html_matrix = HTMLMatrix(self, template) 196 | 197 | return str(html_matrix) 198 | 199 | 200 | class TextReportMatrix(ReportMatrix): 201 | """ 202 | Render a matrix report as text 203 | """ 204 | 205 | def summary(self): 206 | """ 207 | Render as a string 208 | :return: 209 | """ 210 | 211 | output = "\nMatrix Test Report\n" 212 | output += "===================\n" 213 | 214 | axis = list(self.reports.keys()) 215 | axis.sort() 216 | 217 | # find the longest classname or test case name 218 | left_indent = 0 219 | for classname in self.classes: 220 | left_indent = max(len(classname), left_indent) 221 | for casename in self.casenames[classname]: 222 | left_indent = max(len(casename), left_indent) 223 | 224 | # render the axis headings in a stepped tree 225 | treelines = "" 226 | for filename in self.report_order(): 227 | output += "{} {}{}\n".format(" " * left_indent, treelines, 228 | filename) 229 | treelines += "| " 230 | output += "{} {}\n".format(" " * left_indent, treelines) 231 | # render in groups of the same class 232 | 233 | for classname in self.classes: 234 | # new class 235 | output += "{} \n".format(classname) 236 | 237 | # print the case name 238 | for casename in sorted(set(self.casenames[classname])): 239 | output += "- {}{} ".format(casename, 240 | " " * (left_indent - len(casename))) 241 | 242 | # print each test and its result for each axis 243 | case_data = "" 244 | testcase: "Optional[Case]" = None 245 | for axis in self.report_order(): 246 | if axis not in self.cases[classname][casename]: 247 | case_data += " " 248 | else: 249 | testcase = self.cases[classname][casename][axis] 250 | if testcase.skipped: 251 | case_data += "s " 252 | elif testcase.failure: 253 | case_data += "f " 254 | else: 255 | case_data += "/ " 256 | 257 | if testcase is None or testcase.name is None: 258 | testcase_name = "" 259 | else: 260 | testcase_name = testcase.name 261 | combined, combined_name = self.combined_result( 262 | self.case_results[classname][testcase_name]) 263 | 264 | output += case_data 265 | output += " {} {}\n".format(combined, combined_name) 266 | 267 | # print the result stats 268 | 269 | output += "\n" 270 | output += "-" * 79 271 | output += "\n" 272 | 273 | output += "Test Results:\n" 274 | 275 | for outcome in sorted(self.result_stats): 276 | output += " {:<12} : {:>6}\n".format( 277 | outcome.title(), 278 | self.result_stats[outcome]) 279 | 280 | return output 281 | -------------------------------------------------------------------------------- /junit2htmlreport/merge.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for merging several reports into one 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from typing import TYPE_CHECKING 7 | 8 | import os 9 | import xml.etree.ElementTree as ET 10 | from io import BytesIO 11 | 12 | from . import parser 13 | from .common import ReportContainer 14 | from .textutils import unicode_str 15 | 16 | if TYPE_CHECKING: 17 | from typing import List 18 | 19 | 20 | def has_xml_header(filepath: str): 21 | """ 22 | Return True if the first line of the file is ' + u"\n" + unicode_str(buf.getvalue()) 100 | -------------------------------------------------------------------------------- /junit2htmlreport/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse a junit report file into a family of objects 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from typing import TYPE_CHECKING 7 | 8 | import os 9 | import sys 10 | import xml.etree.ElementTree as ET 11 | import collections 12 | import uuid 13 | 14 | from .case_result import CaseResult 15 | from .render import HTMLReport 16 | from .textutils import unicode_str 17 | 18 | if TYPE_CHECKING: 19 | from typing import Dict, List, Optional, Union, Any, OrderedDict 20 | 21 | NO_CLASSNAME = "no-testclass" 22 | PASSED = CaseResult.PASSED 23 | FAILED = CaseResult.FAILED 24 | SKIPPED = CaseResult.SKIPPED 25 | ABSENT = CaseResult.ABSENT 26 | UNKNOWN = CaseResult.UNKNOWN 27 | 28 | 29 | def clean_xml_attribute(element: "ET.Element", attribute: str, default: "Optional[str]"=None): 30 | """ 31 | Get an XML attribute value and ensure it is legal in XML 32 | :param element: 33 | :param attribute: 34 | :param default: 35 | :return: 36 | """ 37 | 38 | value = element.attrib.get(attribute, default) 39 | if value: 40 | value = value.encode("utf-8", errors="replace").decode("utf-8", errors="backslashreplace") 41 | value = value.replace(u"\ufffd", "?") # strip out the unicode replacement char 42 | 43 | return value 44 | 45 | 46 | class ParserError(Exception): 47 | """ 48 | We had a problem parsing a file 49 | """ 50 | def __init__(self, message: str): 51 | super(ParserError, self).__init__(message) 52 | 53 | 54 | class ToJunitXmlBase(object): 55 | """ 56 | Base class of all objects that can be serialized to Junit XML 57 | """ 58 | def tojunit(self) -> "ET.Element": 59 | """ 60 | Return an Element matching this object 61 | :return: 62 | """ 63 | raise NotImplementedError() 64 | 65 | def make_element(self, xmltag: str, text: "Optional[str]"=None, attribs: "Optional[Dict[str, Any]]"=None): 66 | """ 67 | Create an Element and put text and/or attribs into it 68 | :param xmltag: tag name 69 | :param text: 70 | :param attribs: dict of xml attributes 71 | :return: 72 | """ 73 | element = ET.Element(unicode_str(xmltag)) 74 | if text is not None: 75 | element.text = unicode_str(text) 76 | if attribs is not None: 77 | for item in attribs: 78 | element.set(unicode_str(item), unicode_str(attribs[item])) 79 | return element 80 | 81 | 82 | class AnchorBase(object): 83 | """ 84 | Base class that can generate a unique anchor name. 85 | """ 86 | def __init__(self): 87 | self._anchor = None 88 | 89 | def id(self): 90 | return self.anchor() 91 | 92 | def anchor(self): 93 | """ 94 | Generate a html anchor name 95 | :return: 96 | """ 97 | if not self._anchor: 98 | self._anchor = str(uuid.uuid4()) 99 | return self._anchor 100 | 101 | 102 | class Class(AnchorBase): 103 | """ 104 | A namespace for a test 105 | """ 106 | name: "Optional[str]" = None 107 | cases: "list[Case]" 108 | 109 | def __init__(self): 110 | super(Class, self).__init__() 111 | self.cases = list() 112 | 113 | 114 | class Property(AnchorBase, ToJunitXmlBase): 115 | """ 116 | Test Properties 117 | """ 118 | def __init__(self): 119 | super(Property, self).__init__() 120 | self.name: "Optional[str]" = None 121 | self.value: "Optional[str]" = None 122 | 123 | def tojunit(self): 124 | """ 125 | Return the xml element for this property 126 | :return: 127 | """ 128 | prop = self.make_element("property") 129 | prop.set(u"name", unicode_str(self.name)) 130 | prop.set(u"value", unicode_str(self.value)) 131 | return prop 132 | 133 | 134 | class Case(AnchorBase, ToJunitXmlBase): 135 | """ 136 | Test cases 137 | """ 138 | failure: "Optional[str]" = None 139 | failure_msg: "Optional[str]" = None 140 | skipped: "Optional[str]" = None 141 | skipped_msg: "Optional[str]" = None 142 | stderr: "Optional[Union[str,Any]]" = None 143 | stdout: "Optional[Union[str,Any]]" = None 144 | duration: float = 0 145 | name: "Optional[str]" = None 146 | testclass: "Optional[Class]" = None 147 | properties: "List[Property]" 148 | 149 | def __init__(self): 150 | super(Case, self).__init__() 151 | self.properties = list() 152 | 153 | @property 154 | def display_suffix(self): 155 | if self.skipped: 156 | return "[s]" 157 | return "" 158 | 159 | def outcome(self) -> CaseResult: 160 | """ 161 | Return the result of this test case 162 | :return: 163 | """ 164 | if self.skipped: 165 | return CaseResult.SKIPPED 166 | elif self.failed(): 167 | return CaseResult.FAILED 168 | return CaseResult.PASSED 169 | 170 | def prefix(self): 171 | if self.skipped: 172 | return "[S]" 173 | if self.failed(): 174 | return "[F]" 175 | return "" 176 | 177 | def tojunit(self): 178 | """ 179 | Turn this test case back into junit xml 180 | :note: this may not be the exact input we loaded 181 | :return: 182 | """ 183 | if self.testclass is None or self.testclass.name is None: 184 | testclass_name = "" 185 | else: 186 | testclass_name = self.testclass.name 187 | 188 | testcase = self.make_element("testcase") 189 | testcase.set(u"name", unicode_str(self.name)) 190 | testcase.set(u"classname", unicode_str(testclass_name)) 191 | testcase.set(u"time", unicode_str(self.duration)) 192 | 193 | if self.stderr is not None: 194 | testcase.append(self.make_element("system-err", self.stderr)) 195 | if self.stdout is not None: 196 | testcase.append(self.make_element("system-out", self.stdout)) 197 | 198 | if self.failure is not None: 199 | testcase.append(self.make_element( 200 | "failure", self.failure, 201 | { 202 | "message": self.failure_msg 203 | })) 204 | 205 | if self.skipped: 206 | testcase.append(self.make_element( 207 | "skipped", self.skipped, 208 | { 209 | "message": self.skipped_msg 210 | })) 211 | 212 | if self.properties: 213 | props = self.make_element("properties") 214 | for prop in self.properties: 215 | props.append(prop.tojunit()) 216 | testcase.append(props) 217 | 218 | return testcase 219 | 220 | def fullname(self): 221 | """ 222 | Get the full name of a test case 223 | :return: 224 | """ 225 | if self.testclass is None or self.testclass.name is None: 226 | testclass_name = "" 227 | else: 228 | testclass_name = self.testclass.name 229 | return "{} : {}".format(testclass_name, self.name) 230 | 231 | def basename(self): 232 | """ 233 | Get a short name for this case 234 | :return: 235 | """ 236 | if ( self.name is None 237 | or self.testclass is None 238 | or self.testclass.name is None 239 | ): 240 | return None 241 | 242 | if self.name.startswith(self.testclass.name): 243 | return self.name[len(self.testclass.name):] 244 | return self.name 245 | 246 | def failed(self): 247 | """ 248 | Return True if this test failed 249 | :return: 250 | """ 251 | return self.failure is not None 252 | 253 | 254 | class Suite(AnchorBase, ToJunitXmlBase): 255 | """ 256 | Contains test cases (usually only one suite per report) 257 | """ 258 | name: "Optional[str]" = None 259 | properties: "List[Property]" 260 | classes: "OrderedDict[str, Class]" 261 | duration: float = 0 262 | package: "Optional[str]" = None 263 | errors: "List[Dict[str, Optional[Union[str,Any]]]]" 264 | stdout: "Optional[Union[str,Any]]" = None 265 | stderr: "Optional[Union[str,Any]]" = None 266 | 267 | def __init__(self): 268 | super(Suite, self).__init__() 269 | self.classes = collections.OrderedDict() 270 | self.properties = [] 271 | self.errors = [] 272 | 273 | def tojunit(self): 274 | """ 275 | Return an element for this whole suite and all it's cases 276 | :return: 277 | """ 278 | suite = self.make_element("testsuite") 279 | suite.set(u"name", unicode_str(self.name)) 280 | suite.set(u"time", unicode_str(self.duration)) 281 | if self.properties: 282 | props = self.make_element("properties") 283 | for prop in self.properties: 284 | props.append(prop.tojunit()) 285 | suite.append(props) 286 | 287 | for testcase in self.all(): 288 | suite.append(testcase.tojunit()) 289 | return suite 290 | 291 | def __contains__(self, item: str): 292 | """ 293 | Return True if the given test classname is part of this test suite 294 | :param item: 295 | :return: 296 | """ 297 | return item in self.classes 298 | 299 | def __getitem__(self, item: str): 300 | """ 301 | Return the given test class object 302 | :param item: 303 | :return: 304 | """ 305 | return self.classes[item] 306 | 307 | def __setitem__(self, key: str, value: "Class"): 308 | """ 309 | Add a test class 310 | :param key: 311 | :param value: 312 | :return: 313 | """ 314 | self.classes[key] = value 315 | 316 | def all(self): 317 | """ 318 | Return all testcases 319 | :return: 320 | """ 321 | tests: "List[Case]" = list() 322 | for testclass in self.classes: 323 | tests.extend(self.classes[testclass].cases) 324 | return tests 325 | 326 | def failed(self): 327 | """ 328 | Return all the failed testcases 329 | :return: 330 | """ 331 | return [test for test in self.all() if test.failed()] 332 | 333 | def skipped(self): 334 | """ 335 | Return all skipped testcases 336 | :return: 337 | """ 338 | return [test for test in self.all() if test.skipped] 339 | 340 | def passed(self): 341 | """ 342 | Return all the passing testcases 343 | :return: 344 | """ 345 | return [test for test in self.all() if not test.failed() and not test.skipped] 346 | 347 | 348 | class Junit(object): 349 | """ 350 | Parse a single junit xml report 351 | """ 352 | filename: "Optional[str]" 353 | suites: "List[Suite]" 354 | tree: "Union[ET.ElementTree,ET.Element]" 355 | 356 | def __init__(self, filename: "Optional[str]"=None, xmlstring: "Optional[str]"=None): 357 | """ 358 | Parse the file 359 | :param filename: 360 | :return: 361 | """ 362 | self.filename = filename 363 | if filename == "-": 364 | # read the xml from stdin 365 | stdin = sys.stdin.read() 366 | xmlstring = stdin 367 | self.filename = None 368 | 369 | self.tree = None # type: ignore 370 | if self.filename is not None: 371 | self.tree = ET.parse(self.filename) 372 | elif xmlstring is not None: 373 | self._read(xmlstring) 374 | else: 375 | raise ValueError("Missing any filename or xmlstring") 376 | self.suites = [] 377 | self.process() 378 | 379 | 380 | def __iter__(self): 381 | return self.suites.__iter__() 382 | 383 | def _read(self, xmlstring: str): 384 | """ 385 | Populate the junit xml document tree from a string 386 | :param xmlstring: 387 | :return: 388 | """ 389 | self.tree = ET.fromstring(xmlstring) 390 | 391 | 392 | def process(self): 393 | """ 394 | populate the report from the xml 395 | :return: 396 | """ 397 | testrun = False 398 | suites: "Optional[list[ET.Element]]" = None 399 | root: "ET.Element" 400 | if isinstance(self.tree, ET.ElementTree): 401 | root = self.tree.getroot() 402 | else: 403 | root = self.tree 404 | 405 | if root.tag == "testrun": 406 | testrun = True 407 | root: "ET.Element" = root[0] 408 | 409 | if root.tag == "testsuite": 410 | suites = [root] 411 | 412 | if root.tag == "testsuites" or testrun: 413 | suites = [x for x in root] 414 | 415 | if suites is None: 416 | raise ParserError("could not find test suites in results xml") 417 | suitecount = 0 418 | for suite in suites: 419 | suitecount += 1 420 | cursuite = Suite() 421 | self.suites.append(cursuite) 422 | cursuite.name = clean_xml_attribute(suite, "name", default="suite-" + str(suitecount)) 423 | cursuite.package = clean_xml_attribute(suite, "package") 424 | 425 | cursuite.duration = float(suite.attrib.get("time", '0').replace(',', '') or '0') 426 | 427 | for element in suite: 428 | if element.tag == "error": 429 | # top level error? 430 | errtag = { 431 | "message": element.attrib.get("message", ""), 432 | "type": element.attrib.get("type", ""), 433 | "text": element.text 434 | } 435 | cursuite.errors.append(errtag) 436 | if element.tag == "system-out": 437 | cursuite.stdout = element.text 438 | if element.tag == "system-err": 439 | cursuite.stderr = element.text 440 | 441 | if element.tag == "properties": 442 | for prop in element: 443 | if prop.tag == "property": 444 | newproperty = Property() 445 | newproperty.name = prop.attrib["name"] 446 | newproperty.value = prop.attrib["value"] 447 | cursuite.properties.append(newproperty) 448 | 449 | if element.tag == "testcase": 450 | testcase = element 451 | 452 | if not testcase.attrib.get("classname", None): 453 | testcase.attrib["classname"] = NO_CLASSNAME 454 | 455 | if testcase.attrib["classname"] not in cursuite: 456 | testclass = Class() 457 | testclass.name = testcase.attrib["classname"] 458 | cursuite[testclass.name] = testclass 459 | 460 | testclass: "Class" = cursuite[testcase.attrib["classname"]] 461 | newcase = Case() 462 | newcase.name = clean_xml_attribute(testcase, "name") 463 | newcase.testclass = testclass 464 | newcase.duration = float(testcase.attrib.get("time", '0').replace(',', '') or '0') 465 | testclass.cases.append(newcase) 466 | 467 | # does this test case have any children? 468 | for child in testcase: 469 | if child.tag == "skipped": 470 | newcase.skipped = child.text 471 | if "message" in child.attrib: 472 | newcase.skipped_msg = child.attrib["message"] 473 | if not newcase.skipped: 474 | newcase.skipped = "skipped" 475 | elif child.tag == "system-out": 476 | newcase.stdout = child.text 477 | elif child.tag == "system-err": 478 | newcase.stderr = child.text 479 | elif child.tag == "failure": 480 | newcase.failure = child.text 481 | if "message" in child.attrib: 482 | newcase.failure_msg = child.attrib["message"] 483 | if not newcase.failure: 484 | newcase.failure = "failed" 485 | elif child.tag == "error": 486 | newcase.failure = child.text 487 | if "message" in child.attrib: 488 | newcase.failure_msg = child.attrib["message"] 489 | if not newcase.failure: 490 | newcase.failure = "error" 491 | elif child.tag == "properties": 492 | for property in child: 493 | newproperty = Property() 494 | newproperty.name = property.attrib["name"] 495 | newproperty.value = property.attrib["value"] 496 | newcase.properties.append(newproperty) 497 | 498 | def html(self, show_toc: bool=True): 499 | """ 500 | Render the test suite as a HTML report with links to errors first. 501 | :return: 502 | """ 503 | 504 | doc = HTMLReport(show_toc=show_toc) 505 | title = "Test Results" 506 | if self.filename: 507 | if os.path.exists(self.filename): 508 | title = os.path.basename(self.filename) 509 | doc.load(self, title=title) 510 | return str(doc) 511 | -------------------------------------------------------------------------------- /junit2htmlreport/parserimpl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Parse results files 3 | """ 4 | from .parser import Junit 5 | 6 | 7 | def load_report(filename: str) -> Junit: 8 | """ 9 | Load a report from disjk 10 | """ 11 | return Junit(filename=filename) 12 | 13 | 14 | def load_string(text: str) -> Junit: 15 | """ 16 | Load a report from a string 17 | """ 18 | return Junit(xmlstring=text) 19 | -------------------------------------------------------------------------------- /junit2htmlreport/render.py: -------------------------------------------------------------------------------- 1 | """ 2 | Render junit reports as HTML 3 | """ 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: # pragma: no cover 7 | from .matrix import ReportMatrix 8 | from .parser import Junit 9 | from os import PathLike 10 | from typing import Union, Sequence, Optional 11 | 12 | from jinja2 import Environment, PackageLoader, select_autoescape, FileSystemLoader 13 | 14 | 15 | class HTMLReport(object): 16 | title: str = "" 17 | report: "Optional[Junit]" = None 18 | show_toc: bool = True 19 | 20 | def __init__(self, show_toc: bool=True): 21 | self.show_toc = show_toc 22 | 23 | def load(self, report: "Junit", title: str="JUnit2HTML Report"): 24 | self.report = report 25 | self.title = title 26 | 27 | def __iter__(self): 28 | if self.report is None: 29 | raise Exception("A report must be loaded through `load(...)` first.") 30 | 31 | return self.report.__iter__() 32 | 33 | def __str__(self) -> str: 34 | env = Environment( 35 | loader=PackageLoader("junit2htmlreport", "templates"), 36 | autoescape=select_autoescape(["html"]) 37 | ) 38 | 39 | template = env.get_template("report.html") 40 | return template.render(report=self, title=self.title, show_toc=self.show_toc) 41 | 42 | 43 | class HTMLMatrix(object): 44 | title: str = "JUnit Matrix" 45 | matrix: "ReportMatrix" 46 | template: "Optional[Union[str,PathLike[str],Sequence[Union[str,PathLike[str]]]]]" 47 | 48 | def __init__(self, matrix: "ReportMatrix", template:"Optional[Union[str,PathLike[str],Sequence[Union[str,PathLike[str]]]]]"=None): 49 | self.matrix = matrix 50 | self.template = template 51 | 52 | def __str__(self) -> str: 53 | if self.template: 54 | loader = FileSystemLoader(self.template) 55 | else: 56 | loader = PackageLoader("junit2htmlreport", "templates") 57 | env = Environment( 58 | loader=loader, 59 | autoescape=select_autoescape(["html"]) 60 | ) 61 | 62 | template = env.get_template("matrix.html") 63 | return template.render(matrix=self.matrix, title=self.title) 64 | -------------------------------------------------------------------------------- /junit2htmlreport/report.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: sans-serif; 3 | } 4 | h2 { 5 | font-family: sans-serif; 6 | } 7 | 8 | li > a { 9 | font-family: monospace; 10 | } 11 | 12 | body { 13 | margin-bottom: 3em; 14 | } 15 | 16 | .testclass > td { 17 | border-bottom: 1px solid silver; 18 | color: #222222; 19 | padding: 3px; 20 | font-weight: bold; 21 | } 22 | 23 | .testclass > div { 24 | padding-left: 1em; 25 | } 26 | 27 | .testclass > a > div { 28 | font-size: 1.3em; 29 | font-family: sans-serif; 30 | } 31 | 32 | .testcase > div { 33 | border-left: 3px solid black; 34 | padding-left: 1em; 35 | } 36 | 37 | .testcase pre { 38 | display: block; 39 | padding: 4px; 40 | margin-right: 1em; 41 | background-color: #121212; 42 | color: #dedede; 43 | white-space: pre-wrap; 44 | } 45 | 46 | .testcase .details b { 47 | font-size: 1.2em; 48 | } 49 | 50 | .stdout, .stderr, .property { 51 | margin-left: 1em; 52 | } 53 | 54 | .testcase-link { 55 | text-decoration: none; 56 | } 57 | 58 | .testcase-cell { 59 | height: 1.8em; 60 | width: 1.8em; 61 | vertical-align: middle; 62 | text-align: center; 63 | } 64 | .testcase-cell a { 65 | display: inline-block; 66 | margin: -1em; 67 | padding: 1em; 68 | } 69 | 70 | .testcase-combined { 71 | white-space: pre; 72 | padding-left: 1em; 73 | } 74 | 75 | .skipped { 76 | background-color: gold; 77 | } 78 | 79 | .failed { 80 | background-color: lightcoral; 81 | } 82 | 83 | .passed { 84 | background-color: limegreen; 85 | } 86 | 87 | .tooltip { 88 | visibility: hidden; 89 | padding: 2px; 90 | z-index: 1; 91 | position: absolute; 92 | background: cornsilk; 93 | border: 1px solid black; 94 | margin-top: 15px; 95 | margin-left: 10px; 96 | opacity: 0; 97 | } 98 | .tooltip-parent:hover .tooltip { 99 | visibility: visible; 100 | opacity: 1; 101 | } 102 | 103 | .testcase:hover { 104 | background-color: gray; 105 | color: white; 106 | } 107 | 108 | .result-stats { 109 | margin: auto; 110 | } 111 | -------------------------------------------------------------------------------- /junit2htmlreport/runner.py: -------------------------------------------------------------------------------- 1 | """ 2 | Small command line tool to generate a html version of a junit report file 3 | """ 4 | from typing import TYPE_CHECKING 5 | 6 | import os 7 | import sys 8 | from argparse import ArgumentParser 9 | 10 | from . import matrix, merge, parser 11 | 12 | if TYPE_CHECKING: 13 | from typing import List 14 | 15 | PARSER = ArgumentParser(prog="junit2html") 16 | 17 | PARSER.add_argument("--summary-matrix", dest="text_matrix", action="store_true", 18 | default=False, 19 | help="Render multiple result files to the console") 20 | 21 | PARSER.add_argument("--report-matrix", dest="html_matrix", type=str, 22 | metavar="REPORT", 23 | help="Generate an HTML report matrix") 24 | 25 | PARSER.add_argument("--max-failures", dest="fail", type=int, default=0, 26 | metavar="FAILURES", 27 | help="Exit non-zero if FAILURES or more test cases are failures (has no effect with --merge)") 28 | 29 | PARSER.add_argument("--max-skipped", dest="skip", type=int, default=0, 30 | metavar="SKIPPED", 31 | help="Exit non-zero if SKIPPED or more test cases are skipped (has no effect with --merged)") 32 | 33 | PARSER.add_argument("--merge", dest="merge_output", type=str, 34 | metavar="NEWREPORT", 35 | help="Merge multiple test results into one file") 36 | 37 | PARSER.add_argument("--reports-template-folder", dest="template_folder", type=str, 38 | help="Render reports with these templates") 39 | 40 | PARSER.add_argument("--hide-toc", dest="hide_toc", action="store_true", 41 | default=False, 42 | help="Don't include a table-of-contents in the HTML report") 43 | 44 | PARSER.add_argument("REPORTS", metavar="REPORT", type=str, nargs="+", 45 | help="Test file to read") 46 | 47 | PARSER.add_argument("OUTPUT", type=str, nargs="?", 48 | help="Filename to save the html as") 49 | 50 | 51 | def run(args: "List[str]"): 52 | """ 53 | Run this tool 54 | :param args: 55 | :return: 56 | """ 57 | opts = PARSER.parse_args(args) if args else PARSER.parse_args() 58 | inputs = opts.REPORTS 59 | util = None 60 | if opts.merge_output: 61 | util = merge.Merger() 62 | for inputfile in inputs: 63 | util.add_report(inputfile) 64 | 65 | xmltext = util.toxmlstring() 66 | with open(opts.merge_output, "w") as outfile: 67 | outfile.write(xmltext) 68 | elif opts.text_matrix: 69 | util = matrix.TextReportMatrix() 70 | for filename in inputs: 71 | util.add_report(filename) 72 | print(util.summary()) 73 | elif opts.html_matrix: 74 | util = matrix.HtmlReportMatrix(os.path.dirname(opts.html_matrix)) 75 | for filename in inputs: 76 | util.add_report(filename, show_toc=not opts.hide_toc) 77 | with open(opts.html_matrix, "w") as outfile: 78 | outfile.write(util.summary(opts.template_folder)) 79 | 80 | if util: 81 | if opts.fail: 82 | failed = util.failures() 83 | if len(failed) >= opts.fail: 84 | sys.exit(len(failed)) 85 | if opts.skip: 86 | skipped = util.skips() 87 | if len(skipped) >= opts.fail: 88 | sys.exit(len(skipped)) 89 | 90 | if not util: 91 | # legacy interface that we need to preserve 92 | # no options, one or two args, first is input file, optional second is output 93 | 94 | if len(opts.REPORTS) > 2: 95 | PARSER.print_usage() 96 | sys.exit(1) 97 | 98 | infilename = opts.REPORTS[0] 99 | 100 | if len(opts.REPORTS) == 2: 101 | outfilename = opts.REPORTS[1] 102 | else: 103 | outfilename = infilename + ".html" 104 | 105 | report = parser.Junit(infilename) 106 | html = report.html(show_toc=not opts.hide_toc) 107 | if report.filename is not None: 108 | with open(outfilename, "wb") as outfile: 109 | outfile.write(html.encode('utf-8')) 110 | else: 111 | print(html.encode('utf-8')) 112 | 113 | 114 | def start(): 115 | """ 116 | Run using the current sys.argv 117 | """ 118 | run(sys.argv[1:]) 119 | 120 | 121 | if __name__ == "__main__": 122 | start() 123 | -------------------------------------------------------------------------------- /junit2htmlreport/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 9 | 10 | 11 | {% block content %} 12 | {% endblock %} 13 | 16 | 17 | -------------------------------------------------------------------------------- /junit2htmlreport/templates/matrix.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

4 | Reports Matrix 5 |

6 | 7 | 8 | {% set report_names = matrix.report_order() %} 9 | {% set n_reports = report_names.__len__() %} 10 | {% if n_reports > 0 %} 11 | 12 | 22 | 23 | 24 | 25 | {% endif %} 26 | 27 | {% for i in range(n_reports) %} 28 | 29 | 32 | {% for n in range(i) %} 33 | 34 | {% endfor %} 35 | 36 | {% endfor %} 37 | 38 | {% for n in range(n_reports + 1) %} 39 | 40 | {% endfor %} 41 | 42 | 43 | 44 | {% for classname in matrix.classes %} 45 | 46 | 47 | {% for n in range(n_reports) %} 48 | 49 | {% endfor %} 50 | 51 | {% for casename in matrix.casenames[classname] %} 52 | {% set xcase = matrix.cases[classname][casename] %} 53 | 54 | 55 | 58 | {% for n in range(n_reports) %} 59 | {% set axis = report_names[n] %} 60 | 61 | 73 | {% endfor %} 74 | {% endfor %} 75 | 76 | {% endfor %} 77 | 78 |
13 | 14 | {% for outcome in matrix.result_stats %} 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 |
{{outcome.title()}}{{matrix.result_stats[outcome]}}
21 |
30 | {{matrix.short_axis(report_names[n_reports - 1 - i])}} 31 |
{{classname}}
{{casename}} 56 | {{ matrix.combined_result_list(classname, casename)[1] }} 57 | 62 | {% if axis in xcase %} 63 | 64 | 65 | {{ matrix.short_outcome(xcase[axis].outcome()) }} 66 | 67 | 68 | {% else %} 69 |   70 | {% endif %} 71 | 72 |
79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /junit2htmlreport/templates/report.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

4 | Test Report : {{ report.title }} 5 |

6 | 7 | {% if show_toc %} 8 | 9 | 10 | 11 | 26 | 39 | 40 |
12 |
    13 | {% for suite in report %} 14 | {% for classname in suite.classes %} 15 |
  • {{classname}} 16 |
      17 | {% for test in suite.classes[classname].cases %} 18 |
    • {{test.name}}{{test.display_suffix}}
    • 19 | {% endfor %} 20 |
    21 |
  • 22 | {% endfor %} 23 | {% endfor %} 24 |
25 |
27 |
    28 | {% for suite in report %} 29 | {% for classname in suite.classes %} 30 | {% for test in suite.classes[classname].cases %} 31 | {% if test.failed() %} 32 |
  • {{test.prefix()}} {{test.fullname()}}
  • 33 | {% endif %} 34 | {% endfor %} 35 | {% endfor %} 36 | {% endfor %} 37 |
38 |
41 | {% endif %} 42 | 43 | {% for suite in report %} 44 |
45 |

Test Suite: {{ suite.name }}

46 | 47 | {% if suite.package %} 48 | Package: {{suite.package}} 49 | {% endif %} 50 | {% if suite.properties %} 51 |

Suite Properties

52 | 53 | {% for prop in suite.properties %} 54 | 55 | 56 | 57 | {% endfor %} 58 |
{{prop.name}}{{prop.value}}
59 | {% endif %} 60 |

Results

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Duration{{suite.duration |round(3)}} sec
Tests{{suite.all() |length}}
Failures{{suite.failed()| length}}
72 | 73 |
74 |

Tests

75 | {% for classname in suite.classes %} 76 |
77 |

{{classname}}

78 |
79 | {% for test in suite.classes[classname].cases %} 80 |
81 | 82 | 83 | 84 | 85 | 86 | {% if test.failed() %} 87 | 88 | {% endif %} 89 | {% if test.skipped %} 90 | 91 | {% endif %} 92 |
Test case:{{test.name}}
Outcome:{{test.outcome().title()}}
Duration:{{test.duration|round(3)}} sec
Failed{{test.failure_msg}}
Skipped{{test.skipped_msg}}
93 | 94 | {% if test.failed() %} 95 |
{{test.failure}}
96 | {% endif %} 97 | {% if test.skipped %} 98 |
{{test.skipped}}
99 | {% endif %} 100 | 101 | {% if test.properties %} 102 | 103 | {% for prop in test.properties %} 104 | 105 | 106 | 107 | {% endfor %} 108 |
{{prop.name}}{{prop.value}}
109 | {% endif %} 110 | {% if test.stdout %} 111 |
Stdout
112 |
{{test.stdout}}
113 |
114 | {% endif %} 115 | {% if test.stderr %} 116 |
Stderr
117 |
{{test.stderr}}
118 |
119 | {% endif %} 120 |
121 | {% endfor %} 122 |
123 |
124 | {% endfor %} 125 |
126 |
127 | {% if suite.stdout or suite.stderr %} 128 |

Suite stdout:

129 |
{{suite.stdout}}
130 |

Suite stderr:

131 |
{{suite.stderr}}
132 | {% endif %} 133 | {% endfor %} 134 | 135 | {% endblock %} 136 | -------------------------------------------------------------------------------- /junit2htmlreport/templates/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: white; 3 | padding-bottom: 20em; 4 | margin: 0; 5 | min-height: 15cm; 6 | } 7 | 8 | h1, h2, h3, h4, h5, h6, h7 { 9 | font-family: sans-serif; 10 | } 11 | 12 | h1 { 13 | background-color: #007acc; 14 | color: white; 15 | padding: 3mm; 16 | margin-top: 0; 17 | margin-bottom: 1mm; 18 | } 19 | 20 | .footer { 21 | font-style: italic; 22 | font-size: small; 23 | text-align: right; 24 | padding: 1em; 25 | } 26 | 27 | .testsuite { 28 | padding-bottom: 2em; 29 | margin-left: 1em; 30 | } 31 | 32 | .proplist { 33 | width: 100%; 34 | margin-bottom: 2em; 35 | border-collapse: collapse; 36 | border: 1px solid grey; 37 | } 38 | 39 | .proplist th { 40 | background-color: silver; 41 | width: 5em; 42 | padding: 2px; 43 | padding-right: 1em; 44 | text-align: left; 45 | } 46 | 47 | .proplist td { 48 | padding: 2px; 49 | word-break: break-all; 50 | } 51 | 52 | .index-table { 53 | width: 90%; 54 | margin-left: 1em; 55 | } 56 | 57 | .index-table td { 58 | vertical-align: top; 59 | max-width: 200px; 60 | word-wrap: break-word; 61 | } 62 | 63 | .failure-index { 64 | 65 | } 66 | 67 | .toc { 68 | margin-bottom: 2em; 69 | font-family: monospace; 70 | } 71 | 72 | .stdio, pre { 73 | min-height: 1em; 74 | background-color: #1e1e1e; 75 | color: silver; 76 | padding: 0.5em; 77 | } 78 | .tdpre { 79 | background-color: #1e1e1e; 80 | } 81 | 82 | .test { 83 | margin-left: 0.5cm; 84 | } 85 | 86 | .toc { 87 | list-style: none; 88 | } 89 | 90 | .toc li.outcome { 91 | list-style: none; 92 | } 93 | 94 | .toc li.outcome::before { 95 | content: "\2022"; 96 | color: black; 97 | font-weight: bold; 98 | display: inline-block; 99 | width: 1.27em; 100 | margin-left: -1.27em; 101 | font-size: 1.2em; 102 | } 103 | 104 | .toc li.outcome-failed { 105 | background-color: lightcoral; 106 | } 107 | 108 | .toc li.outcome-failed::before { 109 | color: red; 110 | } 111 | 112 | .toc li.outcome-passed::before { 113 | color: green; 114 | } 115 | 116 | .toc li.outcome-skipped::before { 117 | color: orange; 118 | } 119 | 120 | .testcases .outcome { 121 | border-left: 1em; 122 | padding: 2px; 123 | } 124 | 125 | .testcases .outcome-failed { 126 | border-left: 1em solid lightcoral; 127 | } 128 | 129 | 130 | .outcome-passed { 131 | border-left: 1em solid lightgreen; 132 | } 133 | 134 | .outcome-skipped { 135 | border-left: 1em dotted silver; 136 | } 137 | 138 | .testcases .outcome-passed { 139 | border-left: 1em solid lightgreen; 140 | } 141 | 142 | .testcases .outcome-skipped { 143 | border-left: 1em solid #FED8B1; 144 | } 145 | 146 | .stats-table { 147 | } 148 | 149 | .stats-table td { 150 | min-width: 4em; 151 | text-align: right; 152 | } 153 | 154 | .stats-table .failed { 155 | background-color: lightcoral; 156 | } 157 | 158 | .stats-table .passed { 159 | background-color: lightgreen; 160 | } 161 | 162 | .matrix-table { 163 | table-layout: fixed; 164 | border-spacing: 0; 165 | width: available; 166 | margin-left: 1em; 167 | } 168 | 169 | .matrix-table td { 170 | vertical-align: center; 171 | } 172 | 173 | .matrix-table td:last-child { 174 | width: 0; 175 | } 176 | 177 | .matrix-table tr:hover { 178 | background-color: yellow; 179 | } 180 | 181 | .matrix-axis-name { 182 | white-space: nowrap; 183 | padding-right: 0.5em; 184 | border-left: 1px solid black; 185 | border-top: 1px solid black; 186 | text-align: right; 187 | } 188 | 189 | .matrix-axis-line { 190 | border-left: 1px solid black; 191 | width: 0.5em; 192 | } 193 | 194 | .matrix-classname { 195 | text-align: left; 196 | width: 100%; 197 | border-top: 2px solid grey; 198 | border-bottom: 1px solid silver; 199 | } 200 | 201 | .matrix-casename { 202 | text-align: left; 203 | font-weight: normal; 204 | font-style: italic; 205 | padding-left: 1em; 206 | border-bottom: 1px solid silver; 207 | } 208 | 209 | .matrix-result { 210 | display: block; 211 | width: 1em; 212 | text-align: center; 213 | padding: 1mm; 214 | margin: 0; 215 | } 216 | 217 | .matrix-result-combined { 218 | white-space: nowrap; 219 | padding-right: 0.2em; 220 | text-align: right; 221 | } 222 | 223 | .matrix-result-failed { 224 | background-color: lightcoral; 225 | } 226 | 227 | .matrix-result-passed { 228 | background-color: lightgreen; 229 | } 230 | 231 | .matrix-result-skipped { 232 | background-color: lightyellow; 233 | } 234 | 235 | .matrix-even { 236 | background-color: lightgray; 237 | } 238 | -------------------------------------------------------------------------------- /junit2htmlreport/textutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stringify to unicode 3 | """ 4 | 5 | from typing import Any, Optional 6 | 7 | 8 | def unicode_str(text: "Optional[Any]"): 9 | """ 10 | Convert text to unicode 11 | :param text: 12 | :return: 13 | """ 14 | if isinstance(text, bytes): 15 | return text.decode("utf-8", "strict") 16 | return "" if text is None else str(text) 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --junit-xml pytest-results.xml 3 | junit_family = xunit2 -------------------------------------------------------------------------------- /setup-venv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | rm -rf venv 4 | python3 -m venv venv 5 | . venv/bin/activate 6 | pip install -e . -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | files = [os.path.join("templates", "*.css"), 6 | os.path.join("templates", "*.html")] 7 | 8 | 9 | setup( 10 | name="junit2html", 11 | version="31.0.2", 12 | description="Generate HTML reports from Junit results", 13 | author="Ian Norton", 14 | author_email="inorton@gmail.com", 15 | url="https://gitlab.com/inorton/junit2html", 16 | install_requires=["jinja2>=3.0"], 17 | packages=["junit2htmlreport"], 18 | package_data={"junit2htmlreport": files}, 19 | entry_points={'console_scripts': ['junit2html=junit2htmlreport.runner:start']}, 20 | platforms=["any"], 21 | license="License :: OSI Approved :: MIT License", 22 | long_description="Genearate a single file HTML report from a Junit or XUnit XML results file" 23 | ) 24 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inorton/junit2html/b821eb1a79f25a2efa9bd54d147e3101094ff5fb/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper funcs for tests 3 | """ 4 | import os 5 | from .inputfiles import get_filepath 6 | from junit2htmlreport import runner 7 | 8 | 9 | def run_runner(tmpdir, filename, *extra): 10 | """ 11 | Run the junit2html program against the given report and produce a html doc 12 | :param tmpdir: 13 | :param filename: 14 | :param extra: addtional arguments 15 | :return: 16 | """ 17 | testfile = get_filepath(filename=filename) 18 | if not len(extra): 19 | outfile = os.path.join(tmpdir.strpath, "report.html") 20 | runner.run([testfile, outfile]) 21 | assert os.path.exists(outfile) 22 | else: 23 | runner.run([testfile] + list(extra)) 24 | 25 | 26 | def test_runner_simple(tmpdir): 27 | """ 28 | Test the stand-alone app with a simple fairly empty junit file 29 | :param tmpdir: py.test tmpdir fixture 30 | :return: 31 | """ 32 | run_runner(tmpdir, "junit-simple_suites.xml") 33 | -------------------------------------------------------------------------------- /tests/inputfiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper for loading our test files 3 | """ 4 | import os 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | 7 | 8 | def get_reports(): 9 | return [x for x in os.listdir(HERE) if x.endswith(".xml")] 10 | 11 | 12 | def get_filepath(filename): 13 | """ 14 | 15 | :param filename: 16 | :return: 17 | """ 18 | return os.path.join(HERE, filename) 19 | -------------------------------------------------------------------------------- /tests/junit-axis-linux.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Assertion failed 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/junit-axis-solaris.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Assertion failed 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/junit-axis-windows.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/junit-complex_suites.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | -------------------------------------------------------------------------------- /tests/junit-cute2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | SetLarger: your == T1 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | "Set value to -1 should throw" 55 | 56 | 57 | 58 | 59 | 60 | 61 | "Set value overflow" 62 | 63 | 64 | 65 | 66 | "Set use to 1 more than alloc" 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/junit-jenkins-stdout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | expectedRet: 0 - realRet: 141 4 | File "/home/admin/sikuli/sikuli-ide.jar/Lib/unittest.py", line 260, in run 5 | testMethod() 6 | File "/home/admin/workspace/main-POC/test/automation-rdpcore/./RDPTest.sikuli/RDPTest.py", line 75, in testRdp 7 | myRDP.verifyResult(self.tc["ExpectedReturnCode"], self.tc["ExpectedErrorMsg"]) 8 | File "/home/admin/workspace/main-POC/test/automation-rdpcore/lib/rdp.sikuli/rdp.py", line 388, in verifyResult 9 | assert self.ret == int(expectedRet), "expectedRet: %s - realRet: %s" % (str(expectedRet), str(self.ret)) 10 | 11 | 12 | Executing test case[99]: Stand-alone SH which disables NLA with default setting 19 | 20 | Test Case Action: ExitDisconnect 21 | 22 | HostOS= 2008r2 , CMD= 10.200.25.179 -u auto1 -p Test#123 -d dellrdp, Actions= ExitDisconnect 23 | In init(). 24 | IN getImage(). 25 | rdp(): title = rdpclient.png 26 | running rdp client... ./rdpclient 10.200.25.179 -u auto1 -p Test#123 -d dellrdp --no-color-log 27 | Searching: RDP initialization has succeeded and ready to start main rdp loop.' to check if RDP session is connected or done. 28 | until timeout in 20 seconds. 29 | 0:00:01.027507084: rdpclient:b514a700: GStreamer Element/CODEC ' bmmxvimagesink' not found 30 | 31 | 0:00:01.027605840: rdpclient:b514a700: rdp(): starting 32 | 33 | 0:00:01.027648135: __BASE__:b514a700: Client Name is 'ubuntu-12.04' 34 | 35 | 0:00:01.027753259: __BASE__:b514a700: mchannel_init(): thread id 0xb514a700 36 | 37 | 0:00:01.028589778: __BASE__:b514a700: Connect to the host 10.200.25.179 ... 38 | 39 | 0:00:01.028625424: __BASE__:b514a700: client_sock_connect(): fqdn=(null) tna=(null) tsgwEnable=0 40 | 41 | 0:00:01.036141354: __BASE__:b514a700: RDP: NLA CredSSP Authentication 42 | 43 | 0:00:01.044063727: __BASE__:b514a700: NLA_SSL_connect():300: SSL_get_verify_result=20: unable to get local issuer certificate 44 | 45 | 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) 46 | 47 | 0:00:01.138312578: __BASE__:b514a700: Start main send-to-RDP thread... 48 | 49 | 0:00:01.138400992: __BASE__:b514a700: RDP initialization has succeeded and ready to start main rdp loop. 50 | 51 | 1.138400992 52 | Found: RDP initialization has succeeded and ready to start main rdp loop 53 | Wait for 10 seconds... 54 | Call performExit(). 55 | HostOD = 2008r2 56 | In performExit(): 57 | 2008r2 58 | rdpclient.png 59 | Wait for 15 secs. 60 | IN getImage(). 61 | IN getImage(). 62 | IN getImage(). 63 | IN getImage(). 64 | IN getImage(). 65 | IN getImage(). 66 | IN getImage(). 67 | IN getImage(). 68 | IN getImage(). 69 | window.png 70 | IN getImage(). 71 | Call verifyResult(). 72 | IN getImage(). 73 | IN getImage(). 74 | IN getImage(). 75 | Polling the rdpclient process and check if it exits properly in 120 seconds...... 76 | 141 77 | IN getImage(). 78 | IN getImage(). 79 | IN getImage(). 80 | *******************************stdout******************************* 81 | 82 | *******************************stderr******************************* 83 | 0:00:01.027507084: rdpclient:b514a700: GStreamer Element/CODEC ' bmmxvimagesink' not found 84 | 0:00:01.027605840: rdpclient:b514a700: rdp(): starting 85 | 0:00:01.027648135: __BASE__:b514a700: Client Name is 'ubuntu-12.04' 86 | 0:00:01.027753259: __BASE__:b514a700: mchannel_init(): thread id 0xb514a700 87 | 0:00:01.028589778: __BASE__:b514a700: Connect to the host 10.200.25.179 ... 88 | 0:00:01.028625424: __BASE__:b514a700: client_sock_connect(): fqdn=(null) tna=(null) tsgwEnable=0 89 | 0:00:01.036141354: __BASE__:b514a700: RDP: NLA CredSSP Authentication 90 | 0:00:01.044063727: __BASE__:b514a700: NLA_SSL_connect():300: SSL_get_verify_result=20: unable to get local issuer certificate 91 | 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) 92 | 0:00:01.138312578: __BASE__:b514a700: Start main send-to-RDP thread... 93 | 0:00:01.138400992: __BASE__:b514a700: RDP initialization has succeeded and ready to start main rdp loop. 94 | 0:00:01.142691288: PDU READ:afb15b40: clt_plat_chal_res(): vchannel sec_license_pkt 95 | 0:00:01.206414711: PDU READ:afb15b40: WARNING: Error opening TS-CAL file to save license: error code 2 (No such file or directory) 96 | 0:00:01.322600645: PDU READ:afb15b40: RDP: Decompression type is RDP 6.1 Bulk 97 | 0:00:01.323062845: -MAIN-:b331cb40: Max gc count: 3 98 | 0:00:01.329853196: -MAIN-:b331cb40: Max gc count: 10 99 | 0:00:01.545970383: -MAIN-:b331cb40: Max gc count: 21 100 | 0:00:01.547702120: -MAIN-:b331cb40: Max gc count: 24 101 | 0:00:03.173001519: -MAIN-:b331cb40: Max gc count: 30 102 | 0:00:29.063941827: -MAIN-:b331cb40: Max gc count: 36 103 | 0:00:29.402013036: -MAIN-:b331cb40: Max gc count: 72 104 | 0:00:45.667702527: -MAIN-:b331cb40: Max gc count: 252 105 | 0:00:51.223839720: PDU READ:afb15b40: mcs_filter(): SERVER HAS TOLD US TO TERMINATE (32) 106 | 107 | ***ERROR: Unexpected error return code - expectedRet: 0 , realRet: 141 108 | In tearDown(). 109 | ]]> 110 | 111 | 112 | -------------------------------------------------------------------------------- /tests/junit-report-6700.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Etc etc 6 | 7 | java.lang.NoClassDefFoundError: gda/device/detector/DAServer 8 | at java.lang.Class.forName0(Native Method) 9 | at java.lang.Class.forName(Class.java:169) 10 | Caused by: java.lang.ClassNotFoundException: gda.device.detector.DAServer 11 | at java.net.URLClassLoader$1.run(URLClassLoader.java:202) 12 | at java.security.AccessController.doPrivileged(Native Method) 13 | at java.net.URLClassLoader.findClass(URLClassLoader.java:190) 14 | at java.lang.ClassLoader.loadClass(ClassLoader.java:307) 15 | at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301) 16 | at java.lang.ClassLoader.loadClass(ClassLoader.java:248) 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/junit-simple_suite.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Assertion failed 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/junit-simple_suites.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Assertion failed 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/junit-testrun.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/junit-unicode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 }). 5 | /home/gereon/Schreibtisch/evalTest/ex-7_9-moodle-karma-test-execution/test-classes/test_ex4.js:40:50 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/junit-unicode2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/pytest-binary-names.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/test_example_output.py: -------------------------------------------------------------------------------- 1 | """ 2 | A series of tests that produce different example output 3 | """ 4 | 5 | import sys 6 | 7 | 8 | def test_stderr_only(): 9 | """ 10 | Print some text to stderr 11 | :return: 12 | """ 13 | sys.stderr.write(""" 14 | Hello Standard Error 15 | ===================================== 16 | 17 | This is some formatted stderr 18 | """) 19 | 20 | 21 | def test_stdout_only(): 22 | """ 23 | Print some text to stderr 24 | :return: 25 | """ 26 | sys.stdout.write(""" 27 | Hello Standard Out 28 | ===================================== 29 | 30 | This is some formatted stdout 31 | """) 32 | 33 | 34 | def test_stdoe(): 35 | """ 36 | Print some stuff to stderr and stdout 37 | :return: 38 | """ 39 | def err(msg): 40 | sys.stderr.write(msg) 41 | sys.stderr.write("\n") 42 | 43 | def out(msg): 44 | sys.stdout.write(msg) 45 | sys.stdout.write("\n") 46 | 47 | for _ in range(3): 48 | for word in ["Hello", "World"]: 49 | err("Err " + word) 50 | out("Out " + word) 51 | 52 | -------------------------------------------------------------------------------- /tests/test_junit2html.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test that does nothing other than import 3 | """ 4 | import os 5 | import pytest 6 | from .inputfiles import get_filepath 7 | from .helpers import run_runner 8 | from junit2htmlreport import parser, runner 9 | 10 | 11 | def test_runner_sonobouy(tmpdir): 12 | """ 13 | Test the stand-alone app with report produced by sonobouy 14 | :param tmpdir: py.test tmpdir fixture 15 | :return: 16 | """ 17 | run_runner(tmpdir, "junit-sonobouy.xml") 18 | 19 | 20 | def test_runner_complex(tmpdir): 21 | """ 22 | Test the stand-alone app with a large fairly complex junit file 23 | :param tmpdir: py.test tmpdir fixture 24 | :return: 25 | """ 26 | run_runner(tmpdir, "junit-complex_suites.xml") 27 | 28 | 29 | def test_runner_6700(tmpdir): 30 | """ 31 | Test the 6700 report 32 | I can't remember what is special about this file! 33 | :param tmpdir: 34 | :return: 35 | """ 36 | run_runner(tmpdir, "junit-report-6700.xml") 37 | 38 | 39 | def test_runner_unicode(tmpdir): 40 | """ 41 | Test the stand-alone app with a unicode file (contains a euro symbol) 42 | :param tmpdir: 43 | :return: 44 | """ 45 | run_runner(tmpdir, "junit-unicode.xml") 46 | 47 | 48 | def test_runner_testrun(tmpdir): 49 | """ 50 | Test the stand-alone app with a file rooted at 51 | :param tmpdir: 52 | :return: 53 | """ 54 | run_runner(tmpdir, "junit-testrun.xml") 55 | 56 | 57 | def test_runner_merge(tmpdir): 58 | """ 59 | Test merging multiple files 60 | :param tmpdir: 61 | :return: 62 | """ 63 | filenames = ["junit-complex_suites.xml", 64 | "junit-cute2.xml", 65 | "junit-unicode.xml"] 66 | 67 | filepaths = [] 68 | for filename in filenames: 69 | filepaths.append( 70 | os.path.join(tmpdir.strpath, get_filepath(filename))) 71 | newfile = os.path.join(tmpdir.strpath, "merged.xml") 72 | args = ["--merge", newfile] 73 | args.extend(filepaths) 74 | runner.run(args) 75 | assert os.path.exists(newfile) 76 | 77 | 78 | def test_emit_stdio(): 79 | """ 80 | Test the stand-alone app can generate a page from a report containing stdio text 81 | But also save the result in the current folder 82 | :return: 83 | """ 84 | folder = os.path.dirname(__file__) 85 | reportfile = os.path.join(folder, "junit-jenkins-stdout.xml") 86 | runner.run([reportfile]) 87 | htmlfile = os.path.join(folder, "junit-jenkins-stdout.xml.html") 88 | assert os.path.exists(htmlfile) 89 | with open(htmlfile, "r") as readfile: 90 | content = readfile.read() 91 | assert "===> Executing test case" in content 92 | 93 | 94 | def test_parser(): 95 | """ 96 | Test the junit parser directly 97 | :return: 98 | """ 99 | junit = parser.Junit(filename=get_filepath("junit-simple_suite.xml")) 100 | assert len(junit.suites) == 1 101 | assert len(junit.suites[0].properties) == 3 102 | 103 | junit = parser.Junit(filename=get_filepath("junit-simple_suites.xml")) 104 | assert len(junit.suites) == 1 105 | assert len(junit.suites[0].properties) == 3 106 | 107 | junit = parser.Junit(filename=get_filepath("junit-complex_suites.xml")) 108 | assert len(junit.suites) == 66 109 | 110 | junit = parser.Junit(filename=get_filepath("junit-cute2.xml")) 111 | assert len(junit.suites) == 6 112 | 113 | junit = parser.Junit(filename=get_filepath("junit-unicode.xml")) 114 | assert len(junit.suites) == 1 115 | assert len(junit.suites[0].classes) == 2 116 | 117 | # different report structure, both files contain unicode symbols 118 | junit = parser.Junit(filename=get_filepath("junit-unicode2.xml")) 119 | assert len(junit.suites) == 1 120 | assert len(junit.suites[0].classes) == 1 121 | 122 | 123 | def test_binary_names(): 124 | # a test with nonsense binary in the case names 125 | junit = parser.Junit(filename=get_filepath("pytest-binary-names.xml")) 126 | assert junit 127 | 128 | 129 | def test_parser_stringreader(): 130 | """ 131 | Test the junit parser when reading strings 132 | :return: 133 | """ 134 | with open(get_filepath("junit-complex_suites.xml"), "r") as data: 135 | junit = parser.Junit(xmlstring=data.read()) 136 | assert len(junit.suites) == 66 137 | assert junit.suites[0].name == "Untitled suite in /Users/niko/Sites/casperjs/tests/suites/casper/agent.js" 138 | assert junit.suites[0].package == "tests/suites/casper/agent" 139 | assert junit.suites[0].classes["tests/suites/casper/agent"].cases[1].name == "Default user agent matches /plop/" 140 | 141 | 142 | def test_fail_exit(tmpdir): 143 | """ 144 | Test the tool exits with an error for --max-failures 145 | :return: 146 | """ 147 | with pytest.raises(SystemExit) as err: 148 | run_runner(tmpdir, "junit-unicode.xml", "--summary-matrix", "--max-failures", "1") 149 | 150 | assert err.value.code != 0 151 | 152 | 153 | def test_skip_exit(tmpdir): 154 | """ 155 | Test the tool exits with an error for --max-skipped 156 | :return: 157 | """ 158 | with pytest.raises(SystemExit) as err: 159 | run_runner(tmpdir, "junit-axis-windows.xml", "--summary-matrix", "--max-skipped", "1") 160 | 161 | assert err.value.code != 0 -------------------------------------------------------------------------------- /tests/test_junitparser_render.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test that we can load all our input files without error 3 | """ 4 | import pytest 5 | from . import inputfiles 6 | from junit2htmlreport import render, parserimpl 7 | 8 | 9 | @pytest.mark.parametrize("filename", inputfiles.get_reports()) 10 | def test_load(filename): 11 | report = parserimpl.load_report(inputfiles.get_filepath(filename)) 12 | assert report is not None 13 | 14 | doc = render.HTMLReport() 15 | doc.load(report, filename) 16 | 17 | output = str(doc) 18 | assert output 19 | -------------------------------------------------------------------------------- /tests/test_matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the matrix functionality 3 | """ 4 | 5 | from junit2htmlreport import matrix 6 | from junit2htmlreport.matrix import PARTIAL_PASS, PARTIAL_FAIL, TOTAL_FAIL, UNTESTED 7 | from junit2htmlreport.parser import PASSED, SKIPPED, FAILED, UNKNOWN 8 | 9 | 10 | from .inputfiles import get_filepath 11 | 12 | 13 | def test_combined_result(): 14 | """ 15 | Test that the combined result string and short result value are correct 16 | :return: 17 | """ 18 | textmatrix = matrix.TextReportMatrix() 19 | short, result = textmatrix.combined_result([PASSED, SKIPPED]) 20 | 21 | assert short == textmatrix.short_outcome(PASSED) 22 | assert result == PASSED.title() 23 | 24 | short, result = textmatrix.combined_result([PASSED, FAILED]) 25 | assert short == textmatrix.short_outcome(PARTIAL_FAIL) 26 | assert result == PARTIAL_FAIL.title() 27 | 28 | short, result = textmatrix.combined_result([PARTIAL_PASS]) 29 | assert short == textmatrix.short_outcome(PARTIAL_PASS) 30 | assert result == PARTIAL_PASS.title() 31 | 32 | short, result = textmatrix.combined_result([TOTAL_FAIL]) 33 | assert short == textmatrix.short_outcome(TOTAL_FAIL) 34 | assert result == TOTAL_FAIL.title() 35 | 36 | short, result = textmatrix.combined_result([FAILED, FAILED]) 37 | assert short == textmatrix.short_outcome(FAILED) 38 | assert result == FAILED.title() 39 | 40 | short, result = textmatrix.combined_result([PASSED]) 41 | assert short == textmatrix.short_outcome(PASSED) 42 | assert result == PASSED.title() 43 | 44 | short, result = textmatrix.combined_result([SKIPPED, SKIPPED]) 45 | assert short == textmatrix.short_outcome(UNTESTED) 46 | assert result == UNTESTED.title() 47 | 48 | short, result = textmatrix.combined_result([]) 49 | assert '?' == textmatrix.short_outcome(None) # type: ignore 50 | assert result == UNKNOWN.title() 51 | 52 | 53 | def test_matrix_load(tmpdir): 54 | """ 55 | Test loading multiple reports 56 | :return: 57 | """ 58 | textmatrix = matrix.TextReportMatrix() 59 | textmatrix.add_report(get_filepath("junit-simple_suite.xml")) 60 | textmatrix.add_report(get_filepath("junit-simple_suites.xml")) 61 | textmatrix.add_report(get_filepath("junit-unicode.xml")) 62 | textmatrix.add_report(get_filepath("junit-unicode2.xml")) 63 | textmatrix.add_report(get_filepath("junit-cute2.xml")) 64 | textmatrix.add_report(get_filepath("junit-jenkins-stdout.xml")) 65 | 66 | assert len(textmatrix.reports) == 6 67 | 68 | result = textmatrix.summary() 69 | 70 | print(result) 71 | 72 | 73 | def test_matrix_html(tmpdir): 74 | """ 75 | Test loading multiple reports 76 | :return: 77 | """ 78 | htmatrix = matrix.HtmlReportMatrix(str(tmpdir)) 79 | htmatrix.add_report(get_filepath("junit-simple_suite.xml")) 80 | htmatrix.add_report(get_filepath("junit-simple_suites.xml")) 81 | htmatrix.add_report(get_filepath("junit-unicode.xml")) 82 | htmatrix.add_report(get_filepath("junit-axis-linux.xml")) 83 | assert len(htmatrix.reports) == 4 84 | result = htmatrix.summary() 85 | assert result.endswith("") 86 | 87 | -------------------------------------------------------------------------------- /tests/test_matrix_stdout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from junit2htmlreport import runner 3 | from .inputfiles import get_filepath 4 | 5 | 6 | def test_matrix_stdout(capsys): 7 | runner.run(["--summary-matrix", get_filepath("junit-unicode.xml")]) 8 | 9 | out, err = capsys.readouterr() 10 | 11 | assert u"A Class with a cent ¢" in out 12 | assert u"Euro € Test Case" in out -------------------------------------------------------------------------------- /tests/test_parser_api.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | from junit2htmlreport import parser as j2h 4 | 5 | 6 | def test_public_api(): 7 | container = j2h.Junit(xmlstring=""" 8 | 9 | """) 10 | container.filename = "test_results.xml" 11 | document = j2h.Suite() 12 | container.suites = [document] 13 | document.name = "test report" 14 | document.duration = 0.1 15 | document.package = "com.tests" 16 | first = j2h.Class() 17 | first.name = "myclass" 18 | document.classes[first.name] = first 19 | 20 | test1 = j2h.Case() 21 | test1.name = "test_one" 22 | test1.duration = 1.1 23 | test1.testclass = first 24 | first.cases.append(test1) 25 | 26 | test2 = j2h.Case() 27 | test2.name = "test_two" 28 | test2.duration = 1.2 29 | test2.testclass = first 30 | first.cases.append(test2) 31 | 32 | skipped1 = j2h.Case() 33 | skipped1.name = "test_skippy" 34 | skipped1.duration = 1.3 35 | skipped1.testclass = first 36 | skipped1.skipped = "test skipped" 37 | skipped1.skipped_msg = "test was skipped at runtime" 38 | first.cases.append(skipped1) 39 | 40 | failed1 = j2h.Case() 41 | failed1.name = "test_bad" 42 | failed1.duration = 1.4 43 | failed1.testclass = first 44 | failed1.failure = "test failed" 45 | failed1.failure_msg = "an exception happened" 46 | first.cases.append(failed1) 47 | 48 | html = container.html() 49 | 50 | assert html 51 | assert "