Skip to content

Commit ad137a3

Browse files
authored
Fix the mutation API to maintain elements count correctly (#25, #24)
1 parent 11863b2 commit ad137a3

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

immutables/_map.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1806,6 +1806,7 @@ map_node_array_assoc(MapNode_Array *self,
18061806

18071807
if (mutid != 0 && self->a_mutid == mutid) {
18081808
new_node = self;
1809+
self->a_count++; /*must update count*/
18091810
Py_INCREF(self);
18101811
}
18111812
else {
@@ -1940,9 +1941,9 @@ map_node_array_without(MapNode_Array *self,
19401941
if (target == NULL) {
19411942
return W_ERROR;
19421943
}
1943-
target->a_count = new_count;
19441944
}
19451945

1946+
target->a_count = new_count;
19461947
Py_CLEAR(target->a_array[idx]);
19471948

19481949
*new_node = (MapNode*)target; /* borrow */

tests/test_issue24.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import unittest
2+
3+
from immutables.map import Map as PyMap, map_bitcount
4+
5+
6+
class CollisionKey:
7+
def __hash__(self):
8+
return 0
9+
10+
11+
class Issue24Base:
12+
Map = None
13+
14+
def test_issue24(self):
15+
keys = range(27)
16+
new_entries = dict.fromkeys(keys, True)
17+
m = self.Map(new_entries)
18+
self.assertTrue(17 in m)
19+
with m.mutate() as mm:
20+
for i in keys:
21+
del mm[i]
22+
self.assertEqual(len(mm), 0)
23+
24+
def dump_check_node_kind(self, header, kind):
25+
header = header.strip()
26+
self.assertTrue(header.strip().startswith(kind))
27+
28+
def dump_check_node_size(self, header, size):
29+
node_size = header.split('size=', 1)[1]
30+
node_size = int(node_size.split(maxsplit=1)[0])
31+
self.assertEqual(node_size, size)
32+
33+
def dump_check_bitmap_count(self, header, count):
34+
header = header.split('bitmap=')[1]
35+
bitmap = int(header.split(maxsplit=1)[0], 0)
36+
self.assertEqual(map_bitcount(bitmap), count)
37+
38+
def dump_check_bitmap_node_count(self, header, count):
39+
self.dump_check_node_kind(header, 'Bitmap')
40+
self.dump_check_node_size(header, count * 2)
41+
self.dump_check_bitmap_count(header, count)
42+
43+
def dump_check_collision_node_count(self, header, count):
44+
self.dump_check_node_kind(header, 'Collision')
45+
self.dump_check_node_size(header, 2 * count)
46+
47+
def test_bitmap_node_update_in_place_count(self):
48+
keys = range(7)
49+
new_entries = dict.fromkeys(keys, True)
50+
m = self.Map(new_entries)
51+
d = m.__dump__().splitlines()
52+
self.assertTrue(d)
53+
if d[0].startswith('HAMT'):
54+
header = d[1] # skip _map.Map.__dump__() header
55+
else:
56+
header = d[0]
57+
self.dump_check_bitmap_node_count(header, 7)
58+
59+
def test_bitmap_node_delete_in_place_count(self):
60+
keys = range(7)
61+
new_entries = dict.fromkeys(keys, True)
62+
m = self.Map(new_entries)
63+
with m.mutate() as mm:
64+
del mm[0], mm[2], mm[3]
65+
m2 = mm.finish()
66+
d = m2.__dump__().splitlines()
67+
self.assertTrue(d)
68+
if d[0].startswith('HAMT'):
69+
header = d[1] # skip _map.Map.__dump__() header
70+
else:
71+
header = d[0]
72+
self.dump_check_bitmap_node_count(header, 4)
73+
74+
def test_collision_node_update_in_place_count(self):
75+
keys = (CollisionKey() for i in range(7))
76+
new_entries = dict.fromkeys(keys, True)
77+
m = self.Map(new_entries)
78+
d = m.__dump__().splitlines()
79+
self.assertTrue(len(d) > 3)
80+
# get node headers
81+
if d[0].startswith('HAMT'):
82+
h1, h2 = d[1], d[3] # skip _map.Map.__dump__() header
83+
else:
84+
h1, h2 = d[0], d[2]
85+
self.dump_check_node_kind(h1, 'Bitmap')
86+
self.dump_check_collision_node_count(h2, 7)
87+
88+
def test_collision_node_delete_in_place_count(self):
89+
keys = [CollisionKey() for i in range(7)]
90+
new_entries = dict.fromkeys(keys, True)
91+
m = self.Map(new_entries)
92+
with m.mutate() as mm:
93+
del mm[keys[0]], mm[keys[2]], mm[keys[3]]
94+
m2 = mm.finish()
95+
d = m2.__dump__().splitlines()
96+
self.assertTrue(len(d) > 3)
97+
# get node headers
98+
if d[0].startswith('HAMT'):
99+
h1, h2 = d[1], d[3] # skip _map.Map.__dump__() header
100+
else:
101+
h1, h2 = d[0], d[2]
102+
self.dump_check_node_kind(h1, 'Bitmap')
103+
self.dump_check_collision_node_count(h2, 4)
104+
105+
try:
106+
from immutables._map import Map as CMap
107+
except ImportError:
108+
CMap = None
109+
110+
111+
class Issue24PyTest(Issue24Base, unittest.TestCase):
112+
Map = PyMap
113+
114+
115+
@unittest.skipIf(CMap is None, 'C Map is not available')
116+
class Issue24CTest(Issue24Base, unittest.TestCase):
117+
Map = CMap
118+
119+
def hamt_dump_check_first_return_second(self, m):
120+
d = m.__dump__().splitlines()
121+
self.assertTrue(len(d) > 2)
122+
self.assertTrue(d[0].startswith('HAMT'))
123+
return d[1]
124+
125+
def test_array_node_update_in_place_count(self):
126+
keys = range(27)
127+
new_entries = dict.fromkeys(keys, True)
128+
m = self.Map(new_entries)
129+
header = self.hamt_dump_check_first_return_second(m)
130+
self.dump_check_node_kind(header, 'Array')
131+
for i in range(2, 18):
132+
m = m.delete(i)
133+
header = self.hamt_dump_check_first_return_second(m)
134+
self.dump_check_bitmap_node_count(header, 11)
135+
136+
def test_array_node_delete_in_place_count(self):
137+
keys = range(27)
138+
new_entries = dict.fromkeys(keys, True)
139+
m = self.Map(new_entries)
140+
header = self.hamt_dump_check_first_return_second(m)
141+
self.dump_check_node_kind(header, 'Array')
142+
with m.mutate() as mm:
143+
for i in range(5):
144+
del mm[i]
145+
m2 = mm.finish()
146+
header = self.hamt_dump_check_first_return_second(m2)
147+
self.dump_check_node_kind(header, 'Array')
148+
for i in range(6, 17):
149+
m2 = m2.delete(i)
150+
header = self.hamt_dump_check_first_return_second(m2)
151+
self.dump_check_bitmap_node_count(header, 11)
152+
153+
154+
if __name__ == '__main__':
155+
unittest.main()

0 commit comments

Comments
 (0)