Skip to content

Commit eb6dbd7

Browse files
[3.13] Add regression test for add() after remove() with hash collision in set (GH-143781) (GH-143859)
(cherry picked from commit 565685f)
1 parent 2516dd0 commit eb6dbd7

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

Lib/test/test_dict.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@
1111
from test.support import import_helper, get_c_recursion_limit
1212

1313

14+
class CustomHash:
15+
def __init__(self, hash):
16+
self.hash = hash
17+
def __hash__(self):
18+
return self.hash
19+
def __repr__(self):
20+
return f'<CustomHash {self.hash} at {id(self):#x}>'
21+
22+
1423
class DictTest(unittest.TestCase):
1524

1625
def test_invalid_keyword_arguments(self):
@@ -1701,6 +1710,29 @@ class MyClass: pass
17011710
d[MyStr("attr1")] = 2
17021711
self.assertIsInstance(list(d)[0], MyStr)
17031712

1713+
def test_hash_collision_remove_add(self):
1714+
self.maxDiff = None
1715+
# There should be enough space, so all elements with unique hash
1716+
# will be placed in corresponding cells without collision.
1717+
n = 64
1718+
items = [(CustomHash(h), h) for h in range(n)]
1719+
# Keys with hash collision.
1720+
a = CustomHash(n)
1721+
b = CustomHash(n)
1722+
items += [(a, 'a'), (b, 'b')]
1723+
d = dict(items)
1724+
self.assertEqual(len(d), len(items), d)
1725+
del d[a]
1726+
# "a" has been replaced with a dummy.
1727+
del items[n]
1728+
self.assertEqual(len(d), len(items), d)
1729+
self.assertEqual(d, dict(items))
1730+
d[b] = 'c'
1731+
# "b" should not replace the dummy.
1732+
items[n] = (b, 'c')
1733+
self.assertEqual(len(d), len(items), d)
1734+
self.assertEqual(d, dict(items))
1735+
17041736

17051737
class CAPITest(unittest.TestCase):
17061738

Lib/test/test_set.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def check_pass_thru():
1919
raise PassThru
2020
yield 1
2121

22+
class CustomHash:
23+
def __init__(self, hash):
24+
self.hash = hash
25+
def __hash__(self):
26+
return self.hash
27+
def __repr__(self):
28+
return f'<CustomHash {self.hash} at {id(self):#x}>'
29+
2230
class BadCmp:
2331
def __hash__(self):
2432
return 1
@@ -635,6 +643,38 @@ def __le__(self, some_set):
635643
myset >= myobj
636644
self.assertTrue(myobj.le_called)
637645

646+
def test_set_membership(self):
647+
myfrozenset = frozenset(range(3))
648+
myset = {myfrozenset, "abc", 1}
649+
self.assertIn(set(range(3)), myset)
650+
self.assertNotIn(set(range(1)), myset)
651+
myset.discard(set(range(3)))
652+
self.assertEqual(myset, {"abc", 1})
653+
self.assertRaises(KeyError, myset.remove, set(range(1)))
654+
self.assertRaises(KeyError, myset.remove, set(range(3)))
655+
656+
def test_hash_collision_remove_add(self):
657+
self.maxDiff = None
658+
# There should be enough space, so all elements with unique hash
659+
# will be placed in corresponding cells without collision.
660+
n = 64
661+
elems = [CustomHash(h) for h in range(n)]
662+
# Elements with hash collision.
663+
a = CustomHash(n)
664+
b = CustomHash(n)
665+
elems += [a, b]
666+
s = self.thetype(elems)
667+
self.assertEqual(len(s), len(elems), s)
668+
s.remove(a)
669+
# "a" has been replaced with a dummy.
670+
del elems[n]
671+
self.assertEqual(len(s), len(elems), s)
672+
self.assertEqual(s, set(elems))
673+
s.add(b)
674+
# "b" should not replace the dummy.
675+
self.assertEqual(len(s), len(elems), s)
676+
self.assertEqual(s, set(elems))
677+
638678

639679
class SetSubclass(set):
640680
pass

0 commit comments

Comments
 (0)