Skip to content

Commit 2197cfe

Browse files
committed
tests: Add ordered dict tests and benchmarks.
Signed-off-by: Andrew Leech <andrew.leech@planetinnovation.com.au>
1 parent 355e1d9 commit 2197cfe

15 files changed

Lines changed: 540 additions & 5 deletions

tests/basics/dict_compact_add.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Check if dicts preserve insertion order (MICROPY_PY_MAP_ORDERED).
2+
if list({2: 0, 1: 0, 3: 0}.keys()) != [2, 1, 3]:
3+
print("SKIP")
4+
raise SystemExit
5+
6+
7+
# Test that in-place compact triggered by add (dense array full with tombstones)
8+
# correctly preserves all entries and allows the new add to succeed.
9+
10+
# Fill a dict, delete entries to create tombstones, then add enough new entries
11+
# to trigger compact-on-add (wrap-around in hash probe). Verify all entries.
12+
d = {}
13+
for i in range(10):
14+
d[i] = i * 10
15+
16+
# Delete half the entries (creates tombstones in dense array).
17+
for i in range(0, 10, 2):
18+
del d[i]
19+
20+
# Add new entries. Eventually the dense array fills with tombstones + live entries,
21+
# triggering in-place compact on wrap-around instead of rehash.
22+
for i in range(100, 120):
23+
d[i] = i * 10
24+
25+
# Verify all expected keys are present with correct values.
26+
for i in range(1, 10, 2):
27+
assert d[i] == i * 10, "original key {} has wrong value".format(i)
28+
for i in range(100, 120):
29+
assert d[i] == i * 10, "new key {} has wrong value".format(i)
30+
31+
# Verify deleted keys are gone.
32+
for i in range(0, 10, 2):
33+
assert i not in d, "deleted key {} still present".format(i)
34+
35+
print("compact-on-add: OK")
36+
print("len:", len(d))
37+
38+
# Test insertion order is preserved after compact-on-add.
39+
keys = list(d.keys())
40+
expected = [1, 3, 5, 7, 9] + list(range(100, 120))
41+
assert keys == expected, "order wrong: {}".format(keys)
42+
print("order: OK")
43+
44+
45+
# Test with non-qstr keys (tuples have __hash__ via mp_unary_op).
46+
d2 = {}
47+
for i in range(8):
48+
d2[(i, "x")] = i
49+
50+
for i in range(0, 8, 2):
51+
del d2[(i, "x")]
52+
53+
for i in range(100, 110):
54+
d2[(i, "x")] = i
55+
56+
# Verify all entries.
57+
for i in range(1, 8, 2):
58+
assert d2[(i, "x")] == i
59+
for i in range(100, 110):
60+
assert d2[(i, "x")] == i
61+
62+
print("non-qstr keys: OK")
63+
64+
65+
# Test hash index rebuild correctness after compact.
66+
# After compact, all probe chains in the hash index are rebuilt from scratch.
67+
# Use enough entries that the hash index is stressed across multiple probe steps.
68+
d3 = {}
69+
for i in range(12):
70+
d3[i] = i
71+
72+
for i in [0, 1, 2, 3]:
73+
del d3[i]
74+
75+
# Adding 10 more entries forces the dense array to fill and triggers compact.
76+
# The rebuilt hash index must correctly resolve all existing keys.
77+
for i in range(20, 30):
78+
d3[i] = i
79+
80+
for i in range(4, 12):
81+
assert d3[i] == i, "key {} not found after hash rebuild".format(i)
82+
for i in range(20, 30):
83+
assert d3[i] == i, "new key {} not found after hash rebuild".format(i)
84+
for i in range(0, 4):
85+
assert i not in d3
86+
87+
print("hash index rebuild: OK")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Check if dicts preserve insertion order (MICROPY_PY_MAP_ORDERED).
2+
if list({2: 0, 1: 0, 3: 0}.keys()) != [2, 1, 3]:
3+
print("SKIP")
4+
raise SystemExit
5+
6+
7+
# Test dict.popitem() returns items in LIFO order and leaves the dict consistent.
8+
9+
# Pop entire dict, verify all entries are returned in LIFO order.
10+
d = {}
11+
for i in range(5):
12+
d[i] = i * 10
13+
items = []
14+
while d:
15+
items.append(d.popitem())
16+
print("all items:", items)
17+
18+
# Interleaved popitem and add: verify remaining dict is consistent.
19+
d = {1: 10, 2: 20, 3: 30}
20+
print(d.popitem())
21+
d[4] = 40
22+
d[5] = 50
23+
print(d.popitem())
24+
print(d.popitem())
25+
print("remaining:", sorted(d.items()))
26+
27+
# popitem after del (mixed operations).
28+
d = {}
29+
for i in range(8):
30+
d[i] = i
31+
del d[3]
32+
del d[5]
33+
# Remaining: 0,1,2,4,6,7 -- popitem should pop from end.
34+
p1 = d.popitem()
35+
p2 = d.popitem()
36+
print("after del pops:", p1, p2)
37+
# Verify remaining keys are accessible.
38+
for k in list(d.keys()):
39+
assert d[k] == k, "key {} has wrong value after mixed ops".format(k)
40+
print("remaining keys:", sorted(d.keys()))
41+
42+
# Pop everything after deletes.
43+
d = {1: 10, 2: 20, 3: 30, 4: 40, 5: 50}
44+
del d[2]
45+
del d[4]
46+
items = []
47+
while d:
48+
items.append(d.popitem())
49+
print("pop-all after del:", items)
50+
51+
# Verify popitem on empty dict raises KeyError.
52+
try:
53+
{}.popitem()
54+
except KeyError:
55+
print("empty popitem: KeyError")

tests/basics/slice_optimise.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
except KeyError as e:
1717
print("KeyError", e.args)
1818

19-
# Verify slice-as-key in OrderedDict works or raises TypeError depending
20-
# on whether the ordered hash table is enabled (slices are not hashable).
19+
# Slice-as-key in OrderedDict: when backed by the ordered hash table
20+
# (MICROPY_PY_MAP_ORDERED=1) slices are not hashable so TypeError is raised.
21+
# When backed by linear scan (MAP_ORDERED=0) slices work as keys.
2122
x = OrderedDict()
2223
try:
2324
x[:"a"] = 1
2425
x["b"] = 2
26+
# Linear scan path: verify keys and values are correct.
27+
assert list(x.keys()) == [slice(None, "a", None), "b"]
28+
assert list(x.values()) == [1, 2]
29+
print("slice key OK")
2530
except TypeError:
26-
pass
27-
print("OrderedDict slice test OK")
31+
# Hash table path: slices not hashable, this is expected.
32+
print("slice key TypeError")

tests/basics/slice_optimise.py.exp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
KeyError (slice(None, None, None),)
2-
OrderedDict slice test OK
2+
slice key TypeError
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Test that deleting the last element doesn't cause issues.
2+
# Compaction is skipped when dict becomes empty (filled == 0).
3+
4+
# Test 1: Delete all entries one by one
5+
d = {1: "a", 2: "b", 3: "c"}
6+
del d[1]
7+
del d[2]
8+
del d[3]
9+
print(len(d))
10+
11+
# Dict should work normally after being emptied
12+
d[10] = "x"
13+
print(d[10])
14+
print(len(d))
15+
16+
# Test 2: Single element dict
17+
d2 = {42: "only"}
18+
del d2[42]
19+
print(len(d2))
20+
d2[100] = "new"
21+
print(d2[100])
22+
23+
# Test 3: Empty via popitem
24+
d3 = {"a": 1, "b": 2}
25+
d3.popitem()
26+
d3.popitem()
27+
print(len(d3))
28+
d3["c"] = 3
29+
print(d3["c"])
30+
31+
# Test 4: Repeated empty/fill cycles
32+
d4 = {}
33+
for cycle in range(5):
34+
for i in range(10):
35+
d4[i] = cycle
36+
for i in range(10):
37+
del d4[i]
38+
print("cycles OK, len:", len(d4))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
0
2+
x
3+
1
4+
0
5+
new
6+
0
7+
3
8+
cycles OK, len: 0
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Check if dicts preserve insertion order (MICROPY_PY_MAP_ORDERED).
2+
if list({2: 0, 1: 0, 3: 0}.keys()) != [2, 1, 3]:
3+
print("SKIP")
4+
raise SystemExit
5+
6+
7+
# Test that dict ordering is preserved after compaction.
8+
# When tombstones exceed 50% of live entries, the dict compacts.
9+
# This must preserve insertion order.
10+
11+
d = {}
12+
for i in range(100):
13+
d[i] = i
14+
15+
# Delete 67 entries: filled=33, tombstones=67
16+
# Threshold = 33/2 = 16, and 67 > 16, so compaction triggers
17+
for i in range(67):
18+
del d[i]
19+
20+
# Remaining keys must be in insertion order
21+
print(list(d.keys()))
22+
23+
# Values should also be correct (spot check)
24+
print(d[67], d[80], d[99])
25+
26+
# Dict should still be usable after compaction
27+
d[200] = 200
28+
d[201] = 201
29+
keys = list(d.keys())
30+
print(keys[-3], keys[-2], keys[-1])
31+
32+
# Test with mixed key types (strings and ints)
33+
d2 = {}
34+
for i in range(20):
35+
d2[i] = i
36+
d2["key" + str(i)] = "val" + str(i)
37+
38+
# Delete enough to trigger compaction
39+
for i in range(15):
40+
del d2[i]
41+
del d2["key" + str(i)]
42+
43+
# Remaining should preserve insertion order
44+
print(list(d2.keys()))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
2+
67 80 99
3+
99 200 201
4+
[15, 'key15', 16, 'key16', 17, 'key17', 18, 'key18', 19, 'key19']
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Stress test for dict compaction.
2+
# Without compaction, repeated add/delete cycles would cause unbounded
3+
# memory growth from tombstone accumulation.
4+
5+
try:
6+
import gc
7+
except ImportError:
8+
print("SKIP")
9+
raise SystemExit
10+
11+
d = {}
12+
13+
# Run many add/delete cycles
14+
# Without compaction, this would accumulate 50 * 100 = 5000 tombstones
15+
for cycle in range(50):
16+
# Add entries
17+
for i in range(100):
18+
d[i] = i * cycle
19+
20+
# Delete all entries
21+
for i in range(100):
22+
del d[i]
23+
24+
# If we got here without MemoryError, basic compaction is working
25+
print("pass1")
26+
27+
# Test with popitem too
28+
d2 = {}
29+
for cycle in range(50):
30+
for i in range(100):
31+
d2[i] = i
32+
for _ in range(100):
33+
d2.popitem()
34+
35+
print("pass2")
36+
37+
# Test mixed operations
38+
d3 = {}
39+
for cycle in range(30):
40+
# Add 200 entries
41+
for i in range(200):
42+
d3[i] = i
43+
44+
# Delete half via del
45+
for i in range(100):
46+
del d3[i]
47+
48+
# Delete rest via popitem
49+
for _ in range(100):
50+
d3.popitem()
51+
52+
print("pass3")
53+
54+
# Force GC and verify we haven't leaked too much memory
55+
gc.collect()
56+
print("pass4")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pass1
2+
pass2
3+
pass3
4+
pass4

0 commit comments

Comments
 (0)