1import os
2import platform
3from datetime import datetime
4from typing import cast
5from typing import List
6from typing import Tuple
7from xml.dom import minidom
8
9import py
10import xmlschema
11
12import pytest
13from _pytest.compat import TYPE_CHECKING
14from _pytest.config import Config
15from _pytest.junitxml import bin_xml_escape
16from _pytest.junitxml import LogXML
17from _pytest.pathlib import Path
18from _pytest.reports import BaseReport
19from _pytest.reports import TestReport
20from _pytest.store import Store
21
22
23@pytest.fixture(scope="session")
24def schema():
25    """Return an xmlschema.XMLSchema object for the junit-10.xsd file."""
26    fn = Path(__file__).parent / "example_scripts/junit-10.xsd"
27    with fn.open() as f:
28        return xmlschema.XMLSchema(f)
29
30
31@pytest.fixture
32def run_and_parse(testdir, schema):
33    """Fixture that returns a function that can be used to execute pytest and
34    return the parsed ``DomNode`` of the root xml node.
35
36    The ``family`` parameter is used to configure the ``junit_family`` of the written report.
37    "xunit2" is also automatically validated against the schema.
38    """
39
40    def run(*args, family="xunit1"):
41        if family:
42            args = ("-o", "junit_family=" + family) + args
43        xml_path = testdir.tmpdir.join("junit.xml")
44        result = testdir.runpytest("--junitxml=%s" % xml_path, *args)
45        if family == "xunit2":
46            with xml_path.open() as f:
47                schema.validate(f)
48        xmldoc = minidom.parse(str(xml_path))
49        return result, DomNode(xmldoc)
50
51    return run
52
53
54def assert_attr(node, **kwargs):
55    __tracebackhide__ = True
56
57    def nodeval(node, name):
58        anode = node.getAttributeNode(name)
59        if anode is not None:
60            return anode.value
61
62    expected = {name: str(value) for name, value in kwargs.items()}
63    on_node = {name: nodeval(node, name) for name in expected}
64    assert on_node == expected
65
66
67class DomNode:
68    def __init__(self, dom):
69        self.__node = dom
70
71    def __repr__(self):
72        return self.__node.toxml()
73
74    def find_first_by_tag(self, tag):
75        return self.find_nth_by_tag(tag, 0)
76
77    def _by_tag(self, tag):
78        return self.__node.getElementsByTagName(tag)
79
80    @property
81    def children(self):
82        return [type(self)(x) for x in self.__node.childNodes]
83
84    @property
85    def get_unique_child(self):
86        children = self.children
87        assert len(children) == 1
88        return children[0]
89
90    def find_nth_by_tag(self, tag, n):
91        items = self._by_tag(tag)
92        try:
93            nth = items[n]
94        except IndexError:
95            pass
96        else:
97            return type(self)(nth)
98
99    def find_by_tag(self, tag):
100        t = type(self)
101        return [t(x) for x in self.__node.getElementsByTagName(tag)]
102
103    def __getitem__(self, key):
104        node = self.__node.getAttributeNode(key)
105        if node is not None:
106            return node.value
107
108    def assert_attr(self, **kwargs):
109        __tracebackhide__ = True
110        return assert_attr(self.__node, **kwargs)
111
112    def toxml(self):
113        return self.__node.toxml()
114
115    @property
116    def text(self):
117        return self.__node.childNodes[0].wholeText
118
119    @property
120    def tag(self):
121        return self.__node.tagName
122
123    @property
124    def next_sibling(self):
125        return type(self)(self.__node.nextSibling)
126
127
128parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"])
129
130
131class TestPython:
132    @parametrize_families
133    def test_summing_simple(self, testdir, run_and_parse, xunit_family):
134        testdir.makepyfile(
135            """
136            import pytest
137            def test_pass():
138                pass
139            def test_fail():
140                assert 0
141            def test_skip():
142                pytest.skip("")
143            @pytest.mark.xfail
144            def test_xfail():
145                assert 0
146            @pytest.mark.xfail
147            def test_xpass():
148                assert 1
149        """
150        )
151        result, dom = run_and_parse(family=xunit_family)
152        assert result.ret
153        node = dom.find_first_by_tag("testsuite")
154        node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5)
155
156    @parametrize_families
157    def test_summing_simple_with_errors(self, testdir, run_and_parse, xunit_family):
158        testdir.makepyfile(
159            """
160            import pytest
161            @pytest.fixture
162            def fixture():
163                raise Exception()
164            def test_pass():
165                pass
166            def test_fail():
167                assert 0
168            def test_error(fixture):
169                pass
170            @pytest.mark.xfail
171            def test_xfail():
172                assert False
173            @pytest.mark.xfail(strict=True)
174            def test_xpass():
175                assert True
176        """
177        )
178        result, dom = run_and_parse(family=xunit_family)
179        assert result.ret
180        node = dom.find_first_by_tag("testsuite")
181        node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5)
182
183    @parametrize_families
184    def test_hostname_in_xml(self, testdir, run_and_parse, xunit_family):
185        testdir.makepyfile(
186            """
187            def test_pass():
188                pass
189        """
190        )
191        result, dom = run_and_parse(family=xunit_family)
192        node = dom.find_first_by_tag("testsuite")
193        node.assert_attr(hostname=platform.node())
194
195    @parametrize_families
196    def test_timestamp_in_xml(self, testdir, run_and_parse, xunit_family):
197        testdir.makepyfile(
198            """
199            def test_pass():
200                pass
201        """
202        )
203        start_time = datetime.now()
204        result, dom = run_and_parse(family=xunit_family)
205        node = dom.find_first_by_tag("testsuite")
206        timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f")
207        assert start_time <= timestamp < datetime.now()
208
209    def test_timing_function(self, testdir, run_and_parse, mock_timing):
210        testdir.makepyfile(
211            """
212            from _pytest import timing
213            def setup_module():
214                timing.sleep(1)
215            def teardown_module():
216                timing.sleep(2)
217            def test_sleep():
218                timing.sleep(4)
219        """
220        )
221        result, dom = run_and_parse()
222        node = dom.find_first_by_tag("testsuite")
223        tnode = node.find_first_by_tag("testcase")
224        val = tnode["time"]
225        assert float(val) == 7.0
226
227    @pytest.mark.parametrize("duration_report", ["call", "total"])
228    def test_junit_duration_report(
229        self, testdir, monkeypatch, duration_report, run_and_parse
230    ):
231
232        # mock LogXML.node_reporter so it always sets a known duration to each test report object
233        original_node_reporter = LogXML.node_reporter
234
235        def node_reporter_wrapper(s, report):
236            report.duration = 1.0
237            reporter = original_node_reporter(s, report)
238            return reporter
239
240        monkeypatch.setattr(LogXML, "node_reporter", node_reporter_wrapper)
241
242        testdir.makepyfile(
243            """
244            def test_foo():
245                pass
246        """
247        )
248        result, dom = run_and_parse(
249            "-o", "junit_duration_report={}".format(duration_report)
250        )
251        node = dom.find_first_by_tag("testsuite")
252        tnode = node.find_first_by_tag("testcase")
253        val = float(tnode["time"])
254        if duration_report == "total":
255            assert val == 3.0
256        else:
257            assert duration_report == "call"
258            assert val == 1.0
259
260    @parametrize_families
261    def test_setup_error(self, testdir, run_and_parse, xunit_family):
262        testdir.makepyfile(
263            """
264            import pytest
265
266            @pytest.fixture
267            def arg(request):
268                raise ValueError("Error reason")
269            def test_function(arg):
270                pass
271        """
272        )
273        result, dom = run_and_parse(family=xunit_family)
274        assert result.ret
275        node = dom.find_first_by_tag("testsuite")
276        node.assert_attr(errors=1, tests=1)
277        tnode = node.find_first_by_tag("testcase")
278        tnode.assert_attr(classname="test_setup_error", name="test_function")
279        fnode = tnode.find_first_by_tag("error")
280        fnode.assert_attr(message='failed on setup with "ValueError: Error reason"')
281        assert "ValueError" in fnode.toxml()
282
283    @parametrize_families
284    def test_teardown_error(self, testdir, run_and_parse, xunit_family):
285        testdir.makepyfile(
286            """
287            import pytest
288
289            @pytest.fixture
290            def arg():
291                yield
292                raise ValueError('Error reason')
293            def test_function(arg):
294                pass
295        """
296        )
297        result, dom = run_and_parse(family=xunit_family)
298        assert result.ret
299        node = dom.find_first_by_tag("testsuite")
300        tnode = node.find_first_by_tag("testcase")
301        tnode.assert_attr(classname="test_teardown_error", name="test_function")
302        fnode = tnode.find_first_by_tag("error")
303        fnode.assert_attr(message='failed on teardown with "ValueError: Error reason"')
304        assert "ValueError" in fnode.toxml()
305
306    @parametrize_families
307    def test_call_failure_teardown_error(self, testdir, run_and_parse, xunit_family):
308        testdir.makepyfile(
309            """
310            import pytest
311
312            @pytest.fixture
313            def arg():
314                yield
315                raise Exception("Teardown Exception")
316            def test_function(arg):
317                raise Exception("Call Exception")
318        """
319        )
320        result, dom = run_and_parse(family=xunit_family)
321        assert result.ret
322        node = dom.find_first_by_tag("testsuite")
323        node.assert_attr(errors=1, failures=1, tests=1)
324        first, second = dom.find_by_tag("testcase")
325        assert first
326        assert second
327        assert first != second
328        fnode = first.find_first_by_tag("failure")
329        fnode.assert_attr(message="Exception: Call Exception")
330        snode = second.find_first_by_tag("error")
331        snode.assert_attr(
332            message='failed on teardown with "Exception: Teardown Exception"'
333        )
334
335    @parametrize_families
336    def test_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family):
337        testdir.makepyfile(
338            """
339            import pytest
340            def test_skip():
341                pytest.skip("hello23")
342        """
343        )
344        result, dom = run_and_parse(family=xunit_family)
345        assert result.ret == 0
346        node = dom.find_first_by_tag("testsuite")
347        node.assert_attr(skipped=1)
348        tnode = node.find_first_by_tag("testcase")
349        tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip")
350        snode = tnode.find_first_by_tag("skipped")
351        snode.assert_attr(type="pytest.skip", message="hello23")
352
353    @parametrize_families
354    def test_mark_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family):
355        testdir.makepyfile(
356            """
357            import pytest
358            @pytest.mark.skip(reason="hello24")
359            def test_skip():
360                assert True
361        """
362        )
363        result, dom = run_and_parse(family=xunit_family)
364        assert result.ret == 0
365        node = dom.find_first_by_tag("testsuite")
366        node.assert_attr(skipped=1)
367        tnode = node.find_first_by_tag("testcase")
368        tnode.assert_attr(
369            classname="test_mark_skip_contains_name_reason", name="test_skip"
370        )
371        snode = tnode.find_first_by_tag("skipped")
372        snode.assert_attr(type="pytest.skip", message="hello24")
373
374    @parametrize_families
375    def test_mark_skipif_contains_name_reason(
376        self, testdir, run_and_parse, xunit_family
377    ):
378        testdir.makepyfile(
379            """
380            import pytest
381            GLOBAL_CONDITION = True
382            @pytest.mark.skipif(GLOBAL_CONDITION, reason="hello25")
383            def test_skip():
384                assert True
385        """
386        )
387        result, dom = run_and_parse(family=xunit_family)
388        assert result.ret == 0
389        node = dom.find_first_by_tag("testsuite")
390        node.assert_attr(skipped=1)
391        tnode = node.find_first_by_tag("testcase")
392        tnode.assert_attr(
393            classname="test_mark_skipif_contains_name_reason", name="test_skip"
394        )
395        snode = tnode.find_first_by_tag("skipped")
396        snode.assert_attr(type="pytest.skip", message="hello25")
397
398    @parametrize_families
399    def test_mark_skip_doesnt_capture_output(
400        self, testdir, run_and_parse, xunit_family
401    ):
402        testdir.makepyfile(
403            """
404            import pytest
405            @pytest.mark.skip(reason="foo")
406            def test_skip():
407                print("bar!")
408        """
409        )
410        result, dom = run_and_parse(family=xunit_family)
411        assert result.ret == 0
412        node_xml = dom.find_first_by_tag("testsuite").toxml()
413        assert "bar!" not in node_xml
414
415    @parametrize_families
416    def test_classname_instance(self, testdir, run_and_parse, xunit_family):
417        testdir.makepyfile(
418            """
419            class TestClass(object):
420                def test_method(self):
421                    assert 0
422        """
423        )
424        result, dom = run_and_parse(family=xunit_family)
425        assert result.ret
426        node = dom.find_first_by_tag("testsuite")
427        node.assert_attr(failures=1)
428        tnode = node.find_first_by_tag("testcase")
429        tnode.assert_attr(
430            classname="test_classname_instance.TestClass", name="test_method"
431        )
432
433    @parametrize_families
434    def test_classname_nested_dir(self, testdir, run_and_parse, xunit_family):
435        p = testdir.tmpdir.ensure("sub", "test_hello.py")
436        p.write("def test_func(): 0/0")
437        result, dom = run_and_parse(family=xunit_family)
438        assert result.ret
439        node = dom.find_first_by_tag("testsuite")
440        node.assert_attr(failures=1)
441        tnode = node.find_first_by_tag("testcase")
442        tnode.assert_attr(classname="sub.test_hello", name="test_func")
443
444    @parametrize_families
445    def test_internal_error(self, testdir, run_and_parse, xunit_family):
446        testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0")
447        testdir.makepyfile("def test_function(): pass")
448        result, dom = run_and_parse(family=xunit_family)
449        assert result.ret
450        node = dom.find_first_by_tag("testsuite")
451        node.assert_attr(errors=1, tests=1)
452        tnode = node.find_first_by_tag("testcase")
453        tnode.assert_attr(classname="pytest", name="internal")
454        fnode = tnode.find_first_by_tag("error")
455        fnode.assert_attr(message="internal error")
456        assert "Division" in fnode.toxml()
457
458    @pytest.mark.parametrize(
459        "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"]
460    )
461    @parametrize_families
462    def test_failure_function(
463        self, testdir, junit_logging, run_and_parse, xunit_family
464    ):
465        testdir.makepyfile(
466            """
467            import logging
468            import sys
469
470            def test_fail():
471                print("hello-stdout")
472                sys.stderr.write("hello-stderr\\n")
473                logging.info('info msg')
474                logging.warning('warning msg')
475                raise ValueError(42)
476        """
477        )
478
479        result, dom = run_and_parse(
480            "-o", "junit_logging=%s" % junit_logging, family=xunit_family
481        )
482        assert result.ret, "Expected ret > 0"
483        node = dom.find_first_by_tag("testsuite")
484        node.assert_attr(failures=1, tests=1)
485        tnode = node.find_first_by_tag("testcase")
486        tnode.assert_attr(classname="test_failure_function", name="test_fail")
487        fnode = tnode.find_first_by_tag("failure")
488        fnode.assert_attr(message="ValueError: 42")
489        assert "ValueError" in fnode.toxml(), "ValueError not included"
490
491        if junit_logging in ["log", "all"]:
492            logdata = tnode.find_first_by_tag("system-out")
493            log_xml = logdata.toxml()
494            assert logdata.tag == "system-out", "Expected tag: system-out"
495            assert "info msg" not in log_xml, "Unexpected INFO message"
496            assert "warning msg" in log_xml, "Missing WARN message"
497        if junit_logging in ["system-out", "out-err", "all"]:
498            systemout = tnode.find_first_by_tag("system-out")
499            systemout_xml = systemout.toxml()
500            assert systemout.tag == "system-out", "Expected tag: system-out"
501            assert "info msg" not in systemout_xml, "INFO message found in system-out"
502            assert (
503                "hello-stdout" in systemout_xml
504            ), "Missing 'hello-stdout' in system-out"
505        if junit_logging in ["system-err", "out-err", "all"]:
506            systemerr = tnode.find_first_by_tag("system-err")
507            systemerr_xml = systemerr.toxml()
508            assert systemerr.tag == "system-err", "Expected tag: system-err"
509            assert "info msg" not in systemerr_xml, "INFO message found in system-err"
510            assert (
511                "hello-stderr" in systemerr_xml
512            ), "Missing 'hello-stderr' in system-err"
513            assert (
514                "warning msg" not in systemerr_xml
515            ), "WARN message found in system-err"
516        if junit_logging == "no":
517            assert not tnode.find_by_tag("log"), "Found unexpected content: log"
518            assert not tnode.find_by_tag(
519                "system-out"
520            ), "Found unexpected content: system-out"
521            assert not tnode.find_by_tag(
522                "system-err"
523            ), "Found unexpected content: system-err"
524
525    @parametrize_families
526    def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family):
527        testdir.makepyfile(
528            """
529            import sys
530            def test_fail():
531                assert 0, "An error"
532        """
533        )
534        result, dom = run_and_parse(family=xunit_family)
535        node = dom.find_first_by_tag("testsuite")
536        tnode = node.find_first_by_tag("testcase")
537        fnode = tnode.find_first_by_tag("failure")
538        fnode.assert_attr(message="AssertionError: An error\nassert 0")
539
540    @parametrize_families
541    def test_failure_escape(self, testdir, run_and_parse, xunit_family):
542        testdir.makepyfile(
543            """
544            import pytest
545            @pytest.mark.parametrize('arg1', "<&'", ids="<&'")
546            def test_func(arg1):
547                print(arg1)
548                assert 0
549        """
550        )
551        result, dom = run_and_parse(
552            "-o", "junit_logging=system-out", family=xunit_family
553        )
554        assert result.ret
555        node = dom.find_first_by_tag("testsuite")
556        node.assert_attr(failures=3, tests=3)
557
558        for index, char in enumerate("<&'"):
559
560            tnode = node.find_nth_by_tag("testcase", index)
561            tnode.assert_attr(
562                classname="test_failure_escape", name="test_func[%s]" % char
563            )
564            sysout = tnode.find_first_by_tag("system-out")
565            text = sysout.text
566            assert "%s\n" % char in text
567
568    @parametrize_families
569    def test_junit_prefixing(self, testdir, run_and_parse, xunit_family):
570        testdir.makepyfile(
571            """
572            def test_func():
573                assert 0
574            class TestHello(object):
575                def test_hello(self):
576                    pass
577        """
578        )
579        result, dom = run_and_parse("--junitprefix=xyz", family=xunit_family)
580        assert result.ret
581        node = dom.find_first_by_tag("testsuite")
582        node.assert_attr(failures=1, tests=2)
583        tnode = node.find_first_by_tag("testcase")
584        tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func")
585        tnode = node.find_nth_by_tag("testcase", 1)
586        tnode.assert_attr(
587            classname="xyz.test_junit_prefixing.TestHello", name="test_hello"
588        )
589
590    @parametrize_families
591    def test_xfailure_function(self, testdir, run_and_parse, xunit_family):
592        testdir.makepyfile(
593            """
594            import pytest
595            def test_xfail():
596                pytest.xfail("42")
597        """
598        )
599        result, dom = run_and_parse(family=xunit_family)
600        assert not result.ret
601        node = dom.find_first_by_tag("testsuite")
602        node.assert_attr(skipped=1, tests=1)
603        tnode = node.find_first_by_tag("testcase")
604        tnode.assert_attr(classname="test_xfailure_function", name="test_xfail")
605        fnode = tnode.find_first_by_tag("skipped")
606        fnode.assert_attr(type="pytest.xfail", message="42")
607
608    @parametrize_families
609    def test_xfailure_marker(self, testdir, run_and_parse, xunit_family):
610        testdir.makepyfile(
611            """
612            import pytest
613            @pytest.mark.xfail(reason="42")
614            def test_xfail():
615                assert False
616        """
617        )
618        result, dom = run_and_parse(family=xunit_family)
619        assert not result.ret
620        node = dom.find_first_by_tag("testsuite")
621        node.assert_attr(skipped=1, tests=1)
622        tnode = node.find_first_by_tag("testcase")
623        tnode.assert_attr(classname="test_xfailure_marker", name="test_xfail")
624        fnode = tnode.find_first_by_tag("skipped")
625        fnode.assert_attr(type="pytest.xfail", message="42")
626
627    @pytest.mark.parametrize(
628        "junit_logging", ["no", "log", "system-out", "system-err", "out-err", "all"]
629    )
630    def test_xfail_captures_output_once(self, testdir, junit_logging, run_and_parse):
631        testdir.makepyfile(
632            """
633            import sys
634            import pytest
635
636            @pytest.mark.xfail()
637            def test_fail():
638                sys.stdout.write('XFAIL This is stdout')
639                sys.stderr.write('XFAIL This is stderr')
640                assert 0
641        """
642        )
643        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
644        node = dom.find_first_by_tag("testsuite")
645        tnode = node.find_first_by_tag("testcase")
646        if junit_logging in ["system-err", "out-err", "all"]:
647            assert len(tnode.find_by_tag("system-err")) == 1
648        else:
649            assert len(tnode.find_by_tag("system-err")) == 0
650
651        if junit_logging in ["log", "system-out", "out-err", "all"]:
652            assert len(tnode.find_by_tag("system-out")) == 1
653        else:
654            assert len(tnode.find_by_tag("system-out")) == 0
655
656    @parametrize_families
657    def test_xfailure_xpass(self, testdir, run_and_parse, xunit_family):
658        testdir.makepyfile(
659            """
660            import pytest
661            @pytest.mark.xfail
662            def test_xpass():
663                pass
664        """
665        )
666        result, dom = run_and_parse(family=xunit_family)
667        # assert result.ret
668        node = dom.find_first_by_tag("testsuite")
669        node.assert_attr(skipped=0, tests=1)
670        tnode = node.find_first_by_tag("testcase")
671        tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass")
672
673    @parametrize_families
674    def test_xfailure_xpass_strict(self, testdir, run_and_parse, xunit_family):
675        testdir.makepyfile(
676            """
677            import pytest
678            @pytest.mark.xfail(strict=True, reason="This needs to fail!")
679            def test_xpass():
680                pass
681        """
682        )
683        result, dom = run_and_parse(family=xunit_family)
684        # assert result.ret
685        node = dom.find_first_by_tag("testsuite")
686        node.assert_attr(skipped=0, tests=1)
687        tnode = node.find_first_by_tag("testcase")
688        tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass")
689        fnode = tnode.find_first_by_tag("failure")
690        fnode.assert_attr(message="[XPASS(strict)] This needs to fail!")
691
692    @parametrize_families
693    def test_collect_error(self, testdir, run_and_parse, xunit_family):
694        testdir.makepyfile("syntax error")
695        result, dom = run_and_parse(family=xunit_family)
696        assert result.ret
697        node = dom.find_first_by_tag("testsuite")
698        node.assert_attr(errors=1, tests=1)
699        tnode = node.find_first_by_tag("testcase")
700        fnode = tnode.find_first_by_tag("error")
701        fnode.assert_attr(message="collection failure")
702        assert "SyntaxError" in fnode.toxml()
703
704    def test_unicode(self, testdir, run_and_parse):
705        value = "hx\xc4\x85\xc4\x87\n"
706        testdir.makepyfile(
707            """\
708            # coding: latin1
709            def test_hello():
710                print(%r)
711                assert 0
712            """
713            % value
714        )
715        result, dom = run_and_parse()
716        assert result.ret == 1
717        tnode = dom.find_first_by_tag("testcase")
718        fnode = tnode.find_first_by_tag("failure")
719        assert "hx" in fnode.toxml()
720
721    def test_assertion_binchars(self, testdir, run_and_parse):
722        """This test did fail when the escaping wasn't strict."""
723        testdir.makepyfile(
724            """
725
726            M1 = '\x01\x02\x03\x04'
727            M2 = '\x01\x02\x03\x05'
728
729            def test_str_compare():
730                assert M1 == M2
731            """
732        )
733        result, dom = run_and_parse()
734        print(dom.toxml())
735
736    @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
737    def test_pass_captures_stdout(self, testdir, run_and_parse, junit_logging):
738        testdir.makepyfile(
739            """
740            def test_pass():
741                print('hello-stdout')
742        """
743        )
744        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
745        node = dom.find_first_by_tag("testsuite")
746        pnode = node.find_first_by_tag("testcase")
747        if junit_logging == "no":
748            assert not node.find_by_tag(
749                "system-out"
750            ), "system-out should not be generated"
751        if junit_logging == "system-out":
752            systemout = pnode.find_first_by_tag("system-out")
753            assert (
754                "hello-stdout" in systemout.toxml()
755            ), "'hello-stdout' should be in system-out"
756
757    @pytest.mark.parametrize("junit_logging", ["no", "system-err"])
758    def test_pass_captures_stderr(self, testdir, run_and_parse, junit_logging):
759        testdir.makepyfile(
760            """
761            import sys
762            def test_pass():
763                sys.stderr.write('hello-stderr')
764        """
765        )
766        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
767        node = dom.find_first_by_tag("testsuite")
768        pnode = node.find_first_by_tag("testcase")
769        if junit_logging == "no":
770            assert not node.find_by_tag(
771                "system-err"
772            ), "system-err should not be generated"
773        if junit_logging == "system-err":
774            systemerr = pnode.find_first_by_tag("system-err")
775            assert (
776                "hello-stderr" in systemerr.toxml()
777            ), "'hello-stderr' should be in system-err"
778
779    @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
780    def test_setup_error_captures_stdout(self, testdir, run_and_parse, junit_logging):
781        testdir.makepyfile(
782            """
783            import pytest
784
785            @pytest.fixture
786            def arg(request):
787                print('hello-stdout')
788                raise ValueError()
789            def test_function(arg):
790                pass
791        """
792        )
793        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
794        node = dom.find_first_by_tag("testsuite")
795        pnode = node.find_first_by_tag("testcase")
796        if junit_logging == "no":
797            assert not node.find_by_tag(
798                "system-out"
799            ), "system-out should not be generated"
800        if junit_logging == "system-out":
801            systemout = pnode.find_first_by_tag("system-out")
802            assert (
803                "hello-stdout" in systemout.toxml()
804            ), "'hello-stdout' should be in system-out"
805
806    @pytest.mark.parametrize("junit_logging", ["no", "system-err"])
807    def test_setup_error_captures_stderr(self, testdir, run_and_parse, junit_logging):
808        testdir.makepyfile(
809            """
810            import sys
811            import pytest
812
813            @pytest.fixture
814            def arg(request):
815                sys.stderr.write('hello-stderr')
816                raise ValueError()
817            def test_function(arg):
818                pass
819        """
820        )
821        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
822        node = dom.find_first_by_tag("testsuite")
823        pnode = node.find_first_by_tag("testcase")
824        if junit_logging == "no":
825            assert not node.find_by_tag(
826                "system-err"
827            ), "system-err should not be generated"
828        if junit_logging == "system-err":
829            systemerr = pnode.find_first_by_tag("system-err")
830            assert (
831                "hello-stderr" in systemerr.toxml()
832            ), "'hello-stderr' should be in system-err"
833
834    @pytest.mark.parametrize("junit_logging", ["no", "system-out"])
835    def test_avoid_double_stdout(self, testdir, run_and_parse, junit_logging):
836        testdir.makepyfile(
837            """
838            import sys
839            import pytest
840
841            @pytest.fixture
842            def arg(request):
843                yield
844                sys.stdout.write('hello-stdout teardown')
845                raise ValueError()
846            def test_function(arg):
847                sys.stdout.write('hello-stdout call')
848        """
849        )
850        result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging)
851        node = dom.find_first_by_tag("testsuite")
852        pnode = node.find_first_by_tag("testcase")
853        if junit_logging == "no":
854            assert not node.find_by_tag(
855                "system-out"
856            ), "system-out should not be generated"
857        if junit_logging == "system-out":
858            systemout = pnode.find_first_by_tag("system-out")
859            assert "hello-stdout call" in systemout.toxml()
860            assert "hello-stdout teardown" in systemout.toxml()
861
862
863def test_mangle_test_address():
864    from _pytest.junitxml import mangle_test_address
865
866    address = "::".join(["a/my.py.thing.py", "Class", "()", "method", "[a-1-::]"])
867    newnames = mangle_test_address(address)
868    assert newnames == ["a.my.py.thing", "Class", "method", "[a-1-::]"]
869
870
871def test_dont_configure_on_workers(tmpdir) -> None:
872    gotten = []  # type: List[object]
873
874    class FakeConfig:
875        if TYPE_CHECKING:
876            workerinput = None
877
878        def __init__(self):
879            self.pluginmanager = self
880            self.option = self
881            self._store = Store()
882
883        def getini(self, name):
884            return "pytest"
885
886        junitprefix = None
887        # XXX: shouldn't need tmpdir ?
888        xmlpath = str(tmpdir.join("junix.xml"))
889        register = gotten.append
890
891    fake_config = cast(Config, FakeConfig())
892    from _pytest import junitxml
893
894    junitxml.pytest_configure(fake_config)
895    assert len(gotten) == 1
896    FakeConfig.workerinput = None
897    junitxml.pytest_configure(fake_config)
898    assert len(gotten) == 1
899
900
901class TestNonPython:
902    @parametrize_families
903    def test_summing_simple(self, testdir, run_and_parse, xunit_family):
904        testdir.makeconftest(
905            """
906            import pytest
907            def pytest_collect_file(path, parent):
908                if path.ext == ".xyz":
909                    return MyItem.from_parent(name=path.basename, parent=parent)
910            class MyItem(pytest.Item):
911                def runtest(self):
912                    raise ValueError(42)
913                def repr_failure(self, excinfo):
914                    return "custom item runtest failed"
915        """
916        )
917        testdir.tmpdir.join("myfile.xyz").write("hello")
918        result, dom = run_and_parse(family=xunit_family)
919        assert result.ret
920        node = dom.find_first_by_tag("testsuite")
921        node.assert_attr(errors=0, failures=1, skipped=0, tests=1)
922        tnode = node.find_first_by_tag("testcase")
923        tnode.assert_attr(name="myfile.xyz")
924        fnode = tnode.find_first_by_tag("failure")
925        fnode.assert_attr(message="custom item runtest failed")
926        assert "custom item runtest failed" in fnode.toxml()
927
928
929@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
930def test_nullbyte(testdir, junit_logging):
931    # A null byte can not occur in XML (see section 2.2 of the spec)
932    testdir.makepyfile(
933        """
934        import sys
935        def test_print_nullbyte():
936            sys.stdout.write('Here the null -->' + chr(0) + '<--')
937            sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--')
938            assert False
939    """
940    )
941    xmlf = testdir.tmpdir.join("junit.xml")
942    testdir.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
943    text = xmlf.read()
944    assert "\x00" not in text
945    if junit_logging == "system-out":
946        assert "#x00" in text
947    if junit_logging == "no":
948        assert "#x00" not in text
949
950
951@pytest.mark.parametrize("junit_logging", ["no", "system-out"])
952def test_nullbyte_replace(testdir, junit_logging):
953    # Check if the null byte gets replaced
954    testdir.makepyfile(
955        """
956        import sys
957        def test_print_nullbyte():
958            sys.stdout.write('Here the null -->' + chr(0) + '<--')
959            sys.stdout.write('In repr form -->' + repr(chr(0)) + '<--')
960            assert False
961    """
962    )
963    xmlf = testdir.tmpdir.join("junit.xml")
964    testdir.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging)
965    text = xmlf.read()
966    if junit_logging == "system-out":
967        assert "#x0" in text
968    if junit_logging == "no":
969        assert "#x0" not in text
970
971
972def test_invalid_xml_escape():
973    # Test some more invalid xml chars, the full range should be
974    # tested really but let's just test the edges of the ranges
975    # instead.
976    # XXX This only tests low unicode character points for now as
977    #     there are some issues with the testing infrastructure for
978    #     the higher ones.
979    # XXX Testing 0xD (\r) is tricky as it overwrites the just written
980    #     line in the output, so we skip it too.
981    invalid = (
982        0x00,
983        0x1,
984        0xB,
985        0xC,
986        0xE,
987        0x19,
988        27,  # issue #126
989        0xD800,
990        0xDFFF,
991        0xFFFE,
992        0x0FFFF,
993    )  # , 0x110000)
994    valid = (0x9, 0xA, 0x20)
995    # 0xD, 0xD7FF, 0xE000, 0xFFFD, 0x10000, 0x10FFFF)
996
997    for i in invalid:
998        got = bin_xml_escape(chr(i))
999        if i <= 0xFF:
1000            expected = "#x%02X" % i
1001        else:
1002            expected = "#x%04X" % i
1003        assert got == expected
1004    for i in valid:
1005        assert chr(i) == bin_xml_escape(chr(i))
1006
1007
1008def test_logxml_path_expansion(tmpdir, monkeypatch):
1009    home_tilde = py.path.local(os.path.expanduser("~")).join("test.xml")
1010    xml_tilde = LogXML("~%stest.xml" % tmpdir.sep, None)
1011    assert xml_tilde.logfile == home_tilde
1012
1013    monkeypatch.setenv("HOME", str(tmpdir))
1014    home_var = os.path.normpath(os.path.expandvars("$HOME/test.xml"))
1015    xml_var = LogXML("$HOME%stest.xml" % tmpdir.sep, None)
1016    assert xml_var.logfile == home_var
1017
1018
1019def test_logxml_changingdir(testdir):
1020    testdir.makepyfile(
1021        """
1022        def test_func():
1023            import os
1024            os.chdir("a")
1025    """
1026    )
1027    testdir.tmpdir.mkdir("a")
1028    result = testdir.runpytest("--junitxml=a/x.xml")
1029    assert result.ret == 0
1030    assert testdir.tmpdir.join("a/x.xml").check()
1031
1032
1033def test_logxml_makedir(testdir):
1034    """--junitxml should automatically create directories for the xml file"""
1035    testdir.makepyfile(
1036        """
1037        def test_pass():
1038            pass
1039    """
1040    )
1041    result = testdir.runpytest("--junitxml=path/to/results.xml")
1042    assert result.ret == 0
1043    assert testdir.tmpdir.join("path/to/results.xml").check()
1044
1045
1046def test_logxml_check_isdir(testdir):
1047    """Give an error if --junit-xml is a directory (#2089)"""
1048    result = testdir.runpytest("--junit-xml=.")
1049    result.stderr.fnmatch_lines(["*--junitxml must be a filename*"])
1050
1051
1052def test_escaped_parametrized_names_xml(testdir, run_and_parse):
1053    testdir.makepyfile(
1054        """\
1055        import pytest
1056        @pytest.mark.parametrize('char', ["\\x00"])
1057        def test_func(char):
1058            assert char
1059        """
1060    )
1061    result, dom = run_and_parse()
1062    assert result.ret == 0
1063    node = dom.find_first_by_tag("testcase")
1064    node.assert_attr(name="test_func[\\x00]")
1065
1066
1067def test_double_colon_split_function_issue469(testdir, run_and_parse):
1068    testdir.makepyfile(
1069        """
1070        import pytest
1071        @pytest.mark.parametrize('param', ["double::colon"])
1072        def test_func(param):
1073            pass
1074    """
1075    )
1076    result, dom = run_and_parse()
1077    assert result.ret == 0
1078    node = dom.find_first_by_tag("testcase")
1079    node.assert_attr(classname="test_double_colon_split_function_issue469")
1080    node.assert_attr(name="test_func[double::colon]")
1081
1082
1083def test_double_colon_split_method_issue469(testdir, run_and_parse):
1084    testdir.makepyfile(
1085        """
1086        import pytest
1087        class TestClass(object):
1088            @pytest.mark.parametrize('param', ["double::colon"])
1089            def test_func(self, param):
1090                pass
1091    """
1092    )
1093    result, dom = run_and_parse()
1094    assert result.ret == 0
1095    node = dom.find_first_by_tag("testcase")
1096    node.assert_attr(classname="test_double_colon_split_method_issue469.TestClass")
1097    node.assert_attr(name="test_func[double::colon]")
1098
1099
1100def test_unicode_issue368(testdir) -> None:
1101    path = testdir.tmpdir.join("test.xml")
1102    log = LogXML(str(path), None)
1103    ustr = "ВНИ!"
1104
1105    class Report(BaseReport):
1106        longrepr = ustr
1107        sections = []  # type: List[Tuple[str, str]]
1108        nodeid = "something"
1109        location = "tests/filename.py", 42, "TestClass.method"
1110
1111    test_report = cast(TestReport, Report())
1112
1113    # hopefully this is not too brittle ...
1114    log.pytest_sessionstart()
1115    node_reporter = log._opentestcase(test_report)
1116    node_reporter.append_failure(test_report)
1117    node_reporter.append_collect_error(test_report)
1118    node_reporter.append_collect_skipped(test_report)
1119    node_reporter.append_error(test_report)
1120    test_report.longrepr = "filename", 1, ustr
1121    node_reporter.append_skipped(test_report)
1122    test_report.longrepr = "filename", 1, "Skipped: 卡嘣嘣"
1123    node_reporter.append_skipped(test_report)
1124    test_report.wasxfail = ustr  # type: ignore[attr-defined]
1125    node_reporter.append_skipped(test_report)
1126    log.pytest_sessionfinish()
1127
1128
1129def test_record_property(testdir, run_and_parse):
1130    testdir.makepyfile(
1131        """
1132        import pytest
1133
1134        @pytest.fixture
1135        def other(record_property):
1136            record_property("bar", 1)
1137        def test_record(record_property, other):
1138            record_property("foo", "<1");
1139    """
1140    )
1141    result, dom = run_and_parse()
1142    node = dom.find_first_by_tag("testsuite")
1143    tnode = node.find_first_by_tag("testcase")
1144    psnode = tnode.find_first_by_tag("properties")
1145    pnodes = psnode.find_by_tag("property")
1146    pnodes[0].assert_attr(name="bar", value="1")
1147    pnodes[1].assert_attr(name="foo", value="<1")
1148    result.stdout.fnmatch_lines(["*= 1 passed in *"])
1149
1150
1151def test_record_property_same_name(testdir, run_and_parse):
1152    testdir.makepyfile(
1153        """
1154        def test_record_with_same_name(record_property):
1155            record_property("foo", "bar")
1156            record_property("foo", "baz")
1157    """
1158    )
1159    result, dom = run_and_parse()
1160    node = dom.find_first_by_tag("testsuite")
1161    tnode = node.find_first_by_tag("testcase")
1162    psnode = tnode.find_first_by_tag("properties")
1163    pnodes = psnode.find_by_tag("property")
1164    pnodes[0].assert_attr(name="foo", value="bar")
1165    pnodes[1].assert_attr(name="foo", value="baz")
1166
1167
1168@pytest.mark.parametrize("fixture_name", ["record_property", "record_xml_attribute"])
1169def test_record_fixtures_without_junitxml(testdir, fixture_name):
1170    testdir.makepyfile(
1171        """
1172        def test_record({fixture_name}):
1173            {fixture_name}("foo", "bar")
1174    """.format(
1175            fixture_name=fixture_name
1176        )
1177    )
1178    result = testdir.runpytest()
1179    assert result.ret == 0
1180
1181
1182@pytest.mark.filterwarnings("default")
1183def test_record_attribute(testdir, run_and_parse):
1184    testdir.makeini(
1185        """
1186        [pytest]
1187        junit_family = xunit1
1188    """
1189    )
1190    testdir.makepyfile(
1191        """
1192        import pytest
1193
1194        @pytest.fixture
1195        def other(record_xml_attribute):
1196            record_xml_attribute("bar", 1)
1197        def test_record(record_xml_attribute, other):
1198            record_xml_attribute("foo", "<1");
1199    """
1200    )
1201    result, dom = run_and_parse()
1202    node = dom.find_first_by_tag("testsuite")
1203    tnode = node.find_first_by_tag("testcase")
1204    tnode.assert_attr(bar="1")
1205    tnode.assert_attr(foo="<1")
1206    result.stdout.fnmatch_lines(
1207        ["*test_record_attribute.py:6:*record_xml_attribute is an experimental feature"]
1208    )
1209
1210
1211@pytest.mark.filterwarnings("default")
1212@pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"])
1213def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse):
1214    """Ensure record_xml_attribute and record_property drop values when outside of legacy family."""
1215    testdir.makeini(
1216        """
1217        [pytest]
1218        junit_family = xunit2
1219    """
1220    )
1221    testdir.makepyfile(
1222        """
1223        import pytest
1224
1225        @pytest.fixture
1226        def other({fixture_name}):
1227            {fixture_name}("bar", 1)
1228        def test_record({fixture_name}, other):
1229            {fixture_name}("foo", "<1");
1230    """.format(
1231            fixture_name=fixture_name
1232        )
1233    )
1234
1235    result, dom = run_and_parse(family=None)
1236    expected_lines = []
1237    if fixture_name == "record_xml_attribute":
1238        expected_lines.append(
1239            "*test_record_fixtures_xunit2.py:6:*record_xml_attribute is an experimental feature"
1240        )
1241    expected_lines = [
1242        "*test_record_fixtures_xunit2.py:6:*{fixture_name} is incompatible "
1243        "with junit_family 'xunit2' (use 'legacy' or 'xunit1')".format(
1244            fixture_name=fixture_name
1245        )
1246    ]
1247    result.stdout.fnmatch_lines(expected_lines)
1248
1249
1250def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse):
1251    """`xdist` calls pytest_runtest_logreport as they are executed by the workers,
1252    with nodes from several nodes overlapping, so junitxml must cope with that
1253    to produce correct reports (#1064)."""
1254    pytest.importorskip("xdist")
1255    monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False)
1256    testdir.makepyfile(
1257        """
1258        import pytest, time
1259        @pytest.mark.parametrize('i', list(range(30)))
1260        def test_x(i):
1261            assert i != 22
1262    """
1263    )
1264    _, dom = run_and_parse("-n2")
1265    suite_node = dom.find_first_by_tag("testsuite")
1266    failed = []
1267    for case_node in suite_node.find_by_tag("testcase"):
1268        if case_node.find_first_by_tag("failure"):
1269            failed.append(case_node["name"])
1270
1271    assert failed == ["test_x[22]"]
1272
1273
1274@parametrize_families
1275def test_root_testsuites_tag(testdir, run_and_parse, xunit_family):
1276    testdir.makepyfile(
1277        """
1278        def test_x():
1279            pass
1280    """
1281    )
1282    _, dom = run_and_parse(family=xunit_family)
1283    root = dom.get_unique_child
1284    assert root.tag == "testsuites"
1285    suite_node = root.get_unique_child
1286    assert suite_node.tag == "testsuite"
1287
1288
1289def test_runs_twice(testdir, run_and_parse):
1290    f = testdir.makepyfile(
1291        """
1292        def test_pass():
1293            pass
1294    """
1295    )
1296
1297    result, dom = run_and_parse(f, f)
1298    result.stdout.no_fnmatch_line("*INTERNALERROR*")
1299    first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
1300    assert first == second
1301
1302
1303def test_runs_twice_xdist(testdir, run_and_parse):
1304    pytest.importorskip("xdist")
1305    testdir.monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
1306    f = testdir.makepyfile(
1307        """
1308        def test_pass():
1309            pass
1310    """
1311    )
1312
1313    result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen")
1314    result.stdout.no_fnmatch_line("*INTERNALERROR*")
1315    first, second = [x["classname"] for x in dom.find_by_tag("testcase")]
1316    assert first == second
1317
1318
1319def test_fancy_items_regression(testdir, run_and_parse):
1320    # issue 1259
1321    testdir.makeconftest(
1322        """
1323        import pytest
1324        class FunItem(pytest.Item):
1325            def runtest(self):
1326                pass
1327        class NoFunItem(pytest.Item):
1328            def runtest(self):
1329                pass
1330
1331        class FunCollector(pytest.File):
1332            def collect(self):
1333                return [
1334                    FunItem.from_parent(name='a', parent=self),
1335                    NoFunItem.from_parent(name='a', parent=self),
1336                    NoFunItem.from_parent(name='b', parent=self),
1337                ]
1338
1339        def pytest_collect_file(path, parent):
1340            if path.check(ext='.py'):
1341                return FunCollector.from_parent(fspath=path, parent=parent)
1342    """
1343    )
1344
1345    testdir.makepyfile(
1346        """
1347        def test_pass():
1348            pass
1349    """
1350    )
1351
1352    result, dom = run_and_parse()
1353
1354    result.stdout.no_fnmatch_line("*INTERNALERROR*")
1355
1356    items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase"))
1357    import pprint
1358
1359    pprint.pprint(items)
1360    assert items == [
1361        "conftest a",
1362        "conftest a",
1363        "conftest b",
1364        "test_fancy_items_regression a",
1365        "test_fancy_items_regression a",
1366        "test_fancy_items_regression b",
1367        "test_fancy_items_regression test_pass",
1368    ]
1369
1370
1371@parametrize_families
1372def test_global_properties(testdir, xunit_family) -> None:
1373    path = testdir.tmpdir.join("test_global_properties.xml")
1374    log = LogXML(str(path), None, family=xunit_family)
1375
1376    class Report(BaseReport):
1377        sections = []  # type: List[Tuple[str, str]]
1378        nodeid = "test_node_id"
1379
1380    log.pytest_sessionstart()
1381    log.add_global_property("foo", "1")
1382    log.add_global_property("bar", "2")
1383    log.pytest_sessionfinish()
1384
1385    dom = minidom.parse(str(path))
1386
1387    properties = dom.getElementsByTagName("properties")
1388
1389    assert properties.length == 1, "There must be one <properties> node"
1390
1391    property_list = dom.getElementsByTagName("property")
1392
1393    assert property_list.length == 2, "There most be only 2 property nodes"
1394
1395    expected = {"foo": "1", "bar": "2"}
1396    actual = {}
1397
1398    for p in property_list:
1399        k = str(p.getAttribute("name"))
1400        v = str(p.getAttribute("value"))
1401        actual[k] = v
1402
1403    assert actual == expected
1404
1405
1406def test_url_property(testdir) -> None:
1407    test_url = "http://www.github.com/pytest-dev"
1408    path = testdir.tmpdir.join("test_url_property.xml")
1409    log = LogXML(str(path), None)
1410
1411    class Report(BaseReport):
1412        longrepr = "FooBarBaz"
1413        sections = []  # type: List[Tuple[str, str]]
1414        nodeid = "something"
1415        location = "tests/filename.py", 42, "TestClass.method"
1416        url = test_url
1417
1418    test_report = cast(TestReport, Report())
1419
1420    log.pytest_sessionstart()
1421    node_reporter = log._opentestcase(test_report)
1422    node_reporter.append_failure(test_report)
1423    log.pytest_sessionfinish()
1424
1425    test_case = minidom.parse(str(path)).getElementsByTagName("testcase")[0]
1426
1427    assert (
1428        test_case.getAttribute("url") == test_url
1429    ), "The URL did not get written to the xml"
1430
1431
1432@parametrize_families
1433def test_record_testsuite_property(testdir, run_and_parse, xunit_family):
1434    testdir.makepyfile(
1435        """
1436        def test_func1(record_testsuite_property):
1437            record_testsuite_property("stats", "all good")
1438
1439        def test_func2(record_testsuite_property):
1440            record_testsuite_property("stats", 10)
1441    """
1442    )
1443    result, dom = run_and_parse(family=xunit_family)
1444    assert result.ret == 0
1445    node = dom.find_first_by_tag("testsuite")
1446    properties_node = node.find_first_by_tag("properties")
1447    p1_node = properties_node.find_nth_by_tag("property", 0)
1448    p2_node = properties_node.find_nth_by_tag("property", 1)
1449    p1_node.assert_attr(name="stats", value="all good")
1450    p2_node.assert_attr(name="stats", value="10")
1451
1452
1453def test_record_testsuite_property_junit_disabled(testdir):
1454    testdir.makepyfile(
1455        """
1456        def test_func1(record_testsuite_property):
1457            record_testsuite_property("stats", "all good")
1458    """
1459    )
1460    result = testdir.runpytest()
1461    assert result.ret == 0
1462
1463
1464@pytest.mark.parametrize("junit", [True, False])
1465def test_record_testsuite_property_type_checking(testdir, junit):
1466    testdir.makepyfile(
1467        """
1468        def test_func1(record_testsuite_property):
1469            record_testsuite_property(1, 2)
1470    """
1471    )
1472    args = ("--junitxml=tests.xml",) if junit else ()
1473    result = testdir.runpytest(*args)
1474    assert result.ret == 1
1475    result.stdout.fnmatch_lines(
1476        ["*TypeError: name parameter needs to be a string, but int given"]
1477    )
1478
1479
1480@pytest.mark.parametrize("suite_name", ["my_suite", ""])
1481@parametrize_families
1482def test_set_suite_name(testdir, suite_name, run_and_parse, xunit_family):
1483    if suite_name:
1484        testdir.makeini(
1485            """
1486            [pytest]
1487            junit_suite_name={suite_name}
1488            junit_family={family}
1489        """.format(
1490                suite_name=suite_name, family=xunit_family
1491            )
1492        )
1493        expected = suite_name
1494    else:
1495        expected = "pytest"
1496    testdir.makepyfile(
1497        """
1498        import pytest
1499
1500        def test_func():
1501            pass
1502    """
1503    )
1504    result, dom = run_and_parse(family=xunit_family)
1505    assert result.ret == 0
1506    node = dom.find_first_by_tag("testsuite")
1507    node.assert_attr(name=expected)
1508
1509
1510def test_escaped_skipreason_issue3533(testdir, run_and_parse):
1511    testdir.makepyfile(
1512        """
1513        import pytest
1514        @pytest.mark.skip(reason='1 <> 2')
1515        def test_skip():
1516            pass
1517    """
1518    )
1519    _, dom = run_and_parse()
1520    node = dom.find_first_by_tag("testcase")
1521    snode = node.find_first_by_tag("skipped")
1522    assert "1 <> 2" in snode.text
1523    snode.assert_attr(message="1 <> 2")
1524
1525
1526@parametrize_families
1527def test_logging_passing_tests_disabled_does_not_log_test_output(
1528    testdir, run_and_parse, xunit_family
1529):
1530    testdir.makeini(
1531        """
1532        [pytest]
1533        junit_log_passing_tests=False
1534        junit_logging=system-out
1535        junit_family={family}
1536    """.format(
1537            family=xunit_family
1538        )
1539    )
1540    testdir.makepyfile(
1541        """
1542        import pytest
1543        import logging
1544        import sys
1545
1546        def test_func():
1547            sys.stdout.write('This is stdout')
1548            sys.stderr.write('This is stderr')
1549            logging.warning('hello')
1550    """
1551    )
1552    result, dom = run_and_parse(family=xunit_family)
1553    assert result.ret == 0
1554    node = dom.find_first_by_tag("testcase")
1555    assert len(node.find_by_tag("system-err")) == 0
1556    assert len(node.find_by_tag("system-out")) == 0
1557
1558
1559@parametrize_families
1560@pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"])
1561def test_logging_passing_tests_disabled_logs_output_for_failing_test_issue5430(
1562    testdir, junit_logging, run_and_parse, xunit_family
1563):
1564    testdir.makeini(
1565        """
1566        [pytest]
1567        junit_log_passing_tests=False
1568        junit_family={family}
1569    """.format(
1570            family=xunit_family
1571        )
1572    )
1573    testdir.makepyfile(
1574        """
1575        import pytest
1576        import logging
1577        import sys
1578
1579        def test_func():
1580            logging.warning('hello')
1581            assert 0
1582    """
1583    )
1584    result, dom = run_and_parse(
1585        "-o", "junit_logging=%s" % junit_logging, family=xunit_family
1586    )
1587    assert result.ret == 1
1588    node = dom.find_first_by_tag("testcase")
1589    if junit_logging == "system-out":
1590        assert len(node.find_by_tag("system-err")) == 0
1591        assert len(node.find_by_tag("system-out")) == 1
1592    elif junit_logging == "system-err":
1593        assert len(node.find_by_tag("system-err")) == 1
1594        assert len(node.find_by_tag("system-out")) == 0
1595    else:
1596        assert junit_logging == "no"
1597        assert len(node.find_by_tag("system-err")) == 0
1598        assert len(node.find_by_tag("system-out")) == 0
1599