From c163a2b957b76e6d4fac0ad9ef2e937fa35c136c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:15:27 +0100 Subject: [PATCH 01/16] chore: setuponly - migrate to f string --- src/_pytest/setuponly.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 1e887a896f5..7e6b46bcdb4 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -73,13 +73,9 @@ def _show_fixture_action( # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc. scope_indent = list(reversed(Scope)).index(fixturedef._scope) tw.write(" " * 2 * scope_indent) - tw.write( - "{step} {scope} {fixture}".format( # noqa: UP032 (Readability) - step=msg.ljust(8), # align the output to TEARDOWN - scope=fixturedef.scope[0].upper(), - fixture=fixturedef.argname, - ) - ) + + scopename = fixturedef.scope[0].upper() + tw.write(f"{msg:<8} {scopename} {fixturedef.argname}") if msg == "SETUP": deps = sorted(arg for arg in fixturedef.argnames if arg != "request") From c48bb32ad537f890587613348aa7cd189b1d16a7 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:16:34 +0100 Subject: [PATCH 02/16] chore: MockTiming - move impl to _pytest.timing --- src/_pytest/timing.py | 36 ++++++++++++++++++++++++++++++++++++ testing/conftest.py | 20 ++------------------ 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index b23c7f69e2d..e2dde161732 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -8,9 +8,45 @@ from __future__ import annotations +import dataclasses +from datetime import datetime from time import perf_counter from time import sleep from time import time +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from pytest import MonkeyPatch + + +@dataclasses.dataclass +class MockTiming: + """Mocks _pytest.timing with a known object that can be used to control timing in tests + deterministically. + + pytest itself should always use functions from `_pytest.timing` instead of `time` directly. + + This then allows us more control over time during testing, if testing code also + uses `_pytest.timing` functions. + + Time is static, and only advances through `sleep` calls, thus tests might sleep over large + numbers and obtain accurate time() calls at the end, making tests reliable and instant.""" + + _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() # noqa: RUF009 + + def sleep(self, seconds: float) -> None: + self._current_time += seconds + + def time(self) -> float: + return self._current_time + + def patch(self, monkeypatch: MonkeyPatch) -> None: + from _pytest import timing # noqa: PLW0406 + + monkeypatch.setattr(timing, "sleep", self.sleep) + monkeypatch.setattr(timing, "time", self.time) + monkeypatch.setattr(timing, "perf_counter", self.time) __all__ = ["perf_counter", "sleep", "time"] diff --git a/testing/conftest.py b/testing/conftest.py index 45a47cbdbaa..5824633bea2 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -233,24 +233,8 @@ def mock_timing(monkeypatch: MonkeyPatch): Time is static, and only advances through `sleep` calls, thus tests might sleep over large numbers and obtain accurate time() calls at the end, making tests reliable and instant. """ - - @dataclasses.dataclass - class MockTiming: - _current_time: float = 1590150050.0 - - def sleep(self, seconds: float) -> None: - self._current_time += seconds - - def time(self) -> float: - return self._current_time - - def patch(self) -> None: - from _pytest import timing - - monkeypatch.setattr(timing, "sleep", self.sleep) - monkeypatch.setattr(timing, "time", self.time) - monkeypatch.setattr(timing, "perf_counter", self.time) + from _pytest.timing import MockTiming result = MockTiming() - result.patch() + result.patch(monkeypatch) return result From 524639efc373c595488e99c2cf3fc8e151185ee0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:18:49 +0100 Subject: [PATCH 03/16] chore: fixture tests: migrate to f-strings --- testing/python/fixtures.py | 42 +++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c8d1eb23838..32453739e8c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2315,14 +2315,14 @@ def test_ordering_dependencies_torndown_first( ) -> None: """#226""" pytester.makepyfile( - """ + f""" import pytest values = [] - @pytest.fixture(%(param1)s) + @pytest.fixture({param1}) def arg1(request): request.addfinalizer(lambda: values.append("fin1")) values.append("new1") - @pytest.fixture(%(param2)s) + @pytest.fixture({param2}) def arg2(request, arg1): request.addfinalizer(lambda: values.append("fin2")) values.append("new2") @@ -2331,8 +2331,7 @@ def test_arg(arg2): pass def test_check(): assert values == ["new1", "new2", "fin2", "fin1"] - """ # noqa: UP031 (python syntax issues) - % locals() + """ ) reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) @@ -3212,21 +3211,21 @@ def test_finalizer_order_on_parametrization( ) -> None: """#246""" pytester.makepyfile( - """ + f""" import pytest values = [] - @pytest.fixture(scope=%(scope)r, params=["1"]) + @pytest.fixture(scope={scope!r}, params=["1"]) def fix1(request): return request.param - @pytest.fixture(scope=%(scope)r) + @pytest.fixture(scope={scope!r}) def fix2(request, base): def cleanup_fix2(): assert not values, "base should not have been finalized" request.addfinalizer(cleanup_fix2) - @pytest.fixture(scope=%(scope)r) + @pytest.fixture(scope={scope!r}) def base(request, fix1): def cleanup_base(): values.append("fin_base") @@ -3239,8 +3238,7 @@ def test_baz(base, fix2): pass def test_other(): pass - """ # noqa: UP031 (python syntax issues) - % {"scope": scope} + """ ) reprec = pytester.inline_run("-lvs") reprec.assertoutcome(passed=3) @@ -3426,42 +3424,40 @@ class TestRequestScopeAccess: def test_setup(self, pytester: Pytester, scope, ok, error) -> None: pytester.makepyfile( - """ + f""" import pytest - @pytest.fixture(scope=%r, autouse=True) + @pytest.fixture(scope={scope!r}, autouse=True) def myscoped(request): - for x in %r: + for x in {ok.split()}: assert hasattr(request, x) - for x in %r: + for x in {error.split()}: pytest.raises(AttributeError, lambda: getattr(request, x)) assert request.session assert request.config def test_func(): pass - """ # noqa: UP031 (python syntax issues) - % (scope, ok.split(), error.split()) + """ ) reprec = pytester.inline_run("-l") reprec.assertoutcome(passed=1) def test_funcarg(self, pytester: Pytester, scope, ok, error) -> None: pytester.makepyfile( - """ + f""" import pytest - @pytest.fixture(scope=%r) + @pytest.fixture(scope={scope!r}) def arg(request): - for x in %r: + for x in {ok.split()!r}: assert hasattr(request, x) - for x in %r: + for x in {error.split()!r}: pytest.raises(AttributeError, lambda: getattr(request, x)) assert request.session assert request.config def test_func(arg): pass - """ # noqa: UP031 (python syntax issues) - % (scope, ok.split(), error.split()) + """ ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) From f55207a47cda64fedb7ece3edab501677d075bdf Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:19:37 +0100 Subject: [PATCH 04/16] chore: skipping tests: migrate to f-strings --- testing/test_skipping.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index d1a63b1d920..57113ba93d5 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -602,13 +602,12 @@ def test_xfail_raises( self, expected, actual, matchline, pytester: Pytester ) -> None: p = pytester.makepyfile( - """ + f""" import pytest - @pytest.mark.xfail(raises=%s) + @pytest.mark.xfail(raises={expected}) def test_raises(): - raise %s() - """ # noqa: UP031 (python syntax issues) - % (expected, actual) + raise {actual}() + """ ) result = pytester.runpytest(p) result.stdout.fnmatch_lines([matchline]) @@ -900,13 +899,12 @@ def test_func(): ) def test_skipif_reporting(self, pytester: Pytester, params) -> None: p = pytester.makepyfile( - test_foo=""" + test_foo=f""" import pytest - @pytest.mark.skipif(%(params)s) + @pytest.mark.skipif({params}) def test_that(): assert 0 - """ # noqa: UP031 (python syntax issues) - % dict(params=params) + """ ) result = pytester.runpytest(p, "-s", "-rs") result.stdout.fnmatch_lines(["*SKIP*1*test_foo.py*platform*", "*1 skipped*"]) From 0a4f394b9f5db27ab882bfc9256ce2b422430b5f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:24:54 +0100 Subject: [PATCH 05/16] chore: junitxml tests: add more type annotations --- testing/test_junitxml.py | 67 +++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 5de0e6a5d7a..3eaa703caf9 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -6,6 +6,7 @@ import os from pathlib import Path import platform +from typing import Any from typing import cast from typing import TYPE_CHECKING from xml.dom import minidom @@ -21,6 +22,7 @@ from _pytest.reports import BaseReport from _pytest.reports import TestReport from _pytest.stash import Stash +import _pytest.timing import pytest @@ -62,13 +64,12 @@ def run_and_parse(pytester: Pytester, schema: xmlschema.XMLSchema) -> RunAndPars return RunAndParse(pytester, schema) -def assert_attr(node, **kwargs): +def assert_attr(node: minidom.Element, **kwargs: object) -> None: __tracebackhide__ = True - def nodeval(node, name): + def nodeval(node: minidom.Element, name: str) -> str | None: anode = node.getAttributeNode(name) - if anode is not None: - return anode.value + return anode.value if anode is not None else None expected = {name: str(value) for name, value in kwargs.items()} on_node = {name: nodeval(node, name) for name in expected} @@ -76,38 +77,35 @@ def nodeval(node, name): class DomNode: - def __init__(self, dom): + def __init__(self, dom: minidom.Element | minidom.Document): self.__node = dom - def __repr__(self): + def __repr__(self) -> str: return self.__node.toxml() - def find_first_by_tag(self, tag): + def find_first_by_tag(self, tag: str) -> DomNode | None: return self.find_nth_by_tag(tag, 0) - def _by_tag(self, tag): - return self.__node.getElementsByTagName(tag) - @property - def children(self): + def children(self) -> list[DomNode]: return [type(self)(x) for x in self.__node.childNodes] @property - def get_unique_child(self): + def get_unique_child(self) -> DomNode: children = self.children assert len(children) == 1 return children[0] - def find_nth_by_tag(self, tag, n): - items = self._by_tag(tag) + def find_nth_by_tag(self, tag: str, n: int) -> DomNode | None: + items = self.__node.getElementsByTagName(tag) try: nth = items[n] except IndexError: - pass + return None else: return type(self)(nth) - def find_by_tag(self, tag): + def find_by_tag(self, tag: str) -> list[DomNode]: t = type(self) return [t(x) for x in self.__node.getElementsByTagName(tag)] @@ -116,23 +114,25 @@ def __getitem__(self, key): if node is not None: return node.value - def assert_attr(self, **kwargs): + def assert_attr(self, **kwargs: object) -> None: __tracebackhide__ = True + assert isinstance(self.__node, minidom.Element) return assert_attr(self.__node, **kwargs) - def toxml(self): + def toxml(self) -> str: return self.__node.toxml() @property - def text(self): - return self.__node.childNodes[0].wholeText + def text(self) -> str: + return cast(str, self.__node.childNodes[0].wholeText) @property - def tag(self): + def tag(self) -> str: + assert isinstance(self.__node, minidom.Element) return self.__node.tagName @property - def next_sibling(self): + def next_sibling(self) -> DomNode: return type(self)(self.__node.nextSibling) @@ -226,7 +226,10 @@ def test_pass(): assert start_time <= timestamp < datetime.now(timezone.utc) def test_timing_function( - self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing + self, + pytester: Pytester, + run_and_parse: RunAndParse, + mock_timing: _pytest.timing.MockTiming, ) -> None: pytester.makepyfile( """ @@ -256,7 +259,7 @@ def test_junit_duration_report( # mock LogXML.node_reporter so it always sets a known duration to each test report object original_node_reporter = LogXML.node_reporter - def node_reporter_wrapper(s, report): + def node_reporter_wrapper(s: Any, report: TestReport) -> Any: report.duration = 1.0 reporter = original_node_reporter(s, report) return reporter @@ -500,9 +503,9 @@ def test_internal_error( def test_failure_function( self, pytester: Pytester, - junit_logging, + junit_logging: str, run_and_parse: RunAndParse, - xunit_family, + xunit_family: str, ) -> None: pytester.makepyfile( """ @@ -600,9 +603,9 @@ def test_func(arg1): assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) - - for index, char in enumerate("<&'"): - tnode = node.find_nth_by_tag("testcase", index) + tnodes = node.find_by_tag("testcase") + assert len(tnodes) == 3 + for tnode, char in zip(tnodes, "<&'"): tnode.assert_attr( classname="test_failure_escape", name=f"test_func[{char}]" ) @@ -945,12 +948,12 @@ class FakeConfig: if TYPE_CHECKING: workerinput = None - def __init__(self): + def __init__(self) -> None: self.pluginmanager = self self.option = self self.stash = Stash() - def getini(self, name): + def getini(self, name: object) -> str: return "pytest" junitprefix = None @@ -1469,7 +1472,7 @@ def test_pass(): result.stdout.no_fnmatch_line("*INTERNALERROR*") items = sorted( - "%(classname)s %(name)s" % x # noqa: UP031 + f"{x['classname']} {x['name']}" # dom is a DomNode not a mapping, it's not possible to ** it. for x in dom.find_by_tag("testcase") ) From 1e8baee02bec75b236f60e6890db62eb837f99e1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 21 Feb 2024 16:29:45 +0100 Subject: [PATCH 06/16] chore: junitxml tests: introduce more typesafe helpers --- testing/test_junitxml.py | 247 ++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 119 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3eaa703caf9..bb66530dead 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -109,10 +109,14 @@ def find_by_tag(self, tag: str) -> list[DomNode]: t = type(self) return [t(x) for x in self.__node.getElementsByTagName(tag)] - def __getitem__(self, key): + def __getitem__(self, key: str) -> str: + if isinstance(self.__node, minidom.Document): + raise TypeError(type(self.__node)) node = self.__node.getAttributeNode(key) if node is not None: - return node.value + return cast(str, node.value) + else: + raise KeyError(key) def assert_attr(self, **kwargs: object) -> None: __tracebackhide__ = True @@ -135,6 +139,13 @@ def tag(self) -> str: def next_sibling(self) -> DomNode: return type(self)(self.__node.nextSibling) + def get_first_by_tag(self, tag: str) -> DomNode: + maybe = self.find_first_by_tag(tag) + if maybe is None: + raise LookupError(tag) + else: + return maybe + parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"]) @@ -163,7 +174,7 @@ def test_xpass(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5) @parametrize_families @@ -192,7 +203,7 @@ def test_xpass(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) @parametrize_families @@ -206,7 +217,7 @@ def test_pass(): """ ) result, dom = run_and_parse(family=xunit_family) - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(hostname=platform.node()) @parametrize_families @@ -221,9 +232,9 @@ def test_pass(): ) start_time = datetime.now(timezone.utc) result, dom = run_and_parse(family=xunit_family) - node = dom.find_first_by_tag("testsuite") - timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z") - assert start_time <= timestamp < datetime.now(timezone.utc) + node = dom.get_first_by_tag("testsuite") + timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") + assert start_time <= timestamp < datetime.now() def test_timing_function( self, @@ -243,9 +254,10 @@ def test_sleep(): """ ) result, dom = run_and_parse() - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") val = tnode["time"] + assert val is not None assert float(val) == 7.0 @pytest.mark.parametrize("duration_report", ["call", "total"]) @@ -273,8 +285,8 @@ def test_foo(): """ ) result, dom = run_and_parse("-o", f"junit_duration_report={duration_report}") - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") val = float(tnode["time"]) if duration_report == "total": assert val == 3.0 @@ -299,11 +311,11 @@ def test_function(arg): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_setup_error", name="test_function") - fnode = tnode.find_first_by_tag("error") + fnode = tnode.get_first_by_tag("error") fnode.assert_attr(message='failed on setup with "ValueError: Error reason"') assert "ValueError" in fnode.toxml() @@ -325,10 +337,10 @@ def test_function(arg): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_teardown_error", name="test_function") - fnode = tnode.find_first_by_tag("error") + fnode = tnode.get_first_by_tag("error") fnode.assert_attr(message='failed on teardown with "ValueError: Error reason"') assert "ValueError" in fnode.toxml() @@ -350,15 +362,15 @@ def test_function(arg): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(errors=1, failures=1, tests=1) first, second = dom.find_by_tag("testcase") assert first assert second assert first != second - fnode = first.find_first_by_tag("failure") + fnode = first.get_first_by_tag("failure") fnode.assert_attr(message="Exception: Call Exception") - snode = second.find_first_by_tag("error") + snode = second.get_first_by_tag("error") snode.assert_attr( message='failed on teardown with "Exception: Teardown Exception"' ) @@ -376,11 +388,11 @@ def test_skip(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_skip_contains_name_reason", name="test_skip") - snode = tnode.find_first_by_tag("skipped") + snode = tnode.get_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello23") @parametrize_families @@ -397,13 +409,13 @@ def test_skip(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr( classname="test_mark_skip_contains_name_reason", name="test_skip" ) - snode = tnode.find_first_by_tag("skipped") + snode = tnode.get_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello24") @parametrize_families @@ -421,13 +433,13 @@ def test_skip(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr( classname="test_mark_skipif_contains_name_reason", name="test_skip" ) - snode = tnode.find_first_by_tag("skipped") + snode = tnode.get_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25") @parametrize_families @@ -444,7 +456,7 @@ def test_skip(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node_xml = dom.find_first_by_tag("testsuite").toxml() + node_xml = dom.get_first_by_tag("testsuite").toxml() assert "bar!" not in node_xml @parametrize_families @@ -460,9 +472,9 @@ def test_method(self): ) result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr( classname="test_classname_instance.TestClass", name="test_method" ) @@ -475,9 +487,9 @@ def test_classname_nested_dir( p.write_text("def test_func(): 0/0", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="sub.test_hello", name="test_func") @parametrize_families @@ -488,11 +500,11 @@ def test_internal_error( pytester.makepyfile("def test_function(): pass") result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="pytest", name="internal") - fnode = tnode.find_first_by_tag("error") + fnode = tnode.get_first_by_tag("error") fnode.assert_attr(message="internal error") assert "Division" in fnode.toxml() @@ -525,22 +537,22 @@ def test_fail(): "-o", f"junit_logging={junit_logging}", family=xunit_family ) assert result.ret, "Expected ret > 0" - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_failure_function", name="test_fail") - fnode = tnode.find_first_by_tag("failure") + fnode = tnode.get_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml(), "ValueError not included" if junit_logging in ["log", "all"]: - logdata = tnode.find_first_by_tag("system-out") + logdata = tnode.get_first_by_tag("system-out") log_xml = logdata.toxml() assert logdata.tag == "system-out", "Expected tag: system-out" assert "info msg" not in log_xml, "Unexpected INFO message" assert "warning msg" in log_xml, "Missing WARN message" if junit_logging in ["system-out", "out-err", "all"]: - systemout = tnode.find_first_by_tag("system-out") + systemout = tnode.get_first_by_tag("system-out") systemout_xml = systemout.toxml() assert systemout.tag == "system-out", "Expected tag: system-out" assert "info msg" not in systemout_xml, "INFO message found in system-out" @@ -548,7 +560,7 @@ def test_fail(): "Missing 'hello-stdout' in system-out" ) if junit_logging in ["system-err", "out-err", "all"]: - systemerr = tnode.find_first_by_tag("system-err") + systemerr = tnode.get_first_by_tag("system-err") systemerr_xml = systemerr.toxml() assert systemerr.tag == "system-err", "Expected tag: system-err" assert "info msg" not in systemerr_xml, "INFO message found in system-err" @@ -579,9 +591,9 @@ def test_fail(): """ ) result, dom = run_and_parse(family=xunit_family) - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") - fnode = tnode.find_first_by_tag("failure") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") + fnode = tnode.get_first_by_tag("failure") fnode.assert_attr(message="AssertionError: An error\nassert 0") @parametrize_families @@ -601,7 +613,7 @@ def test_func(arg1): "-o", "junit_logging=system-out", family=xunit_family ) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) tnodes = node.find_by_tag("testcase") assert len(tnodes) == 3 @@ -609,7 +621,7 @@ def test_func(arg1): tnode.assert_attr( classname="test_failure_escape", name=f"test_func[{char}]" ) - sysout = tnode.find_first_by_tag("system-out") + sysout = tnode.get_first_by_tag("system-out") text = sysout.text assert f"{char}\n" in text @@ -628,11 +640,11 @@ def test_hello(self): ) result, dom = run_and_parse("--junitprefix=xyz", family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(failures=1, tests=2) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="xyz.test_junit_prefixing", name="test_func") - tnode = node.find_nth_by_tag("testcase", 1) + tnode = node.find_by_tag("testcase")[1] tnode.assert_attr( classname="xyz.test_junit_prefixing.TestHello", name="test_hello" ) @@ -650,11 +662,11 @@ def test_xfail(): ) result, dom = run_and_parse(family=xunit_family) assert not result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_function", name="test_xfail") - fnode = tnode.find_first_by_tag("skipped") + fnode = tnode.get_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") @parametrize_families @@ -671,11 +683,11 @@ def test_xfail(): ) result, dom = run_and_parse(family=xunit_family) assert not result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_marker", name="test_xfail") - fnode = tnode.find_first_by_tag("skipped") + fnode = tnode.get_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") @pytest.mark.parametrize( @@ -697,17 +709,13 @@ def test_fail(): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") - if junit_logging in ["system-err", "out-err", "all"]: - assert len(tnode.find_by_tag("system-err")) == 1 - else: - assert len(tnode.find_by_tag("system-err")) == 0 + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") - if junit_logging in ["log", "system-out", "out-err", "all"]: - assert len(tnode.find_by_tag("system-out")) == 1 - else: - assert len(tnode.find_by_tag("system-out")) == 0 + has_logging = junit_logging in ["system-err", "out-err", "all"] + expected_output_len = 1 if has_logging else 0 + print(tnode) + assert len(tnode.find_by_tag("system-err")) == expected_output_len @parametrize_families def test_xfailure_xpass( @@ -723,9 +731,9 @@ def test_xpass(): ) result, dom = run_and_parse(family=xunit_family) # assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass") @parametrize_families @@ -742,11 +750,11 @@ def test_xpass(): ) result, dom = run_and_parse(family=xunit_family) # assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_xpass_strict", name="test_xpass") - fnode = tnode.find_first_by_tag("failure") + fnode = tnode.get_first_by_tag("failure") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") @parametrize_families @@ -756,10 +764,10 @@ def test_collect_error( pytester.makepyfile("syntax error") result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) - tnode = node.find_first_by_tag("testcase") - fnode = tnode.find_first_by_tag("error") + tnode = node.get_first_by_tag("testcase") + fnode = tnode.get_first_by_tag("error") fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() @@ -775,8 +783,8 @@ def test_hello(): ) result, dom = run_and_parse() assert result.ret == 1 - tnode = dom.find_first_by_tag("testcase") - fnode = tnode.find_first_by_tag("failure") + tnode = dom.get_first_by_tag("testcase") + fnode = tnode.get_first_by_tag("failure") assert "hx" in fnode.toxml() def test_assertion_binchars( @@ -807,8 +815,8 @@ def test_pass(): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - pnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + pnode = node.get_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag("system-out"), ( "system-out should not be generated" @@ -831,8 +839,8 @@ def test_pass(): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - pnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + pnode = node.get_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag("system-err"), ( "system-err should not be generated" @@ -860,8 +868,8 @@ def test_function(arg): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - pnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + pnode = node.get_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag("system-out"), ( "system-out should not be generated" @@ -890,8 +898,8 @@ def test_function(arg): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - pnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + pnode = node.get_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag("system-err"), ( "system-err should not be generated" @@ -921,14 +929,14 @@ def test_function(arg): """ ) result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") - node = dom.find_first_by_tag("testsuite") - pnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + pnode = node.get_first_by_tag("testcase") if junit_logging == "no": assert not node.find_by_tag("system-out"), ( "system-out should not be generated" ) if junit_logging == "system-out": - systemout = pnode.find_first_by_tag("system-out") + systemout = pnode.get_first_by_tag("system-out") assert "hello-stdout call" in systemout.toxml() assert "hello-stdout teardown" in systemout.toxml() @@ -992,11 +1000,11 @@ def repr_failure(self, excinfo): pytester.path.joinpath("myfile.xyz").write_text("hello", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(errors=0, failures=1, skipped=0, tests=1) - tnode = node.find_first_by_tag("testcase") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(name="myfile.xyz") - fnode = tnode.find_first_by_tag("failure") + fnode = tnode.get_first_by_tag("failure") fnode.assert_attr(message="custom item runtest failed") assert "custom item runtest failed" in fnode.toxml() @@ -1137,7 +1145,7 @@ def test_func(char): ) result, dom = run_and_parse() assert result.ret == 0 - node = dom.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testcase") node.assert_attr(name="test_func[\\x00]") @@ -1154,7 +1162,7 @@ def test_func(param): ) result, dom = run_and_parse() assert result.ret == 0 - node = dom.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_function_issue469") node.assert_attr(name="test_func[double::colon]") @@ -1173,7 +1181,7 @@ def test_func(self, param): ) result, dom = run_and_parse() assert result.ret == 0 - node = dom.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_method_issue469.TestClass") node.assert_attr(name="test_func[double::colon]") @@ -1221,9 +1229,9 @@ def test_record(record_property, other): """ ) result, dom = run_and_parse() - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") - psnode = tnode.find_first_by_tag("properties") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") + psnode = tnode.get_first_by_tag("properties") pnodes = psnode.find_by_tag("property") pnodes[0].assert_attr(name="bar", value="1") pnodes[1].assert_attr(name="foo", value="<1") @@ -1249,10 +1257,10 @@ def test_record(record_property, other): """ ) result, dom = run_and_parse() - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") tnodes = node.find_by_tag("testcase") for tnode in tnodes: - psnode = tnode.find_first_by_tag("properties") + psnode = tnode.get_first_by_tag("properties") assert psnode, f"testcase didn't had expected properties:\n{tnode}" pnodes = psnode.find_by_tag("property") pnodes[0].assert_attr(name="bar", value="1") @@ -1271,9 +1279,9 @@ def test_record_with_same_name(record_property): """ ) result, dom = run_and_parse() - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") - psnode = tnode.find_first_by_tag("properties") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") + psnode = tnode.get_first_by_tag("properties") pnodes = psnode.find_by_tag("property") pnodes[0].assert_attr(name="foo", value="bar") pnodes[1].assert_attr(name="foo", value="baz") @@ -1313,8 +1321,8 @@ def test_record(record_xml_attribute, other): """ ) result, dom = run_and_parse() - node = dom.find_first_by_tag("testsuite") - tnode = node.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testsuite") + tnode = node.get_first_by_tag("testcase") tnode.assert_attr(bar="1") tnode.assert_attr(foo="<1") result.stdout.fnmatch_lines( @@ -1376,7 +1384,7 @@ def test_x(i): """ ) _, dom = run_and_parse("-n2") - suite_node = dom.find_first_by_tag("testsuite") + suite_node = dom.get_first_by_tag("testsuite") failed = [] for case_node in suite_node.find_by_tag("testcase"): if case_node.find_first_by_tag("failure"): @@ -1567,10 +1575,11 @@ def test_func2(record_testsuite_property): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testsuite") - properties_node = node.find_first_by_tag("properties") - p1_node = properties_node.find_nth_by_tag("property", 0) - p2_node = properties_node.find_nth_by_tag("property", 1) + node = dom.get_first_by_tag("testsuite") + properties_node = node.get_first_by_tag("properties") + p1_node, p2_node = properties_node.find_by_tag( + "property", + )[:2] p1_node.assert_attr(name="stats", value="all good") p2_node.assert_attr(name="stats", value="10") @@ -1630,7 +1639,7 @@ def test_func(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testsuite") + node = dom.get_first_by_tag("testsuite") node.assert_attr(name=expected) @@ -1646,8 +1655,8 @@ def test_skip(): """ ) _, dom = run_and_parse() - node = dom.find_first_by_tag("testcase") - snode = node.find_first_by_tag("skipped") + node = dom.get_first_by_tag("testcase") + snode = node.get_first_by_tag("skipped") assert "1 <> 2" in snode.text snode.assert_attr(message="1 <> 2") @@ -1663,8 +1672,8 @@ def test_skip(): """ ) _, dom = run_and_parse() - node = dom.find_first_by_tag("testcase") - snode = node.find_first_by_tag("skipped") + node = dom.get_first_by_tag("testcase") + snode = node.get_first_by_tag("skipped") assert "#x1B[31;1mred#x1B[0m" in snode.text snode.assert_attr(message="#x1B[31;1mred#x1B[0m") @@ -1685,8 +1694,8 @@ def test_esc(my_setup): """ ) _, dom = run_and_parse() - node = dom.find_first_by_tag("testcase") - snode = node.find_first_by_tag("error") + node = dom.get_first_by_tag("testcase") + snode = node.get_first_by_tag("error") assert "#x1B[31mred#x1B[m" in snode["message"] assert "#x1B[31mred#x1B[m" in snode.text @@ -1717,7 +1726,7 @@ def test_func(): ) result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 - node = dom.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testcase") assert len(node.find_by_tag("system-err")) == 0 assert len(node.find_by_tag("system-out")) == 0 @@ -1752,7 +1761,7 @@ def test_func(): "-o", f"junit_logging={junit_logging}", family=xunit_family ) assert result.ret == 1 - node = dom.find_first_by_tag("testcase") + node = dom.get_first_by_tag("testcase") if junit_logging == "system-out": assert len(node.find_by_tag("system-err")) == 0 assert len(node.find_by_tag("system-out")) == 1 From 865cb4f0b6dcba64d8dcc4c8d8e7c7609731781c Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 21 Jun 2024 12:07:01 +0200 Subject: [PATCH 07/16] add changelog --- changelog/12017.improvement.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/12017.improvement.rst diff --git a/changelog/12017.improvement.rst b/changelog/12017.improvement.rst new file mode 100644 index 00000000000..03c1e4a8877 --- /dev/null +++ b/changelog/12017.improvement.rst @@ -0,0 +1,5 @@ +* migrate formatting tests to fstrings +* use typesafe constructs in the junitxml tests +* separate mocktiming into ``_pytest.timing`` + +-- by :user:`RonnyPfannschmidt` From 674ec71bf6a536d93bf9aa7b2282e8f3773e2791 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 22 Jun 2024 15:05:30 +0200 Subject: [PATCH 08/16] test junitxml: split the DomNode type to express the Document/Element difference --- testing/test_junitxml.py | 71 +++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index bb66530dead..4fabab3798f 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,4 +1,3 @@ -# mypy: allow-untyped-defs from __future__ import annotations from datetime import datetime @@ -41,7 +40,7 @@ def __init__(self, pytester: Pytester, schema: xmlschema.XMLSchema) -> None: def __call__( self, *args: str | os.PathLike[str], family: str | None = "xunit1" - ) -> tuple[RunResult, DomNode]: + ) -> tuple[RunResult, DomDocument]: if family: args = ("-o", "junit_family=" + family, *args) xml_path = self.pytester.path.joinpath("junit.xml") @@ -50,7 +49,7 @@ def __call__( with xml_path.open(encoding="utf-8") as f: self.schema.validate(f) xmldoc = minidom.parse(str(xml_path)) - return result, DomNode(xmldoc) + return result, DomDocument(xmldoc) @pytest.fixture @@ -76,25 +75,21 @@ def nodeval(node: minidom.Element, name: str) -> str | None: assert on_node == expected -class DomNode: - def __init__(self, dom: minidom.Element | minidom.Document): +class DomDocument: + def __init__(self, dom: minidom.Document): self.__node = dom - def __repr__(self) -> str: - return self.__node.toxml() + __node: minidom.Document def find_first_by_tag(self, tag: str) -> DomNode | None: return self.find_nth_by_tag(tag, 0) - @property - def children(self) -> list[DomNode]: - return [type(self)(x) for x in self.__node.childNodes] - - @property - def get_unique_child(self) -> DomNode: - children = self.children - assert len(children) == 1 - return children[0] + def get_first_by_tag(self, tag: str) -> DomNode: + maybe = self.find_first_by_tag(tag) + if maybe is None: + raise LookupError(tag) + else: + return maybe def find_nth_by_tag(self, tag: str, n: int) -> DomNode | None: items = self.__node.getElementsByTagName(tag) @@ -103,15 +98,35 @@ def find_nth_by_tag(self, tag: str, n: int) -> DomNode | None: except IndexError: return None else: - return type(self)(nth) + return DomNode(nth) def find_by_tag(self, tag: str) -> list[DomNode]: - t = type(self) - return [t(x) for x in self.__node.getElementsByTagName(tag)] + return [DomNode(x) for x in self.__node.getElementsByTagName(tag)] + + @property + def children(self) -> list[DomNode]: + return [DomNode(x) for x in self.__node.childNodes] + + @property + def get_unique_child(self) -> DomNode: + children = self.children + assert len(children) == 1 + return children[0] + + def toxml(self) -> str: + return self.__node.toxml() + + +class DomNode(DomDocument): + __node: minidom.Element + + def __init__(self, dom: minidom.Element): + self.__node = dom + + def __repr__(self) -> str: + return self.toxml() def __getitem__(self, key: str) -> str: - if isinstance(self.__node, minidom.Document): - raise TypeError(type(self.__node)) node = self.__node.getAttributeNode(key) if node is not None: return cast(str, node.value) @@ -120,31 +135,19 @@ def __getitem__(self, key: str) -> str: def assert_attr(self, **kwargs: object) -> None: __tracebackhide__ = True - assert isinstance(self.__node, minidom.Element) return assert_attr(self.__node, **kwargs) - def toxml(self) -> str: - return self.__node.toxml() - @property def text(self) -> str: return cast(str, self.__node.childNodes[0].wholeText) @property def tag(self) -> str: - assert isinstance(self.__node, minidom.Element) return self.__node.tagName @property def next_sibling(self) -> DomNode: - return type(self)(self.__node.nextSibling) - - def get_first_by_tag(self, tag: str) -> DomNode: - maybe = self.find_first_by_tag(tag) - if maybe is None: - raise LookupError(tag) - else: - return maybe + return DomNode(self.__node.nextSibling) parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"]) From 83762c56ea7d0c96d413220f4a2d08038ac9923b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 22 Jun 2024 15:28:59 +0200 Subject: [PATCH 09/16] fixup: undo accidential name-mangling in the junitxml test helpers --- testing/test_junitxml.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 4fabab3798f..babe0f089bb 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -77,9 +77,9 @@ def nodeval(node: minidom.Element, name: str) -> str | None: class DomDocument: def __init__(self, dom: minidom.Document): - self.__node = dom + self._node = dom - __node: minidom.Document + _node: minidom.Document | minidom.Element def find_first_by_tag(self, tag: str) -> DomNode | None: return self.find_nth_by_tag(tag, 0) @@ -92,7 +92,7 @@ def get_first_by_tag(self, tag: str) -> DomNode: return maybe def find_nth_by_tag(self, tag: str, n: int) -> DomNode | None: - items = self.__node.getElementsByTagName(tag) + items = self._node.getElementsByTagName(tag) try: nth = items[n] except IndexError: @@ -101,11 +101,11 @@ def find_nth_by_tag(self, tag: str, n: int) -> DomNode | None: return DomNode(nth) def find_by_tag(self, tag: str) -> list[DomNode]: - return [DomNode(x) for x in self.__node.getElementsByTagName(tag)] + return [DomNode(x) for x in self._node.getElementsByTagName(tag)] @property def children(self) -> list[DomNode]: - return [DomNode(x) for x in self.__node.childNodes] + return [DomNode(x) for x in self._node.childNodes] @property def get_unique_child(self) -> DomNode: @@ -114,20 +114,20 @@ def get_unique_child(self) -> DomNode: return children[0] def toxml(self) -> str: - return self.__node.toxml() + return self._node.toxml() class DomNode(DomDocument): - __node: minidom.Element + _node: minidom.Element def __init__(self, dom: minidom.Element): - self.__node = dom + self._node = dom def __repr__(self) -> str: return self.toxml() def __getitem__(self, key: str) -> str: - node = self.__node.getAttributeNode(key) + node = self._node.getAttributeNode(key) if node is not None: return cast(str, node.value) else: @@ -135,19 +135,19 @@ def __getitem__(self, key: str) -> str: def assert_attr(self, **kwargs: object) -> None: __tracebackhide__ = True - return assert_attr(self.__node, **kwargs) + return assert_attr(self._node, **kwargs) @property def text(self) -> str: - return cast(str, self.__node.childNodes[0].wholeText) + return cast(str, self._node.childNodes[0].wholeText) @property def tag(self) -> str: - return self.__node.tagName + return self._node.tagName @property def next_sibling(self) -> DomNode: - return DomNode(self.__node.nextSibling) + return DomNode(self._node.nextSibling) parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"]) From 599604a29f35947a94a6166a5252a27e81aed66d Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 22 Jun 2024 15:29:43 +0200 Subject: [PATCH 10/16] fixup: junitxml logging test - restore validation for expected output nodes --- testing/test_junitxml.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index babe0f089bb..97e520c93f4 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -715,10 +715,14 @@ def test_fail(): node = dom.get_first_by_tag("testsuite") tnode = node.get_first_by_tag("testcase") - has_logging = junit_logging in ["system-err", "out-err", "all"] - expected_output_len = 1 if has_logging else 0 - print(tnode) - assert len(tnode.find_by_tag("system-err")) == expected_output_len + has_err_logging = junit_logging in ["system-err", "out-err", "all"] + expected_err_output_len = 1 if has_err_logging else 0 + assert len(tnode.find_by_tag("system-err")) == expected_err_output_len + + has_out_logigng = junit_logging in ("log", "system-out", "out-err", "all") + expected_out_output_len = 1 if has_out_logigng else 0 + + assert len(tnode.find_by_tag("system-out")) == expected_out_output_len @parametrize_families def test_xfailure_xpass( From 07cff2f17505df6fff457e1d34dad0e04c82586f Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 22 Jun 2024 15:31:14 +0200 Subject: [PATCH 11/16] fixup: have fake config .getini use correct types --- testing/test_junitxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 97e520c93f4..703ef585603 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -968,7 +968,7 @@ def __init__(self) -> None: self.option = self self.stash = Stash() - def getini(self, name: object) -> str: + def getini(self, name: str) -> str: return "pytest" junitprefix = None From 6b81d3facbdeca1046600f54eeec81b600d944d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 18 Jul 2024 08:10:57 +0200 Subject: [PATCH 12/16] Update changelog/12017.improvement.rst Co-authored-by: Bruno Oliveira --- changelog/12017.improvement.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/changelog/12017.improvement.rst b/changelog/12017.improvement.rst index 03c1e4a8877..ec1861893b3 100644 --- a/changelog/12017.improvement.rst +++ b/changelog/12017.improvement.rst @@ -1,5 +1,7 @@ -* migrate formatting tests to fstrings -* use typesafe constructs in the junitxml tests -* separate mocktiming into ``_pytest.timing`` +Mixed internal improvements: + +* Migrate formatting to f-strings in some tests. +* Use type-safe constructs in JUnitXML tests. +* Moved`` MockTiming`` into ``_pytest.timing``. -- by :user:`RonnyPfannschmidt` From e0088016990ba644eb7aa9b4c26254cc1e4d47cb Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 18 Jul 2024 08:14:54 +0200 Subject: [PATCH 13/16] use contrib category for changelog --- changelog/{12017.improvement.rst => 12017.contrib.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{12017.improvement.rst => 12017.contrib.rst} (100%) diff --git a/changelog/12017.improvement.rst b/changelog/12017.contrib.rst similarity index 100% rename from changelog/12017.improvement.rst rename to changelog/12017.contrib.rst From 16ec43a88c953617579b1af8cffd69cda6f6c3c6 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 18 Jul 2024 08:25:48 +0200 Subject: [PATCH 14/16] use datetime.fromisoformat instead of strptime --- testing/test_junitxml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 703ef585603..cdab0330074 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -236,8 +236,8 @@ def test_pass(): start_time = datetime.now(timezone.utc) result, dom = run_and_parse(family=xunit_family) node = dom.get_first_by_tag("testsuite") - timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") - assert start_time <= timestamp < datetime.now() + timestamp = datetime.fromisoformat(node["timestamp"]) + assert start_time <= timestamp < datetime.now(timezone.utc) def test_timing_function( self, From 9cad2d77f20d97cee4793caf5ccd2c5acad4f86e Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 10 Mar 2025 09:55:17 +0100 Subject: [PATCH 15/16] post rebase ruff fixes --- src/_pytest/timing.py | 2 +- testing/conftest.py | 1 - testing/test_junitxml.py | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index e2dde161732..4422037a9d9 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -33,7 +33,7 @@ class MockTiming: Time is static, and only advances through `sleep` calls, thus tests might sleep over large numbers and obtain accurate time() calls at the end, making tests reliable and instant.""" - _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() # noqa: RUF009 + _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp() def sleep(self, seconds: float) -> None: self._current_time += seconds diff --git a/testing/conftest.py b/testing/conftest.py index 5824633bea2..251b430e9cd 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Generator -import dataclasses import importlib.metadata import re import sys diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index cdab0330074..d05160b4ec9 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -829,7 +829,7 @@ def test_pass(): "system-out should not be generated" ) if junit_logging == "system-out": - systemout = pnode.find_first_by_tag("system-out") + systemout = pnode.get_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml(), ( "'hello-stdout' should be in system-out" ) @@ -853,7 +853,7 @@ def test_pass(): "system-err should not be generated" ) if junit_logging == "system-err": - systemerr = pnode.find_first_by_tag("system-err") + systemerr = pnode.get_first_by_tag("system-err") assert "hello-stderr" in systemerr.toxml(), ( "'hello-stderr' should be in system-err" ) @@ -882,7 +882,7 @@ def test_function(arg): "system-out should not be generated" ) if junit_logging == "system-out": - systemout = pnode.find_first_by_tag("system-out") + systemout = pnode.get_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml(), ( "'hello-stdout' should be in system-out" ) @@ -912,7 +912,7 @@ def test_function(arg): "system-err should not be generated" ) if junit_logging == "system-err": - systemerr = pnode.find_first_by_tag("system-err") + systemerr = pnode.get_first_by_tag("system-err") assert "hello-stderr" in systemerr.toxml(), ( "'hello-stderr' should be in system-err" ) From f5de7c1a193429c86a99209c871d7cd75107635b Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 10 Mar 2025 11:52:59 +0100 Subject: [PATCH 16/16] add extra coverage for junit test helpers --- testing/test_junitxml.py | 44 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index d05160b4ec9..4145f34ab14 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -145,9 +145,47 @@ def text(self) -> str: def tag(self) -> str: return self._node.tagName - @property - def next_sibling(self) -> DomNode: - return DomNode(self._node.nextSibling) + +class TestJunitHelpers: + """minimal test to increase coverage for methods that are used in debugging""" + + @pytest.fixture + def document(self) -> DomDocument: + doc = minidom.parseString(""" + + + + +""") + return DomDocument(doc) + + def test_uc_root(self, document: DomDocument) -> None: + assert document.get_unique_child.tag == "root" + + def test_node_assert_attr(self, document: DomDocument) -> None: + item = document.get_first_by_tag("item") + + item.assert_attr(name="a") + + with pytest.raises(AssertionError): + item.assert_attr(missing="foo") + + def test_node_getitem(self, document: DomDocument) -> None: + item = document.get_first_by_tag("item") + assert item["name"] == "a" + + with pytest.raises(KeyError, match="missing"): + item["missing"] + + def test_node_get_first_lookup(self, document: DomDocument) -> None: + with pytest.raises(LookupError, match="missing"): + document.get_first_by_tag("missing") + + def test_node_repr(self, document: DomDocument) -> None: + item = document.get_first_by_tag("item") + + assert repr(item) == item.toxml() + assert item.toxml() == '' parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"])