"""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", )