├── .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 |
--------------------------------------------------------------------------------