diff --git a/changelog/10617.feature.rst b/changelog/10617.feature.rst new file mode 100644 index 000000000..c99ec4889 --- /dev/null +++ b/changelog/10617.feature.rst @@ -0,0 +1,2 @@ +Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with +the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 01534797d..057352cd3 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -193,6 +193,22 @@ def assertrepr_compare( elif op == "not in": if istext(left) and istext(right): explanation = _notin_text(left, right, verbose) + elif op == "!=": + if isset(left) and isset(right): + explanation = ["Both sets are equal"] + elif op == ">=": + if isset(left) and isset(right): + explanation = _compare_gte_set(left, right, verbose) + elif op == "<=": + if isset(left) and isset(right): + explanation = _compare_lte_set(left, right, verbose) + elif op == ">": + if isset(left) and isset(right): + explanation = _compare_gt_set(left, right, verbose) + elif op == "<": + if isset(left) and isset(right): + explanation = _compare_lt_set(left, right, verbose) + except outcomes.Exit: raise except Exception: @@ -392,15 +408,49 @@ def _compare_eq_set( left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 ) -> List[str]: explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append("Extra items in the left set:") - for item in diff_left: - explanation.append(saferepr(item)) - if diff_right: - explanation.append("Extra items in the right set:") - for item in diff_right: + explanation.extend(_set_one_sided_diff("left", left, right)) + explanation.extend(_set_one_sided_diff("right", right, left)) + return explanation + + +def _compare_gt_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: + explanation = _compare_gte_set(left, right, verbose) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_lt_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: + explanation = _compare_lte_set(left, right, verbose) + if not explanation: + return ["Both sets are equal"] + return explanation + + +def _compare_gte_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: + return _set_one_sided_diff("right", right, left) + + +def _compare_lte_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: + return _set_one_sided_diff("left", left, right) + + +def _set_one_sided_diff( + posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any] +) -> List[str]: + explanation = [] + diff = set1 - set2 + if diff: + explanation.append(f"Extra items in the {posn} set:") + for item in diff: explanation.append(saferepr(item)) return explanation diff --git a/testing/test_assertion.py b/testing/test_assertion.py index c04c31f31..d3caa5e48 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1345,48 +1345,80 @@ def test_reprcompare_whitespaces() -> None: ] -def test_pytest_assertrepr_compare_integration(pytester: Pytester) -> None: - pytester.makepyfile( +class TestSetAssertions: + @pytest.mark.parametrize("op", [">=", ">", "<=", "<", "=="]) + def test_set_extra_item(self, op, pytester: Pytester) -> None: + pytester.makepyfile( + f""" + def test_hello(): + x = set("hello x") + y = set("hello y") + assert x {op} y """ - def test_hello(): - x = set(range(100)) - y = x.copy() - y.remove(50) - assert x == y - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*50*", - "*= 1 failed in*", - ] - ) + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + f"*assert x {op} y*", + ] + ) + if op in [">=", ">", "=="]: + result.stdout.fnmatch_lines( + [ + "*E*Extra items in the right set:*", + "*E*'y'", + ] + ) + if op in ["<=", "<", "=="]: + result.stdout.fnmatch_lines( + [ + "*E*Extra items in the left set:*", + "*E*'x'", + ] + ) -def test_sequence_comparison_uses_repr(pytester: Pytester) -> None: - pytester.makepyfile( + @pytest.mark.parametrize("op", [">", "<", "!="]) + def test_set_proper_superset_equal(self, pytester: Pytester, op) -> None: + pytester.makepyfile( + f""" + def test_hello(): + x = set([1, 2, 3]) + y = x.copy() + assert x {op} y """ - def test_hello(): - x = set("hello x") - y = set("hello y") - assert x == y - """ - ) - result = pytester.runpytest() - result.stdout.fnmatch_lines( - [ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*'x'*", - "*E*Extra items*right*", - "*E*'y'*", - ] - ) + ) + + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + f"*assert x {op} y*", + "*E*Both sets are equal*", + ] + ) + + def test_pytest_assertrepr_compare_integration(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + "*= 1 failed in*", + ] + ) def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: