|
17 | 17 | from _pytest.assertion import truncate |
18 | 18 | from _pytest.assertion import util |
19 | 19 | from _pytest.assertion._compare_any import _compare_eq_cls |
| 20 | +from _pytest.assertion._diff import ndiff_too_slow |
20 | 21 | from _pytest.assertion.compare_text import _compare_eq_text |
21 | 22 | from _pytest.config import Config as _Config |
22 | 23 | from _pytest.monkeypatch import MonkeyPatch |
@@ -459,6 +460,19 @@ def callequal( |
459 | 460 | ) |
460 | 461 |
|
461 | 462 |
|
| 463 | +class TestNdiffTooSlow: |
| 464 | + """Heuristic guarding against pathologically slow diffs (#8998).""" |
| 465 | + |
| 466 | + def test_small_input_uses_ndiff(self) -> None: |
| 467 | + assert ndiff_too_slow(["spam"], ["eggs"]) is False |
| 468 | + |
| 469 | + def test_many_characters_is_too_slow(self) -> None: |
| 470 | + assert ndiff_too_slow(["a" * 6000], ["b" * 6000]) is True |
| 471 | + |
| 472 | + def test_many_lines_is_too_slow(self) -> None: |
| 473 | + assert ndiff_too_slow(["x"] * 1001, ["y"]) is True |
| 474 | + |
| 475 | + |
462 | 476 | class TestAssert_reprcompare: |
463 | 477 | def test_different_types(self) -> None: |
464 | 478 | assert callequal([0, 1], "foo") is None |
@@ -513,6 +527,32 @@ def test_text_skipping_verbose(self) -> None: |
513 | 527 | assert "- " + "a" * 50 + "eggs" in lines |
514 | 528 | assert "+ " + "a" * 50 + "spam" in lines |
515 | 529 |
|
| 530 | + def test_text_diff_large_input_skips_ndiff(self) -> None: |
| 531 | + # A single huge differing line is above the character cutoff and falls |
| 532 | + # back to a fast line-level diff instead of the pathologically slow |
| 533 | + # ndiff (#8998). |
| 534 | + left = "a" + "x" * 20000 |
| 535 | + right = "b" + "y" * 20000 |
| 536 | + lines = callequal(left, right, verbose=1) |
| 537 | + assert lines is not None |
| 538 | + assert any("Diff too large to compute in full" in line for line in lines) |
| 539 | + # The character-level "?" guide lines produced by ndiff must not appear. |
| 540 | + assert not any(line.startswith("? ") for line in lines) |
| 541 | + |
| 542 | + def test_text_diff_many_lines_skips_ndiff(self) -> None: |
| 543 | + # Many lines are above the line cutoff and fall back, capping the |
| 544 | + # number of lines actually diffed (#8998). |
| 545 | + left = "\n".join(f"left line {i}" for i in range(2000)) |
| 546 | + right = "\n".join(f"right line {i}" for i in range(2000)) |
| 547 | + lines = callequal(left, right, verbose=1) |
| 548 | + assert lines is not None |
| 549 | + assert any("Diff too large to compute in full" in line for line in lines) |
| 550 | + assert any("Diffing only the first 1000 lines" in line for line in lines) |
| 551 | + assert not any(line.startswith("? ") for line in lines) |
| 552 | + # The fallback still shows which lines differ. |
| 553 | + assert "-right line 0" in lines |
| 554 | + assert "+left line 0" in lines |
| 555 | + |
516 | 556 | def test_multiline_text_diff(self) -> None: |
517 | 557 | left = "foo\nspam\nbar" |
518 | 558 | right = "foo\neggs\nbar" |
@@ -673,6 +713,17 @@ def test_iterable_quiet(self) -> None: |
673 | 713 | "Use -v to get more diff", |
674 | 714 | ] |
675 | 715 |
|
| 716 | + def test_iterable_large_input_skips_ndiff(self) -> None: |
| 717 | + # Large iterables fall back to a fast line-level diff instead of the |
| 718 | + # pathologically slow ndiff over their pprint output (#8998). |
| 719 | + left = [f"item-{i}" for i in range(2000)] |
| 720 | + right = [f"other-{i}" for i in range(2000)] |
| 721 | + lines = callequal(left, right, verbose=1) |
| 722 | + assert lines is not None |
| 723 | + assert "Full diff:" in lines |
| 724 | + assert any("Diff too large to compute in full" in line for line in lines) |
| 725 | + assert not any(line.startswith("? ") for line in lines) |
| 726 | + |
676 | 727 | def test_iterable_full_diff_ci( |
677 | 728 | self, monkeypatch: MonkeyPatch, pytester: Pytester |
678 | 729 | ) -> None: |
|
0 commit comments