diff --git a/changelog/14050.bugfix.rst b/changelog/14050.bugfix.rst new file mode 100644 index 00000000000..451b248576f --- /dev/null +++ b/changelog/14050.bugfix.rst @@ -0,0 +1 @@ +Display dictionary differences in assertion failures using the original key insertion order instead of sorted order. diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 28f06909206..ec41b449ddf 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -162,7 +162,7 @@ def _pprint_dict( ) -> None: write = stream.write write("{") - items = sorted(object.items(), key=_safe_tuple) + items = object.items() self._format_dict_items(items, stream, indent, allowance, context, level) write("}") @@ -608,7 +608,7 @@ def _safe_repr( components: list[str] = [] append = components.append level += 1 - for k, v in sorted(object.items(), key=_safe_tuple): + for k, v in object.items(): krepr = self._safe_repr(k, context, maxlevels, level) vrepr = self._safe_repr(v, context, maxlevels, level) append(f"{krepr}: {vrepr}") diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index cee70e332f9..3f5c956d9cf 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import islice import pprint import reprlib @@ -77,8 +78,32 @@ def repr_instance(self, x: object, level: int) -> str: s = _format_repr_exception(exc, x) if self.maxsize is not None: s = _ellipsize(s, self.maxsize) + return s + def repr_dict(self, x: dict[object, object], level: int) -> str: + """Represent a dict while preserving its insertion order. + + Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x`` + rather than using the stdlib's sorting helper. + """ + fillvalue = "..." + n = len(x) + if n == 0: + return "{}" + if level <= 0: + return "{" + fillvalue + "}" + newlevel = level - 1 + repr1 = self.repr1 + pieces = [] + for key in islice(x, self.maxdict): + keyrepr = repr1(key, newlevel) + valrepr = repr1(x[key], newlevel) + pieces.append(f"{keyrepr}: {valrepr}") + if n > self.maxdict: + pieces.append(fillvalue) + return "{" + ", ".join(pieces) + "}" + def safeformat(obj: object) -> str: """Return a pretty printed string for the given object. diff --git a/src/_pytest/pytester_assertions.py b/src/_pytest/pytester_assertions.py index 915cc8a10ff..b8d8a195241 100644 --- a/src/_pytest/pytester_assertions.py +++ b/src/_pytest/pytester_assertions.py @@ -26,11 +26,11 @@ def assertoutcome( realpassed, realskipped, realfailed = outcomes obtained = { + "failed": len(realfailed), "passed": len(realpassed), "skipped": len(realskipped), - "failed": len(realfailed), } - expected = {"passed": passed, "skipped": skipped, "failed": failed} + expected = {"failed": failed, "passed": passed, "skipped": skipped} assert obtained == expected, outcomes diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 075d40cdf44..2e3dd55b81f 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -2,6 +2,7 @@ from __future__ import annotations from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE +from _pytest._io.saferepr import SafeRepr from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr_unlimited import pytest @@ -192,3 +193,39 @@ def __repr__(self): assert saferepr_unlimited(A()).startswith( "<[ValueError(42) raised in repr()] A object at 0x" ) + + +def test_saferepr_dict_preserves_insertion_order() -> None: + d = { + "b": 2, + "a": 1, + "d": 4, + "e": 5, + "c": 3, + } + s = SafeRepr(maxsize=None) + s.maxdict = 10 + assert s.repr(d) == "{'b': 2, 'a': 1, 'd': 4, 'e': 5, 'c': 3}" + + +def test_saferepr_dict_truncation_preserves_insertion_order() -> None: + d = { + "b": 2, + "a": 1, + "d": 4, + "e": 5, + "c": 3, + } + s = SafeRepr(maxsize=None) + s.maxdict = 1 + assert s.repr(d) == "{'b': 2, ...}" + + +def test_saferepr_dict_fillvalue_when_level_is_zero() -> None: + s = SafeRepr(maxsize=None) + assert s.repr_dict({"a": 1}, level=0) == "{...}" + + +def test_saferepr_dict_empty() -> None: + s = SafeRepr(maxsize=None) + assert s.repr_dict({}, level=1) == "{}" diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 9c9881cf8ed..d68fd0b1fba 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -2209,3 +2209,34 @@ def test_vvv(): ] ) result.stdout.no_fnmatch_line(expected_non_vvv_arg_line) + + +def test_dict_extra_items_preserve_insertion_order(pytester: Pytester) -> None: + """Assertion output of dict diff shows keys in insertion order (#13503).""" + pytester.makepyfile( + test_order=""" + def test_order(): + a = { + "b": 2, + "a": 1, + "d": 4, + "e": 5, + "c": 3, + } + assert a == {} + """ + ) + + result = pytester.runpytest("-vv") + result.stdout.fnmatch_lines( + [ + "*Left contains 5 more items:*", + "*Full diff:", + "* + *'b': 2,", + "* + *'a': 1,", + "* + *'d': 4,", + "* + *'e': 5,", + "* + *'c': 3,", + "test_order.py:*: AssertionError", + ] + )