Skip to content

Commit cb477f0

Browse files
[3.13] Add regression test for add() after remove() with hash collision in set (pythonGH-143781)
(cherry picked from commit 565685f) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 2516dd0 commit cb477f0

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import copy
88
import pickle
99
from random import randrange, shuffle
10+
import re
1011
import warnings
1112
import collections
1213
import collections.abc
@@ -19,6 +20,14 @@ def check_pass_thru():
1920
raise PassThru
2021
yield 1
2122

23+
class CustomHash:
24+
def __init__(self, hash):
25+
self.hash = hash
26+
def __hash__(self):
27+
return self.hash
28+
def __repr__(self):
29+
return f'<CustomHash {self.hash} at {id(self):#x}>'
30+
2231
class BadCmp:
2332
def __hash__(self):
2433
return 1
@@ -635,6 +644,38 @@ def __le__(self, some_set):
635644
myset >= myobj
636645
self.assertTrue(myobj.le_called)
637646

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

639680
class SetSubclass(set):
640681
pass

0 commit comments

Comments
 (0)