├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.rst ├── junit_xml └── __init__.py ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── asserts.py ├── serializer.py ├── test_test_case.py └── test_test_suite.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | venv 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | coverage.xml 30 | htmlcov/ 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # Pycharm 41 | .idea/** 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - python: pypy 6 | dist: xenial 7 | sudo: true 8 | env: TOXENV=pypy 9 | - python: 3.5 10 | dist: xenial 11 | sudo: true 12 | env: TOXENV=py35 13 | - python: 3.7 14 | dist: xenial 15 | sudo: true 16 | env: TOXENV=py37 17 | - python: 3.8 18 | dist: xenial 19 | sudo: true 20 | env: TOXENV=py38 21 | 22 | env: 23 | - TOXENV=py27 24 | - TOXENV=py36 25 | - TOXENV=cover 26 | - TOXENV=flake8 27 | 28 | install: 29 | - travis_retry pip install tox==3.13.2 30 | 31 | script: 32 | - travis_retry tox 33 | 34 | git: 35 | depth: 1 36 | 37 | notifications: 38 | email: 39 | - brian@kyr.us 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Kyrus Tech, Inc., Brian Beyer 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ACTIVATE=venv/bin/activate 2 | 3 | venv: $(ACTIVATE) 4 | $(ACTIVATE): requirements.txt requirements_dev.txt 5 | test -d venv || virtualenv venv 6 | . $(ACTIVATE); pip install -r requirements_dev.txt 7 | 8 | .PHONY : dist 9 | dist: 10 | python setup.py sdist bdist_wheel 11 | 12 | .PHONY : clean 13 | clean: 14 | find . -name "*.pyc" -delete 15 | find . -name "__pycache__" -delete 16 | rm -rf build 17 | rm -rf dist 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | python-junit-xml 2 | ================ 3 | .. image:: https://travis-ci.org/kyrus/python-junit-xml.png?branch=master 4 | 5 | About 6 | ----- 7 | 8 | A Python module for creating JUnit XML test result documents that can be 9 | read by tools such as Jenkins or Bamboo. If you are ever working with test tool or 10 | test suite written in Python and want to take advantage of Jenkins' or Bamboo's 11 | pretty graphs and test reporting capabilities, this module will let you 12 | generate the XML test reports. 13 | 14 | *As there is no definitive Jenkins JUnit XSD that I could find, the XML 15 | documents created by this module support a schema based on Google 16 | searches and the Jenkins JUnit XML reader source code. File a bug if 17 | something doesn't work like you expect it to. 18 | For Bamboo situation is the same.* 19 | 20 | Installation 21 | ------------ 22 | 23 | Install using pip or easy_install: 24 | 25 | :: 26 | 27 | pip install junit-xml 28 | or 29 | easy_install junit-xml 30 | 31 | You can also clone the Git repository from Github and install it manually: 32 | 33 | :: 34 | 35 | git clone https://github.com/kyrus/python-junit-xml.git 36 | python setup.py install 37 | 38 | Using 39 | ----- 40 | 41 | Create a test suite, add a test case, and print it to the screen: 42 | 43 | .. code-block:: python 44 | 45 | from junit_xml import TestSuite, TestCase 46 | 47 | test_cases = [TestCase('Test1', 'some.class.name', 123.345, 'I am stdout!', 'I am stderr!')] 48 | ts = TestSuite("my test suite", test_cases) 49 | # pretty printing is on by default but can be disabled using prettyprint=False 50 | print(TestSuite.to_xml_string([ts])) 51 | 52 | Produces the following output 53 | 54 | .. code-block:: xml 55 | 56 | 57 | 58 | 59 | 60 | 61 | I am stdout! 62 | 63 | 64 | I am stderr! 65 | 66 | 67 | 68 | 69 | 70 | Writing XML to a file: 71 | 72 | .. code-block:: python 73 | 74 | # you can also write the XML to a file and not pretty print it 75 | with open('output.xml', 'w') as f: 76 | TestSuite.to_file(f, [ts], prettyprint=False) 77 | 78 | See the docs and unit tests for more examples. 79 | 80 | NOTE: Unicode characters identified as "illegal or discouraged" are automatically 81 | stripped from the XML string or file. 82 | 83 | Running the tests 84 | ----------------- 85 | 86 | :: 87 | 88 | # activate your virtualenv 89 | pip install tox 90 | tox 91 | 92 | Releasing a new version 93 | ----------------------- 94 | 95 | 1. Bump version in `setup.py` 96 | 2. Build distribution with `python setup.py sdist bdist_wheel` 97 | 3. Upload to Pypi with `twine upload dist/*` 98 | 4. Verify the new version was uploaded at https://pypi.org/project/junit-xml/#history 99 | -------------------------------------------------------------------------------- /junit_xml/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import warnings 4 | from collections import defaultdict 5 | import sys 6 | import re 7 | import xml.etree.ElementTree as ET 8 | import xml.dom.minidom 9 | 10 | from six import u, iteritems, PY2 11 | 12 | try: 13 | # Python 2 14 | unichr 15 | except NameError: # pragma: nocover 16 | # Python 3 17 | unichr = chr 18 | 19 | """ 20 | Based on the understanding of what Jenkins can parse for JUnit XML files. 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | the output of the testcase 33 | 34 | 35 | 36 | 37 | the output of the testcase 38 | 39 | 40 | 41 | 42 | the output of the testcase 43 | 44 | 45 | 46 | 47 | I am system output 48 | 49 | 50 | I am the error output 51 | 52 | 53 | 54 | 55 | """ 56 | 57 | 58 | def decode(var, encoding): 59 | """ 60 | If not already unicode, decode it. 61 | """ 62 | if PY2: 63 | if isinstance(var, unicode): # noqa: F821 64 | ret = var 65 | elif isinstance(var, str): 66 | if encoding: 67 | ret = var.decode(encoding) 68 | else: 69 | ret = unicode(var) # noqa: F821 70 | else: 71 | ret = unicode(var) # noqa: F821 72 | else: 73 | ret = str(var) 74 | return ret 75 | 76 | 77 | class TestSuite(object): 78 | """ 79 | Suite of test cases. 80 | Can handle unicode strings or binary strings if their encoding is provided. 81 | """ 82 | 83 | def __init__( 84 | self, 85 | name, 86 | test_cases=None, 87 | hostname=None, 88 | id=None, 89 | package=None, 90 | timestamp=None, 91 | properties=None, 92 | file=None, 93 | log=None, 94 | url=None, 95 | stdout=None, 96 | stderr=None, 97 | ): 98 | self.name = name 99 | if not test_cases: 100 | test_cases = [] 101 | try: 102 | iter(test_cases) 103 | except TypeError: 104 | raise TypeError("test_cases must be a list of test cases") 105 | self.test_cases = test_cases 106 | self.timestamp = timestamp 107 | self.hostname = hostname 108 | self.id = id 109 | self.package = package 110 | self.file = file 111 | self.log = log 112 | self.url = url 113 | self.stdout = stdout 114 | self.stderr = stderr 115 | self.properties = properties 116 | 117 | def build_xml_doc(self, encoding=None): 118 | """ 119 | Builds the XML document for the JUnit test suite. 120 | Produces clean unicode strings and decodes non-unicode with the help of encoding. 121 | @param encoding: Used to decode encoded strings. 122 | @return: XML document with unicode string elements 123 | """ 124 | 125 | # build the test suite element 126 | test_suite_attributes = dict() 127 | if any(c.assertions for c in self.test_cases): 128 | test_suite_attributes["assertions"] = str(sum([int(c.assertions) for c in self.test_cases if c.assertions])) 129 | test_suite_attributes["disabled"] = str(len([c for c in self.test_cases if not c.is_enabled])) 130 | test_suite_attributes["errors"] = str(len([c for c in self.test_cases if c.is_error()])) 131 | test_suite_attributes["failures"] = str(len([c for c in self.test_cases if c.is_failure()])) 132 | test_suite_attributes["name"] = decode(self.name, encoding) 133 | test_suite_attributes["skipped"] = str(len([c for c in self.test_cases if c.is_skipped()])) 134 | test_suite_attributes["tests"] = str(len(self.test_cases)) 135 | test_suite_attributes["time"] = str(sum(c.elapsed_sec for c in self.test_cases if c.elapsed_sec)) 136 | 137 | if self.hostname: 138 | test_suite_attributes["hostname"] = decode(self.hostname, encoding) 139 | if self.id: 140 | test_suite_attributes["id"] = decode(self.id, encoding) 141 | if self.package: 142 | test_suite_attributes["package"] = decode(self.package, encoding) 143 | if self.timestamp: 144 | test_suite_attributes["timestamp"] = decode(self.timestamp, encoding) 145 | if self.file: 146 | test_suite_attributes["file"] = decode(self.file, encoding) 147 | if self.log: 148 | test_suite_attributes["log"] = decode(self.log, encoding) 149 | if self.url: 150 | test_suite_attributes["url"] = decode(self.url, encoding) 151 | 152 | xml_element = ET.Element("testsuite", test_suite_attributes) 153 | 154 | # add any properties 155 | if self.properties: 156 | props_element = ET.SubElement(xml_element, "properties") 157 | for k, v in self.properties.items(): 158 | attrs = {"name": decode(k, encoding), "value": decode(v, encoding)} 159 | ET.SubElement(props_element, "property", attrs) 160 | 161 | # add test suite stdout 162 | if self.stdout: 163 | stdout_element = ET.SubElement(xml_element, "system-out") 164 | stdout_element.text = decode(self.stdout, encoding) 165 | 166 | # add test suite stderr 167 | if self.stderr: 168 | stderr_element = ET.SubElement(xml_element, "system-err") 169 | stderr_element.text = decode(self.stderr, encoding) 170 | 171 | # test cases 172 | for case in self.test_cases: 173 | test_case_attributes = dict() 174 | test_case_attributes["name"] = decode(case.name, encoding) 175 | if case.assertions: 176 | # Number of assertions in the test case 177 | test_case_attributes["assertions"] = "%d" % case.assertions 178 | if case.elapsed_sec: 179 | test_case_attributes["time"] = "%f" % case.elapsed_sec 180 | if case.timestamp: 181 | test_case_attributes["timestamp"] = decode(case.timestamp, encoding) 182 | if case.classname: 183 | test_case_attributes["classname"] = decode(case.classname, encoding) 184 | if case.status: 185 | test_case_attributes["status"] = decode(case.status, encoding) 186 | if case.category: 187 | test_case_attributes["class"] = decode(case.category, encoding) 188 | if case.file: 189 | test_case_attributes["file"] = decode(case.file, encoding) 190 | if case.line: 191 | test_case_attributes["line"] = decode(case.line, encoding) 192 | if case.log: 193 | test_case_attributes["log"] = decode(case.log, encoding) 194 | if case.url: 195 | test_case_attributes["url"] = decode(case.url, encoding) 196 | 197 | test_case_element = ET.SubElement(xml_element, "testcase", test_case_attributes) 198 | 199 | # failures 200 | for failure in case.failures: 201 | if failure["output"] or failure["message"]: 202 | attrs = {"type": "failure"} 203 | if failure["message"]: 204 | attrs["message"] = decode(failure["message"], encoding) 205 | if failure["type"]: 206 | attrs["type"] = decode(failure["type"], encoding) 207 | failure_element = ET.Element("failure", attrs) 208 | if failure["output"]: 209 | failure_element.text = decode(failure["output"], encoding) 210 | test_case_element.append(failure_element) 211 | 212 | # errors 213 | for error in case.errors: 214 | if error["message"] or error["output"]: 215 | attrs = {"type": "error"} 216 | if error["message"]: 217 | attrs["message"] = decode(error["message"], encoding) 218 | if error["type"]: 219 | attrs["type"] = decode(error["type"], encoding) 220 | error_element = ET.Element("error", attrs) 221 | if error["output"]: 222 | error_element.text = decode(error["output"], encoding) 223 | test_case_element.append(error_element) 224 | 225 | # skippeds 226 | for skipped in case.skipped: 227 | attrs = {"type": "skipped"} 228 | if skipped["message"]: 229 | attrs["message"] = decode(skipped["message"], encoding) 230 | skipped_element = ET.Element("skipped", attrs) 231 | if skipped["output"]: 232 | skipped_element.text = decode(skipped["output"], encoding) 233 | test_case_element.append(skipped_element) 234 | 235 | # test stdout 236 | if case.stdout: 237 | stdout_element = ET.Element("system-out") 238 | stdout_element.text = decode(case.stdout, encoding) 239 | test_case_element.append(stdout_element) 240 | 241 | # test stderr 242 | if case.stderr: 243 | stderr_element = ET.Element("system-err") 244 | stderr_element.text = decode(case.stderr, encoding) 245 | test_case_element.append(stderr_element) 246 | 247 | return xml_element 248 | 249 | @staticmethod 250 | def to_xml_string(test_suites, prettyprint=True, encoding=None): 251 | """ 252 | Returns the string representation of the JUnit XML document. 253 | @param encoding: The encoding of the input. 254 | @return: unicode string 255 | """ 256 | warnings.warn( 257 | "Testsuite.to_xml_string is deprecated. It will be removed in version 2.0.0. " 258 | "Use function to_xml_report_string", 259 | DeprecationWarning, 260 | ) 261 | return to_xml_report_string(test_suites, prettyprint, encoding) 262 | 263 | @staticmethod 264 | def to_file(file_descriptor, test_suites, prettyprint=True, encoding=None): 265 | """ 266 | Writes the JUnit XML document to a file. 267 | """ 268 | warnings.warn( 269 | "Testsuite.to_file is deprecated. It will be removed in version 2.0.0. Use function to_xml_report_file", 270 | DeprecationWarning, 271 | ) 272 | to_xml_report_file(file_descriptor, test_suites, prettyprint, encoding) 273 | 274 | 275 | def to_xml_report_string(test_suites, prettyprint=True, encoding=None): 276 | """ 277 | Returns the string representation of the JUnit XML document. 278 | @param encoding: The encoding of the input. 279 | @return: unicode string 280 | """ 281 | 282 | try: 283 | iter(test_suites) 284 | except TypeError: 285 | raise TypeError("test_suites must be a list of test suites") 286 | 287 | xml_element = ET.Element("testsuites") 288 | attributes = defaultdict(int) 289 | for ts in test_suites: 290 | ts_xml = ts.build_xml_doc(encoding=encoding) 291 | for key in ["disabled", "errors", "failures", "tests"]: 292 | attributes[key] += int(ts_xml.get(key, 0)) 293 | for key in ["time"]: 294 | attributes[key] += float(ts_xml.get(key, 0)) 295 | xml_element.append(ts_xml) 296 | for key, value in iteritems(attributes): 297 | xml_element.set(key, str(value)) 298 | 299 | xml_string = ET.tostring(xml_element, encoding=encoding) 300 | # is encoded now 301 | xml_string = _clean_illegal_xml_chars(xml_string.decode(encoding or "utf-8")) 302 | # is unicode now 303 | 304 | if prettyprint: 305 | # minidom.parseString() works just on correctly encoded binary strings 306 | xml_string = xml_string.encode(encoding or "utf-8") 307 | xml_string = xml.dom.minidom.parseString(xml_string) 308 | # toprettyxml() produces unicode if no encoding is being passed or binary string with an encoding 309 | xml_string = xml_string.toprettyxml(encoding=encoding) 310 | if encoding: 311 | xml_string = xml_string.decode(encoding) 312 | # is unicode now 313 | return xml_string 314 | 315 | 316 | def to_xml_report_file(file_descriptor, test_suites, prettyprint=True, encoding=None): 317 | """ 318 | Writes the JUnit XML document to a file. 319 | """ 320 | xml_string = to_xml_report_string(test_suites, prettyprint=prettyprint, encoding=encoding) 321 | # has problems with encoded str with non-ASCII (non-default-encoding) characters! 322 | file_descriptor.write(xml_string) 323 | 324 | 325 | def _clean_illegal_xml_chars(string_to_clean): 326 | """ 327 | Removes any illegal unicode characters from the given XML string. 328 | 329 | @see: http://stackoverflow.com/questions/1707890/fast-way-to-filter-illegal-xml-unicode-chars-in-python 330 | """ 331 | 332 | illegal_unichrs = [ 333 | (0x00, 0x08), 334 | (0x0B, 0x1F), 335 | (0x7F, 0x84), 336 | (0x86, 0x9F), 337 | (0xD800, 0xDFFF), 338 | (0xFDD0, 0xFDDF), 339 | (0xFFFE, 0xFFFF), 340 | (0x1FFFE, 0x1FFFF), 341 | (0x2FFFE, 0x2FFFF), 342 | (0x3FFFE, 0x3FFFF), 343 | (0x4FFFE, 0x4FFFF), 344 | (0x5FFFE, 0x5FFFF), 345 | (0x6FFFE, 0x6FFFF), 346 | (0x7FFFE, 0x7FFFF), 347 | (0x8FFFE, 0x8FFFF), 348 | (0x9FFFE, 0x9FFFF), 349 | (0xAFFFE, 0xAFFFF), 350 | (0xBFFFE, 0xBFFFF), 351 | (0xCFFFE, 0xCFFFF), 352 | (0xDFFFE, 0xDFFFF), 353 | (0xEFFFE, 0xEFFFF), 354 | (0xFFFFE, 0xFFFFF), 355 | (0x10FFFE, 0x10FFFF), 356 | ] 357 | 358 | illegal_ranges = ["%s-%s" % (unichr(low), unichr(high)) for (low, high) in illegal_unichrs if low < sys.maxunicode] 359 | 360 | illegal_xml_re = re.compile(u("[%s]") % u("").join(illegal_ranges)) 361 | return illegal_xml_re.sub("", string_to_clean) 362 | 363 | 364 | class TestCase(object): 365 | """A JUnit test case with a result and possibly some stdout or stderr""" 366 | 367 | def __init__( 368 | self, 369 | name, 370 | classname=None, 371 | elapsed_sec=None, 372 | stdout=None, 373 | stderr=None, 374 | assertions=None, 375 | timestamp=None, 376 | status=None, 377 | category=None, 378 | file=None, 379 | line=None, 380 | log=None, 381 | url=None, 382 | allow_multiple_subelements=False, 383 | ): 384 | self.name = name 385 | self.assertions = assertions 386 | self.elapsed_sec = elapsed_sec 387 | self.timestamp = timestamp 388 | self.classname = classname 389 | self.status = status 390 | self.category = category 391 | self.file = file 392 | self.line = line 393 | self.log = log 394 | self.url = url 395 | self.stdout = stdout 396 | self.stderr = stderr 397 | 398 | self.is_enabled = True 399 | self.errors = [] 400 | self.failures = [] 401 | self.skipped = [] 402 | self.allow_multiple_subalements = allow_multiple_subelements 403 | 404 | def add_error_info(self, message=None, output=None, error_type=None): 405 | """Adds an error message, output, or both to the test case""" 406 | error = {} 407 | error["message"] = message 408 | error["output"] = output 409 | error["type"] = error_type 410 | if self.allow_multiple_subalements: 411 | if message or output: 412 | self.errors.append(error) 413 | elif not len(self.errors): 414 | self.errors.append(error) 415 | else: 416 | if message: 417 | self.errors[0]["message"] = message 418 | if output: 419 | self.errors[0]["output"] = output 420 | if error_type: 421 | self.errors[0]["type"] = error_type 422 | 423 | def add_failure_info(self, message=None, output=None, failure_type=None): 424 | """Adds a failure message, output, or both to the test case""" 425 | failure = {} 426 | failure["message"] = message 427 | failure["output"] = output 428 | failure["type"] = failure_type 429 | if self.allow_multiple_subalements: 430 | if message or output: 431 | self.failures.append(failure) 432 | elif not len(self.failures): 433 | self.failures.append(failure) 434 | else: 435 | if message: 436 | self.failures[0]["message"] = message 437 | if output: 438 | self.failures[0]["output"] = output 439 | if failure_type: 440 | self.failures[0]["type"] = failure_type 441 | 442 | def add_skipped_info(self, message=None, output=None): 443 | """Adds a skipped message, output, or both to the test case""" 444 | skipped = {} 445 | skipped["message"] = message 446 | skipped["output"] = output 447 | if self.allow_multiple_subalements: 448 | if message or output: 449 | self.skipped.append(skipped) 450 | elif not len(self.skipped): 451 | self.skipped.append(skipped) 452 | else: 453 | if message: 454 | self.skipped[0]["message"] = message 455 | if output: 456 | self.skipped[0]["output"] = output 457 | 458 | def is_failure(self): 459 | """returns true if this test case is a failure""" 460 | return sum(1 for f in self.failures if f["message"] or f["output"]) > 0 461 | 462 | def is_error(self): 463 | """returns true if this test case is an error""" 464 | return sum(1 for e in self.errors if e["message"] or e["output"]) > 0 465 | 466 | def is_skipped(self): 467 | """returns true if this test case has been skipped""" 468 | return len(self.skipped) > 0 469 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = .git .tox build dist venv 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | flake8-black 3 | pytest-sugar 4 | pytest-flake8 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = .git,.tox,build,dist,venv 4 | 5 | [wheel] 6 | universal = 1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | import os 4 | 5 | 6 | def read(fname): 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | 9 | 10 | setup( 11 | name="junit-xml", 12 | author="Brian Beyer", 13 | author_email="brian@kyr.us", 14 | url="https://github.com/kyrus/python-junit-xml", 15 | license="MIT", 16 | packages=find_packages(exclude=["tests"]), 17 | description="Creates JUnit XML test result documents that can be read by tools such as Jenkins", 18 | long_description=read("README.rst"), 19 | version="1.9", 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "Intended Audience :: Developers", 23 | "License :: Freely Distributable", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Topic :: Software Development :: Build Tools", 29 | "Topic :: Software Development :: Testing", 30 | ], 31 | install_requires=["six"], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyrus/python-junit-xml/4bd08a272f059998cedf9b7779f944d49eba13a6/tests/__init__.py -------------------------------------------------------------------------------- /tests/asserts.py: -------------------------------------------------------------------------------- 1 | def verify_test_case( # noqa: E302 2 | test_case_element, 3 | expected_attributes, 4 | error_message=None, 5 | error_output=None, 6 | error_type=None, 7 | failure_message=None, 8 | failure_output=None, 9 | failure_type=None, 10 | skipped_message=None, 11 | skipped_output=None, 12 | stdout=None, 13 | stderr=None, 14 | errors=None, 15 | failures=None, 16 | skipped=None, 17 | ): 18 | for k, v in expected_attributes.items(): 19 | assert test_case_element.attributes[k].value == v 20 | 21 | for k in test_case_element.attributes.keys(): 22 | assert k in expected_attributes.keys() 23 | 24 | if stderr: 25 | assert test_case_element.getElementsByTagName("system-err")[0].firstChild.nodeValue.strip() == stderr 26 | if stdout: 27 | assert test_case_element.getElementsByTagName("system-out")[0].firstChild.nodeValue.strip() == stdout 28 | 29 | _errors = test_case_element.getElementsByTagName("error") 30 | if error_message or error_output: 31 | assert len(_errors) > 0 32 | elif errors: 33 | assert len(errors) == len(_errors) 34 | else: 35 | assert len(_errors) == 0 36 | 37 | if error_message: 38 | assert _errors[0].attributes["message"].value == error_message 39 | 40 | if error_type and _errors: 41 | assert _errors[0].attributes["type"].value == error_type 42 | 43 | if error_output: 44 | assert _errors[0].firstChild.nodeValue.strip() == error_output 45 | 46 | for error_exp, error_r in zip(errors or [], _errors): 47 | assert error_r.attributes["message"].value == error_exp["message"] 48 | assert error_r.firstChild.nodeValue.strip() == error_exp["output"] 49 | assert error_r.attributes["type"].value == error_exp["type"] 50 | 51 | _failures = test_case_element.getElementsByTagName("failure") 52 | if failure_message or failure_output: 53 | assert len(_failures) > 0 54 | elif failures: 55 | assert len(failures) == len(_failures) 56 | else: 57 | assert len(_failures) == 0 58 | 59 | if failure_message: 60 | assert _failures[0].attributes["message"].value == failure_message 61 | 62 | if failure_type and _failures: 63 | assert _failures[0].attributes["type"].value == failure_type 64 | 65 | if failure_output: 66 | assert _failures[0].firstChild.nodeValue.strip() == failure_output 67 | 68 | for failure_exp, failure_r in zip(failures or [], _failures): 69 | assert failure_r.attributes["message"].value == failure_exp["message"] 70 | assert failure_r.firstChild.nodeValue.strip() == failure_exp["output"] 71 | assert failure_r.attributes["type"].value == failure_exp["type"] 72 | 73 | _skipped = test_case_element.getElementsByTagName("skipped") 74 | if skipped_message or skipped_output: 75 | assert len(_skipped) > 0 76 | elif skipped: 77 | assert len(skipped) == len(_skipped) 78 | else: 79 | assert len(_skipped) == 0 80 | 81 | for skipped_exp, skipped_r in zip(skipped or [], _skipped): 82 | assert skipped_r.attributes["message"].value == skipped_exp["message"] 83 | assert skipped_r.firstChild.nodeValue.strip() == skipped_exp["output"] 84 | -------------------------------------------------------------------------------- /tests/serializer.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import tempfile 4 | from xml.dom import minidom 5 | 6 | from six import PY2 7 | 8 | from junit_xml import to_xml_report_file, to_xml_report_string 9 | 10 | 11 | def serialize_and_read(test_suites, to_file=False, prettyprint=False, encoding=None): 12 | """writes the test suite to an XML string and then re-reads it using minidom, 13 | returning => (test suite element, list of test case elements)""" 14 | try: 15 | iter(test_suites) 16 | except TypeError: 17 | test_suites = [test_suites] 18 | 19 | if to_file: 20 | fd, filename = tempfile.mkstemp(text=True) 21 | os.close(fd) 22 | with codecs.open(filename, mode="w", encoding=encoding) as f: 23 | to_xml_report_file(f, test_suites, prettyprint=prettyprint, encoding=encoding) 24 | print("Serialized XML to temp file [%s]" % filename) 25 | xmldoc = minidom.parse(filename) 26 | os.remove(filename) 27 | else: 28 | xml_string = to_xml_report_string(test_suites, prettyprint=prettyprint, encoding=encoding) 29 | if PY2: 30 | assert isinstance(xml_string, unicode) # noqa: F821 31 | print("Serialized XML to string:\n%s" % xml_string) 32 | if encoding: 33 | xml_string = xml_string.encode(encoding) 34 | xmldoc = minidom.parseString(xml_string) 35 | 36 | def remove_blanks(node): 37 | for x in node.childNodes: 38 | if x.nodeType == minidom.Node.TEXT_NODE: 39 | if x.nodeValue: 40 | x.nodeValue = x.nodeValue.strip() 41 | elif x.nodeType == minidom.Node.ELEMENT_NODE: 42 | remove_blanks(x) 43 | 44 | remove_blanks(xmldoc) 45 | xmldoc.normalize() 46 | 47 | ret = [] 48 | suites = xmldoc.getElementsByTagName("testsuites")[0] 49 | for suite in suites.getElementsByTagName("testsuite"): 50 | cases = suite.getElementsByTagName("testcase") 51 | ret.append((suite, cases)) 52 | return ret 53 | -------------------------------------------------------------------------------- /tests/test_test_case.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import with_statement 3 | 4 | from six import u 5 | 6 | from .asserts import verify_test_case 7 | from junit_xml import TestCase as Case 8 | from junit_xml import TestSuite as Suite 9 | from junit_xml import decode 10 | from .serializer import serialize_and_read 11 | 12 | 13 | def test_init(): 14 | ts, tcs = serialize_and_read(Suite("test", [Case("Test1")]))[0] 15 | verify_test_case(tcs[0], {"name": "Test1"}) 16 | 17 | 18 | def test_init_classname(): 19 | ts, tcs = serialize_and_read(Suite("test", [Case(name="Test1", classname="some.class.name")]))[0] 20 | verify_test_case(tcs[0], {"name": "Test1", "classname": "some.class.name"}) 21 | 22 | 23 | def test_init_classname_time(): 24 | ts, tcs = serialize_and_read(Suite("test", [Case(name="Test1", classname="some.class.name", elapsed_sec=123.345)]))[ 25 | 0 26 | ] 27 | verify_test_case(tcs[0], {"name": "Test1", "classname": "some.class.name", "time": ("%f" % 123.345)}) 28 | 29 | 30 | def test_init_classname_time_timestamp(): 31 | ts, tcs = serialize_and_read( 32 | Suite("test", [Case(name="Test1", classname="some.class.name", elapsed_sec=123.345, timestamp=99999)]) 33 | )[0] 34 | verify_test_case( 35 | tcs[0], {"name": "Test1", "classname": "some.class.name", "time": ("%f" % 123.345), "timestamp": ("%s" % 99999)} 36 | ) 37 | 38 | 39 | def test_init_stderr(): 40 | ts, tcs = serialize_and_read( 41 | Suite("test", [Case(name="Test1", classname="some.class.name", elapsed_sec=123.345, stderr="I am stderr!")]) 42 | )[0] 43 | verify_test_case( 44 | tcs[0], {"name": "Test1", "classname": "some.class.name", "time": ("%f" % 123.345)}, stderr="I am stderr!" 45 | ) 46 | 47 | 48 | def test_init_stdout_stderr(): 49 | ts, tcs = serialize_and_read( 50 | Suite( 51 | "test", 52 | [ 53 | Case( 54 | name="Test1", 55 | classname="some.class.name", 56 | elapsed_sec=123.345, 57 | stdout="I am stdout!", 58 | stderr="I am stderr!", 59 | ) 60 | ], 61 | ) 62 | )[0] 63 | verify_test_case( 64 | tcs[0], 65 | {"name": "Test1", "classname": "some.class.name", "time": ("%f" % 123.345)}, 66 | stdout="I am stdout!", 67 | stderr="I am stderr!", 68 | ) 69 | 70 | 71 | def test_init_disable(): 72 | tc = Case("Disabled-Test") 73 | tc.is_enabled = False 74 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 75 | verify_test_case(tcs[0], {"name": "Disabled-Test"}) 76 | 77 | 78 | def test_init_failure_message(): 79 | tc = Case("Failure-Message") 80 | tc.add_failure_info("failure message") 81 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 82 | verify_test_case(tcs[0], {"name": "Failure-Message"}, failure_message="failure message") 83 | 84 | 85 | def test_init_failure_output(): 86 | tc = Case("Failure-Output") 87 | tc.add_failure_info(output="I failed!") 88 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 89 | verify_test_case(tcs[0], {"name": "Failure-Output"}, failure_output="I failed!") 90 | 91 | 92 | def test_init_failure_type(): 93 | tc = Case("Failure-Type") 94 | tc.add_failure_info(failure_type="com.example.Error") 95 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 96 | verify_test_case(tcs[0], {"name": "Failure-Type"}) 97 | 98 | tc.add_failure_info("failure message") 99 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 100 | verify_test_case( 101 | tcs[0], {"name": "Failure-Type"}, failure_message="failure message", failure_type="com.example.Error" 102 | ) 103 | 104 | 105 | def test_init_failure(): 106 | tc = Case("Failure-Message-and-Output") 107 | tc.add_failure_info("failure message", "I failed!") 108 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 109 | verify_test_case( 110 | tcs[0], 111 | {"name": "Failure-Message-and-Output"}, 112 | failure_message="failure message", 113 | failure_output="I failed!", 114 | failure_type="failure", 115 | ) 116 | 117 | 118 | def test_init_error_message(): 119 | tc = Case("Error-Message") 120 | tc.add_error_info("error message") 121 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 122 | verify_test_case(tcs[0], {"name": "Error-Message"}, error_message="error message") 123 | 124 | 125 | def test_init_error_output(): 126 | tc = Case("Error-Output") 127 | tc.add_error_info(output="I errored!") 128 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 129 | verify_test_case(tcs[0], {"name": "Error-Output"}, error_output="I errored!") 130 | 131 | 132 | def test_init_error_type(): 133 | tc = Case("Error-Type") 134 | tc.add_error_info(error_type="com.example.Error") 135 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 136 | verify_test_case(tcs[0], {"name": "Error-Type"}) 137 | 138 | tc.add_error_info("error message") 139 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 140 | verify_test_case(tcs[0], {"name": "Error-Type"}, error_message="error message", error_type="com.example.Error") 141 | 142 | 143 | def test_init_error(): 144 | tc = Case("Error-Message-and-Output") 145 | tc.add_error_info("error message", "I errored!") 146 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 147 | verify_test_case( 148 | tcs[0], 149 | {"name": "Error-Message-and-Output"}, 150 | error_message="error message", 151 | error_output="I errored!", 152 | error_type="error", 153 | ) 154 | 155 | 156 | def test_init_skipped_message(): 157 | tc = Case("Skipped-Message") 158 | tc.add_skipped_info("skipped message") 159 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 160 | verify_test_case(tcs[0], {"name": "Skipped-Message"}, skipped_message="skipped message") 161 | 162 | 163 | def test_init_skipped_output(): 164 | tc = Case("Skipped-Output") 165 | tc.add_skipped_info(output="I skipped!") 166 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 167 | verify_test_case(tcs[0], {"name": "Skipped-Output"}, skipped_output="I skipped!") 168 | 169 | 170 | def test_init_skipped_err_output(): 171 | tc = Case("Skipped-Output") 172 | tc.add_skipped_info(output="I skipped!") 173 | tc.add_error_info(output="I skipped with an error!") 174 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 175 | verify_test_case( 176 | tcs[0], {"name": "Skipped-Output"}, skipped_output="I skipped!", error_output="I skipped with an error!" 177 | ) 178 | 179 | 180 | def test_init_skipped(): 181 | tc = Case("Skipped-Message-and-Output") 182 | tc.add_skipped_info("skipped message", "I skipped!") 183 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 184 | verify_test_case( 185 | tcs[0], {"name": "Skipped-Message-and-Output"}, skipped_message="skipped message", skipped_output="I skipped!" 186 | ) 187 | 188 | 189 | def test_init_legal_unicode_char(): 190 | tc = Case("Failure-Message") 191 | tc.add_failure_info(u("failure message with legal unicode char: [\x22]")) 192 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 193 | verify_test_case( 194 | tcs[0], {"name": "Failure-Message"}, failure_message=u("failure message with legal unicode char: [\x22]") 195 | ) 196 | 197 | 198 | def test_init_illegal_unicode_char(): 199 | tc = Case("Failure-Message") 200 | tc.add_failure_info(u("failure message with illegal unicode char: [\x02]")) 201 | ts, tcs = serialize_and_read(Suite("test", [tc]))[0] 202 | verify_test_case( 203 | tcs[0], {"name": "Failure-Message"}, failure_message=u("failure message with illegal unicode char: []") 204 | ) 205 | 206 | 207 | def test_init_utf8(): 208 | tc = Case( 209 | name="Test äöü", 210 | classname="some.class.name.äöü", 211 | elapsed_sec=123.345, 212 | stdout="I am stdöüt!", 213 | stderr="I am stdärr!", 214 | ) 215 | tc.add_skipped_info(message="Skipped äöü", output="I skippäd!") 216 | tc.add_error_info(message="Skipped error äöü", output="I skippäd with an error!") 217 | test_suite = Suite("Test UTF-8", [tc]) 218 | ts, tcs = serialize_and_read(test_suite, encoding="utf-8")[0] 219 | verify_test_case( 220 | tcs[0], 221 | { 222 | "name": decode("Test äöü", "utf-8"), 223 | "classname": decode("some.class.name.äöü", "utf-8"), 224 | "time": ("%f" % 123.345), 225 | }, 226 | stdout=decode("I am stdöüt!", "utf-8"), 227 | stderr=decode("I am stdärr!", "utf-8"), 228 | skipped_message=decode("Skipped äöü", "utf-8"), 229 | skipped_output=decode("I skippäd!", "utf-8"), 230 | error_message=decode("Skipped error äöü", "utf-8"), 231 | error_output=decode("I skippäd with an error!", "utf-8"), 232 | ) 233 | 234 | 235 | def test_init_unicode(): 236 | tc = Case( 237 | name=decode("Test äöü", "utf-8"), 238 | classname=decode("some.class.name.äöü", "utf-8"), 239 | elapsed_sec=123.345, 240 | stdout=decode("I am stdöüt!", "utf-8"), 241 | stderr=decode("I am stdärr!", "utf-8"), 242 | ) 243 | tc.add_skipped_info(message=decode("Skipped äöü", "utf-8"), output=decode("I skippäd!", "utf-8")) 244 | tc.add_error_info(message=decode("Skipped error äöü", "utf-8"), output=decode("I skippäd with an error!", "utf-8")) 245 | 246 | ts, tcs = serialize_and_read(Suite("Test Unicode", [tc]))[0] 247 | verify_test_case( 248 | tcs[0], 249 | { 250 | "name": decode("Test äöü", "utf-8"), 251 | "classname": decode("some.class.name.äöü", "utf-8"), 252 | "time": ("%f" % 123.345), 253 | }, 254 | stdout=decode("I am stdöüt!", "utf-8"), 255 | stderr=decode("I am stdärr!", "utf-8"), 256 | skipped_message=decode("Skipped äöü", "utf-8"), 257 | skipped_output=decode("I skippäd!", "utf-8"), 258 | error_message=decode("Skipped error äöü", "utf-8"), 259 | error_output=decode("I skippäd with an error!", "utf-8"), 260 | ) 261 | 262 | 263 | def test_multiple_errors(): 264 | """Tests multiple errors in one test case""" 265 | tc = Case("Multiple error", allow_multiple_subelements=True) 266 | tc.add_error_info("First error", "First error message") 267 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 268 | verify_test_case( 269 | tcs[0], 270 | {"name": "Multiple error"}, 271 | errors=[{"message": "First error", "output": "First error message", "type": "error"}], 272 | ) 273 | tc.add_error_info("Second error", "Second error message") 274 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 275 | verify_test_case( 276 | tcs[0], 277 | {"name": "Multiple error"}, 278 | errors=[ 279 | {"message": "First error", "output": "First error message", "type": "error"}, 280 | {"message": "Second error", "output": "Second error message", "type": "error"}, 281 | ], 282 | ) 283 | 284 | 285 | def test_multiple_failures(): 286 | """Tests multiple failures in one test case""" 287 | tc = Case("Multiple failures", allow_multiple_subelements=True) 288 | tc.add_failure_info("First failure", "First failure message") 289 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 290 | verify_test_case( 291 | tcs[0], 292 | {"name": "Multiple failures"}, 293 | failures=[{"message": "First failure", "output": "First failure message", "type": "failure"}], 294 | ) 295 | tc.add_failure_info("Second failure", "Second failure message") 296 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 297 | verify_test_case( 298 | tcs[0], 299 | {"name": "Multiple failures"}, 300 | failures=[ 301 | {"message": "First failure", "output": "First failure message", "type": "failure"}, 302 | {"message": "Second failure", "output": "Second failure message", "type": "failure"}, 303 | ], 304 | ) 305 | 306 | 307 | def test_multiple_skipped(): 308 | """Tests multiple skipped messages in one test case""" 309 | tc = Case("Multiple skipped", allow_multiple_subelements=True) 310 | tc.add_skipped_info("First skipped", "First skipped message") 311 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 312 | verify_test_case( 313 | tcs[0], {"name": "Multiple skipped"}, skipped=[{"message": "First skipped", "output": "First skipped message"}] 314 | ) 315 | tc.add_skipped_info("Second skipped", "Second skipped message") 316 | (_, tcs) = serialize_and_read(Suite("test", [tc]))[0] 317 | verify_test_case( 318 | tcs[0], 319 | {"name": "Multiple skipped"}, 320 | skipped=[ 321 | {"message": "First skipped", "output": "First skipped message"}, 322 | {"message": "Second skipped", "output": "Second skipped message"}, 323 | ], 324 | ) 325 | -------------------------------------------------------------------------------- /tests/test_test_suite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from __future__ import with_statement 3 | 4 | import textwrap 5 | import warnings 6 | 7 | import pytest 8 | from six import PY2, StringIO 9 | 10 | from .asserts import verify_test_case 11 | from junit_xml import TestCase as Case 12 | from junit_xml import TestSuite as Suite 13 | from junit_xml import decode, to_xml_report_string 14 | from .serializer import serialize_and_read 15 | 16 | 17 | def test_single_suite_single_test_case(): 18 | with pytest.raises(TypeError) as excinfo: 19 | serialize_and_read(Suite("test", Case("Test1")), to_file=True)[0] 20 | assert str(excinfo.value) == "test_cases must be a list of test cases" 21 | 22 | 23 | def test_single_suite_no_test_cases(): 24 | properties = {"foo": "bar"} 25 | package = "mypackage" 26 | timestamp = 1398382805 27 | 28 | ts, tcs = serialize_and_read( 29 | Suite( 30 | name="test", 31 | test_cases=[], 32 | hostname="localhost", 33 | id=1, 34 | properties=properties, 35 | package=package, 36 | timestamp=timestamp, 37 | ), 38 | to_file=True, 39 | prettyprint=True, 40 | )[0] 41 | assert ts.tagName == "testsuite" 42 | assert ts.attributes["package"].value == package 43 | assert ts.attributes["timestamp"].value == str(timestamp) 44 | assert ts.childNodes[0].childNodes[0].attributes["name"].value == "foo" 45 | assert ts.childNodes[0].childNodes[0].attributes["value"].value == "bar" 46 | 47 | 48 | def test_single_suite_no_test_cases_utf8(): 49 | properties = {"foö": "bär"} 50 | package = "mypäckage" 51 | timestamp = 1398382805 52 | 53 | test_suite = Suite( 54 | name="äöü", 55 | test_cases=[], 56 | hostname="löcalhost", 57 | id="äöü", 58 | properties=properties, 59 | package=package, 60 | timestamp=timestamp, 61 | ) 62 | ts, tcs = serialize_and_read(test_suite, to_file=True, prettyprint=True, encoding="utf-8")[0] 63 | assert ts.tagName == "testsuite" 64 | assert ts.attributes["package"].value == decode(package, "utf-8") 65 | assert ts.attributes["timestamp"].value == str(timestamp) 66 | assert ts.childNodes[0].childNodes[0].attributes["name"].value == decode("foö", "utf-8") 67 | assert ts.childNodes[0].childNodes[0].attributes["value"].value == decode("bär", "utf-8") 68 | 69 | 70 | def test_single_suite_no_test_cases_unicode(): 71 | properties = {decode("foö", "utf-8"): decode("bär", "utf-8")} 72 | package = decode("mypäckage", "utf-8") 73 | timestamp = 1398382805 74 | 75 | ts, tcs = serialize_and_read( 76 | Suite( 77 | name=decode("äöü", "utf-8"), 78 | test_cases=[], 79 | hostname=decode("löcalhost", "utf-8"), 80 | id=decode("äöü", "utf-8"), 81 | properties=properties, 82 | package=package, 83 | timestamp=timestamp, 84 | ), 85 | to_file=True, 86 | prettyprint=True, 87 | encoding="utf-8", 88 | )[0] 89 | assert ts.tagName == "testsuite" 90 | assert ts.attributes["package"].value == package 91 | assert ts.attributes["timestamp"].value, str(timestamp) 92 | assert ts.childNodes[0].childNodes[0].attributes["name"].value == decode("foö", "utf-8") 93 | assert ts.childNodes[0].childNodes[0].attributes["value"].value == decode("bär", "utf-8") 94 | 95 | 96 | def test_single_suite_to_file(): 97 | ts, tcs = serialize_and_read(Suite("test", [Case("Test1")]), to_file=True)[0] 98 | verify_test_case(tcs[0], {"name": "Test1"}) 99 | 100 | 101 | def test_single_suite_to_file_prettyprint(): 102 | ts, tcs = serialize_and_read(Suite("test", [Case("Test1")]), to_file=True, prettyprint=True)[0] 103 | verify_test_case(tcs[0], {"name": "Test1"}) 104 | 105 | 106 | def test_single_suite_prettyprint(): 107 | ts, tcs = serialize_and_read(Suite("test", [Case("Test1")]), to_file=False, prettyprint=True)[0] 108 | verify_test_case(tcs[0], {"name": "Test1"}) 109 | 110 | 111 | def test_single_suite_to_file_no_prettyprint(): 112 | ts, tcs = serialize_and_read(Suite("test", [Case("Test1")]), to_file=True, prettyprint=False)[0] 113 | verify_test_case(tcs[0], {"name": "Test1"}) 114 | 115 | 116 | def test_multiple_suites_to_file(): 117 | tss = [Suite("suite1", [Case("Test1")]), Suite("suite2", [Case("Test2")])] 118 | suites = serialize_and_read(tss, to_file=True) 119 | 120 | assert suites[0][0].attributes["name"].value == "suite1" 121 | verify_test_case(suites[0][1][0], {"name": "Test1"}) 122 | 123 | assert suites[1][0].attributes["name"].value == "suite2" 124 | verify_test_case(suites[1][1][0], {"name": "Test2"}) 125 | 126 | 127 | def test_multiple_suites_to_string(): 128 | tss = [Suite("suite1", [Case("Test1")]), Suite("suite2", [Case("Test2")])] 129 | suites = serialize_and_read(tss) 130 | 131 | assert suites[0][0].attributes["name"].value == "suite1" 132 | verify_test_case(suites[0][1][0], {"name": "Test1"}) 133 | 134 | assert suites[1][0].attributes["name"].value == "suite2" 135 | verify_test_case(suites[1][1][0], {"name": "Test2"}) 136 | 137 | 138 | def test_attribute_time(): 139 | tss = [ 140 | Suite( 141 | "suite1", 142 | [ 143 | Case(name="Test1", classname="some.class.name", elapsed_sec=123.345), 144 | Case(name="Test2", classname="some2.class.name", elapsed_sec=123.345), 145 | ], 146 | ), 147 | Suite("suite2", [Case("Test2")]), 148 | ] 149 | suites = serialize_and_read(tss) 150 | 151 | assert suites[0][0].attributes["name"].value == "suite1" 152 | assert suites[0][0].attributes["time"].value == "246.69" 153 | 154 | assert suites[1][0].attributes["name"].value == "suite2" 155 | # here the time in testsuite is "0" even there is no attribute time for 156 | # testcase 157 | assert suites[1][0].attributes["time"].value == "0" 158 | 159 | 160 | def test_attribute_disable(): 161 | tc = Case("Disabled-Test") 162 | tc.is_enabled = False 163 | tss = [Suite("suite1", [tc])] 164 | suites = serialize_and_read(tss) 165 | 166 | assert suites[0][0].attributes["disabled"].value == "1" 167 | 168 | 169 | def test_stderr(): 170 | suites = serialize_and_read(Suite(name="test", stderr="I am stderr!", test_cases=[Case(name="Test1")]))[0] 171 | assert suites[0].getElementsByTagName("system-err")[0].firstChild.data == "I am stderr!" 172 | 173 | 174 | def test_stdout_stderr(): 175 | suites = serialize_and_read( 176 | Suite(name="test", stdout="I am stdout!", stderr="I am stderr!", test_cases=[Case(name="Test1")]) 177 | )[0] 178 | assert suites[0].getElementsByTagName("system-err")[0].firstChild.data == "I am stderr!" 179 | assert suites[0].getElementsByTagName("system-out")[0].firstChild.data == "I am stdout!" 180 | 181 | 182 | def test_no_assertions(): 183 | suites = serialize_and_read(Suite(name="test", test_cases=[Case(name="Test1")]))[0] 184 | assert not suites[0].getElementsByTagName("testcase")[0].hasAttribute("assertions") 185 | 186 | 187 | def test_assertions(): 188 | suites = serialize_and_read(Suite(name="test", test_cases=[Case(name="Test1", assertions=5)]))[0] 189 | assert suites[0].getElementsByTagName("testcase")[0].attributes["assertions"].value == "5" 190 | 191 | # @todo: add more tests for the other attributes and properties 192 | 193 | 194 | def test_to_xml_string(): 195 | test_suites = [ 196 | Suite(name="suite1", test_cases=[Case(name="Test1")]), 197 | Suite(name="suite2", test_cases=[Case(name="Test2")]), 198 | ] 199 | xml_string = to_xml_report_string(test_suites) 200 | if PY2: 201 | assert isinstance(xml_string, unicode) # noqa: F821 202 | expected_xml_string = textwrap.dedent( 203 | """ 204 | 205 | 206 | \t 207 | \t\t 208 | \t 209 | \t 210 | \t\t 211 | \t 212 | 213 | """.strip( 214 | "\n" 215 | ) 216 | ) 217 | assert xml_string == expected_xml_string 218 | 219 | 220 | def test_to_xml_string_test_suites_not_a_list(): 221 | test_suites = Suite("suite1", [Case("Test1")]) 222 | 223 | with pytest.raises(TypeError) as excinfo: 224 | to_xml_report_string(test_suites) 225 | assert str(excinfo.value) == "test_suites must be a list of test suites" 226 | 227 | 228 | def test_deprecated_to_xml_string(): 229 | with warnings.catch_warnings(record=True) as w: 230 | Suite.to_xml_string([]) 231 | assert len(w) == 1 232 | assert issubclass(w[0].category, DeprecationWarning) 233 | assert "Testsuite.to_xml_string is deprecated" in str(w[0].message) 234 | 235 | 236 | def test_deprecated_to_file(): 237 | with warnings.catch_warnings(record=True) as w: 238 | Suite.to_file(StringIO(), []) 239 | assert len(w) == 1 240 | assert issubclass(w[0].category, DeprecationWarning) 241 | assert "Testsuite.to_file is deprecated" in str(w[0].message) 242 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, pypy, py35, py36, py37, py38, cover, flake8 3 | sitepackages = False 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | pytest-sugar 9 | six 10 | commands = 11 | py.test \ 12 | --junitxml={envlogdir}/junit-{envname}.xml \ 13 | {posargs} 14 | 15 | [testenv:cover] 16 | deps = 17 | pytest 18 | pytest-sugar 19 | pytest-cov 20 | six 21 | commands = 22 | py.test \ 23 | --cov=junit_xml \ 24 | --cov-report=term-missing \ 25 | --cov-report=xml \ 26 | --cov-report=html \ 27 | {posargs} 28 | 29 | [testenv:flake8] 30 | deps = 31 | flake8-black 32 | pytest 33 | pytest-sugar 34 | pytest-flake8 35 | six 36 | commands = 37 | py.test \ 38 | --flake8 \ 39 | {posargs} 40 | --------------------------------------------------------------------------------