|
5 | 5 | import doctest |
6 | 6 |
|
7 | 7 | from test.support import import_helper |
8 | | -from unittest import TestCase, skipUnless |
| 8 | +from unittest import TestCase, mock, skipUnless |
9 | 9 | from operator import itemgetter |
10 | 10 |
|
11 | 11 | py_heapq = import_helper.import_fresh_module('heapq', blocked=['_heapq']) |
@@ -637,5 +637,178 @@ class TestErrorHandlingC(TestErrorHandling, TestCase): |
637 | 637 | module = c_heapq |
638 | 638 |
|
639 | 639 |
|
| 640 | +#============================================================================== |
| 641 | +# Object-oriented interface: MinHeap and MaxHeap. |
| 642 | +# |
| 643 | +# The classes add no new heap algorithm; they are thin wrappers that delegate |
| 644 | +# to the module-level functions. They are tested in two ways: |
| 645 | +# |
| 646 | +# * Functional "sanity check" tests, run against both the pure-Python and the |
| 647 | +# C classes (like the rest of this file). They are end-to-end, so a |
| 648 | +# mis-wired method (say, a MaxHeap that pushed with the min-heap routine) |
| 649 | +# changes the observable output and is caught even for the C class. |
| 650 | +# |
| 651 | +# * Delegation tests that patch the module-level functions and check that each |
| 652 | +# method forwards to the right one. These only apply to the pure-Python |
| 653 | +# class, since the C class calls the underlying routines directly and cannot |
| 654 | +# be intercepted; its wiring is covered by the functional tests instead. |
| 655 | + |
| 656 | + |
| 657 | +class HeapClassSanityTest: |
| 658 | + """Functional sanity checks shared by the MinHeap/MaxHeap test cases. |
| 659 | +
|
| 660 | + Subclasses set: |
| 661 | +
|
| 662 | + * *cls* -- the heap class under test (e.g. ``py_heapq.MinHeap``) |
| 663 | + * *ordered* -- a small list already satisfying the heap invariant |
| 664 | + * *reverse* -- ``True`` for max-heaps (the order ``pop()`` yields items in) |
| 665 | + """ |
| 666 | + |
| 667 | + def in_heap_order(self, data): |
| 668 | + return sorted(data, reverse=self.reverse) |
| 669 | + |
| 670 | + def test_construct_empty(self): |
| 671 | + h = self.cls() |
| 672 | + self.assertEqual(len(h), 0) |
| 673 | + self.assertFalse(h) |
| 674 | + |
| 675 | + def test_construct_from_iterable(self): |
| 676 | + data = [random.randrange(1000) for _ in range(100)] |
| 677 | + # Accept any iterable, not just a sequence. |
| 678 | + h = self.cls(iter(data)) |
| 679 | + self.assertEqual(len(h), len(data)) |
| 680 | + drained = [h.pop() for _ in range(len(data))] |
| 681 | + self.assertEqual(drained, self.in_heap_order(data)) |
| 682 | + |
| 683 | + def test_push_then_pop_sorts(self): |
| 684 | + data = [random.randrange(1000) for _ in range(100)] |
| 685 | + h = self.cls() |
| 686 | + for item in data: |
| 687 | + self.assertIsNone(h.push(item)) |
| 688 | + self.assertEqual(len(h), len(data)) |
| 689 | + drained = [h.pop() for _ in range(len(data))] |
| 690 | + self.assertEqual(drained, self.in_heap_order(data)) |
| 691 | + |
| 692 | + def test_len_and_bool(self): |
| 693 | + h = self.cls() |
| 694 | + self.assertEqual(len(h), 0) |
| 695 | + self.assertFalse(h) |
| 696 | + h.push(1) |
| 697 | + self.assertEqual(len(h), 1) |
| 698 | + self.assertTrue(h) |
| 699 | + |
| 700 | + def test_pop_empty_raises(self): |
| 701 | + h = self.cls() |
| 702 | + self.assertRaises(IndexError, h.pop) |
| 703 | + |
| 704 | + def test_pushpop(self): |
| 705 | + h = self.cls(self.ordered) |
| 706 | + result = h.pushpop(2) |
| 707 | + expected = self.in_heap_order(list(self.ordered) + [2])[0] |
| 708 | + self.assertEqual(result, expected) |
| 709 | + self.assertEqual(len(h), len(self.ordered)) |
| 710 | + |
| 711 | + def test_replace(self): |
| 712 | + h = self.cls(self.ordered) |
| 713 | + expected = self.in_heap_order(self.ordered)[0] |
| 714 | + new_item = -1000 if self.reverse else 1000 |
| 715 | + self.assertEqual(h.replace(new_item), expected) |
| 716 | + self.assertEqual(len(h), len(self.ordered)) |
| 717 | + |
| 718 | + def test_repr(self): |
| 719 | + h = self.cls(self.ordered) |
| 720 | + self.assertEqual(repr(h), f'{self.cls.__name__}({self.ordered!r})') |
| 721 | + |
| 722 | + |
| 723 | +class TestMinHeapPython(HeapClassSanityTest, TestCase): |
| 724 | + cls = py_heapq.MinHeap |
| 725 | + ordered = [1, 2, 3] |
| 726 | + reverse = False |
| 727 | + |
| 728 | + |
| 729 | +@skipUnless(c_heapq, 'requires _heapq') |
| 730 | +class TestMinHeapC(HeapClassSanityTest, TestCase): |
| 731 | + cls = c_heapq.MinHeap if c_heapq else None |
| 732 | + ordered = [1, 2, 3] |
| 733 | + reverse = False |
| 734 | + |
| 735 | + |
| 736 | +class TestMaxHeapPython(HeapClassSanityTest, TestCase): |
| 737 | + cls = py_heapq.MaxHeap |
| 738 | + ordered = [3, 2, 1] |
| 739 | + reverse = True |
| 740 | + |
| 741 | + |
| 742 | +@skipUnless(c_heapq, 'requires _heapq') |
| 743 | +class TestMaxHeapC(HeapClassSanityTest, TestCase): |
| 744 | + cls = c_heapq.MaxHeap if c_heapq else None |
| 745 | + ordered = [3, 2, 1] |
| 746 | + reverse = True |
| 747 | + |
| 748 | + |
| 749 | +class HeapClassDelegationTest: |
| 750 | + """Check that each method forwards to the right module-level function. |
| 751 | +
|
| 752 | + Delegation only makes sense for the pure-Python class, so these tests |
| 753 | + always patch ``py_heapq``. |
| 754 | +
|
| 755 | + Subclasses set *cls* and the names of the functions each method is |
| 756 | + expected to forward to (*heapify_func*, *push_func*, *pop_func*, |
| 757 | + *pushpop_func* and *replace_func*). |
| 758 | + """ |
| 759 | + |
| 760 | + def test_init_delegates_to_heapify(self): |
| 761 | + with mock.patch.object(py_heapq, self.heapify_func) as m: |
| 762 | + h = self.cls([3, 1, 2]) |
| 763 | + m.assert_called_once_with(h._queue) |
| 764 | + |
| 765 | + def test_init_empty_does_not_heapify(self): |
| 766 | + with mock.patch.object(py_heapq, self.heapify_func) as m: |
| 767 | + self.cls() |
| 768 | + m.assert_not_called() |
| 769 | + |
| 770 | + def test_push_delegates(self): |
| 771 | + h = self.cls() |
| 772 | + with mock.patch.object(py_heapq, self.push_func) as m: |
| 773 | + h.push(42) |
| 774 | + m.assert_called_once_with(h._queue, 42) |
| 775 | + |
| 776 | + def test_pop_delegates(self): |
| 777 | + h = self.cls([1, 2, 3]) |
| 778 | + with mock.patch.object(py_heapq, self.pop_func) as m: |
| 779 | + h.pop() |
| 780 | + m.assert_called_once_with(h._queue) |
| 781 | + |
| 782 | + def test_pushpop_delegates(self): |
| 783 | + h = self.cls([1, 2, 3]) |
| 784 | + with mock.patch.object(py_heapq, self.pushpop_func) as m: |
| 785 | + h.pushpop(42) |
| 786 | + m.assert_called_once_with(h._queue, 42) |
| 787 | + |
| 788 | + def test_replace_delegates(self): |
| 789 | + h = self.cls([1, 2, 3]) |
| 790 | + with mock.patch.object(py_heapq, self.replace_func) as m: |
| 791 | + h.replace(42) |
| 792 | + m.assert_called_once_with(h._queue, 42) |
| 793 | + |
| 794 | + |
| 795 | +class TestMinHeapDelegation(HeapClassDelegationTest, TestCase): |
| 796 | + cls = py_heapq.MinHeap |
| 797 | + heapify_func = 'heapify' |
| 798 | + push_func = 'heappush' |
| 799 | + pop_func = 'heappop' |
| 800 | + pushpop_func = 'heappushpop' |
| 801 | + replace_func = 'heapreplace' |
| 802 | + |
| 803 | + |
| 804 | +class TestMaxHeapDelegation(HeapClassDelegationTest, TestCase): |
| 805 | + cls = py_heapq.MaxHeap |
| 806 | + heapify_func = 'heapify_max' |
| 807 | + push_func = 'heappush_max' |
| 808 | + pop_func = 'heappop_max' |
| 809 | + pushpop_func = 'heappushpop_max' |
| 810 | + replace_func = 'heapreplace_max' |
| 811 | + |
| 812 | + |
640 | 813 | if __name__ == "__main__": |
641 | 814 | unittest.main() |
0 commit comments