Files
troupe-standup/wizard-work/beam/test_segment_tree.py

197 lines
7.1 KiB
Python

"""Cells for segment_tree.SegmentTree (Q77).
Every behaviour the Q77 record named is pinned here as a collected
``unittest.TestCase`` method, so ``unittest discover -p 'test_*.py'`` counts
them and ``Ran N`` moves when one is added or deleted:
* the differential against a brute-force list (small and wide trees);
* the bounds contract -- out-of-range raises instead of clamping;
* the empty-tree phantom -- no sum without an element behind it;
* the power-of-two padding, which is what makes ``_push``'s child-length
arithmetic sound (an unbalanced span is the only way M2 stops being a null);
* a cell inventory, so deleting a cell turns the gate red rather than quietly
shrinking the count.
"""
import random
import sys
import unittest
from segment_tree import SegmentTree
class TestRandomDifferential(unittest.TestCase):
"""The primitive against a list: every add and every sum, plus the whole array."""
def test_random_ops_match_brute_list(self):
rng = random.Random(42)
for trial in range(400):
n = rng.randint(1, 12)
data = [rng.randint(-5, 5) for _ in range(n)]
st = SegmentTree(data)
arr = list(data)
for _ in range(40):
if rng.random() < 0.5:
l = rng.randint(0, n - 1)
r = rng.randint(l, n - 1)
v = rng.randint(-5, 5)
st.range_add(l, r, v)
for i in range(l, r + 1):
arr[i] += v
else:
l = rng.randint(0, n - 1)
r = rng.randint(l, n - 1)
self.assertEqual(
st.range_sum(l, r), sum(arr[l:r + 1]),
msg=f"trial {trial} sum[{l},{r}]",
)
self.assertEqual(st.to_list(), arr, msg=f"trial {trial} list")
def test_wide_tree_matches_brute_list(self):
rng = random.Random(7)
n = 1000
data = [rng.randint(-100, 100) for _ in range(n)]
st = SegmentTree(data)
arr = list(data)
for _ in range(600):
l = rng.randint(0, n - 1)
r = rng.randint(l, n - 1)
v = rng.randint(-50, 50)
st.range_add(l, r, v)
for i in range(l, r + 1):
arr[i] += v
self.assertEqual(st.to_list(), arr)
self.assertEqual(st.range_sum(0, n - 1), sum(arr))
class TestBoundsContract(unittest.TestCase):
"""Out-of-range raises IndexError; nothing is silently clamped."""
def test_right_edge_out_of_range_raises(self):
st = SegmentTree([1, 2, 3])
with self.assertRaises(IndexError):
st.range_sum(0, 3)
with self.assertRaises(IndexError):
st.range_sum(3, 3)
with self.assertRaises(IndexError):
st.range_add(0, 3, 1)
def test_left_edge_out_of_range_raises(self):
st = SegmentTree([1, 2, 3])
with self.assertRaises(IndexError):
st.range_sum(-1, 1)
with self.assertRaises(IndexError):
st.range_add(-5, 10, 1)
with self.assertRaises(IndexError):
st.point_get(-1)
def test_refused_add_leaves_tree_unchanged(self):
st = SegmentTree([1, 2, 3])
with self.assertRaises(IndexError):
st.range_add(-5, 10, 1)
self.assertEqual(st.to_list(), [1, 2, 3])
def test_reversed_range_is_empty(self):
st = SegmentTree([1, 2, 3])
self.assertEqual(st.range_sum(2, 1), 0)
st.range_add(2, 1, 5)
self.assertEqual(st.to_list(), [1, 2, 3])
class TestEmptyTree(unittest.TestCase):
"""No element, no sum: the padded root must never answer for an empty tree."""
def test_empty_tree_rejects_range_sum(self):
st = SegmentTree([])
with self.assertRaises(IndexError):
st.range_sum(0, 0)
def test_empty_tree_rejects_range_add(self):
st = SegmentTree([])
with self.assertRaises(IndexError):
st.range_add(0, 0, 5)
self.assertEqual(st.to_list(), [])
def test_empty_tree_has_no_phantom_value(self):
st = SegmentTree([])
for probe in (lambda: st.range_sum(0, 0), lambda: st.point_get(0)):
try:
got = probe()
except IndexError:
continue
self.fail(f"empty tree answered {got!r} where there is no element")
class TestPaddingInvariant(unittest.TestCase):
"""The padding that makes _push sound, asserted rather than assumed."""
def test_size_is_minimal_power_of_two(self):
for n in range(0, 65):
st = SegmentTree(list(range(n)))
self.assertGreaterEqual(st.size, max(1, n), msg=f"n={n}")
self.assertEqual(st.size & (st.size - 1), 0, msg=f"n={n} not a power of two")
if st.size > 1:
self.assertLess(st.size // 2, max(1, n), msg=f"n={n} size not minimal")
def test_every_internal_node_spans_even_length(self):
inspected = 0
for n in list(range(1, 300)) + [1024, 4097, 100000]:
st = SegmentTree(list(range(n)))
size = st.size
self.assertEqual(size & (size - 1), 0, msg=f"n={n}")
for node in range(1, size):
span = size >> (node.bit_length() - 1)
inspected += 1
self.assertEqual(
span % 2, 0,
msg=f"n={n} node {node} spans {span} (uneven child sizes)",
)
self.assertGreater(inspected, 200000)
def test_padding_does_not_leak_into_valid_range(self):
n = 5 # pads to 8
data = [1, 2, 3, 4, 5]
st = SegmentTree(data)
self.assertEqual(st.size, 8)
st.range_add(0, n - 1, 7)
self.assertEqual(st.range_sum(0, n - 1), sum(data) + 7 * n)
self.assertEqual(st.to_list(), [x + 7 for x in data])
class TestLazyCorrectness(unittest.TestCase):
"""Hand-checked cases for the push path."""
def test_overlapping_range_adds(self):
st = SegmentTree([0, 0, 0, 0, 0])
st.range_add(0, 2, 1)
st.range_add(1, 3, 10)
st.range_add(2, 4, 100)
self.assertEqual(st.to_list(), [1, 11, 111, 110, 100])
self.assertEqual(st.range_sum(1, 3), 232)
self.assertEqual(st.range_sum(2, 2), 111)
def test_single_element_tree(self):
st = SegmentTree([5])
self.assertEqual(st.range_sum(0, 0), 5)
st.range_add(0, 0, -2)
self.assertEqual(st.point_get(0), 3)
with self.assertRaises(IndexError):
st.range_sum(0, 1)
class TestCellInventory(unittest.TestCase):
"""A deleted cell must turn the gate red, not just shrink Ran N."""
MIN_CELLS = 15
def test_no_cell_was_deleted(self):
module = sys.modules[__name__]
names = set()
for obj in vars(module).values():
if isinstance(obj, type) and issubclass(obj, unittest.TestCase):
names.update(n for n in dir(obj) if n.startswith("test_"))
self.assertGreaterEqual(
len(names), self.MIN_CELLS,
msg=f"only {len(names)} cells present; {self.MIN_CELLS} expected",
)