Files
troupe-standup/wizard-work/beam/test_dsu_rollback.py
T
ledger ca3e13d328 beam: Q94 bullets 2/3/4 -- three cells that each kill a named mutant
Rebuild test_no_path_compression_find_stable on 4 nodes so the node it asks
about (3) sits two levels down under union-by-size; the old cell built
union(0,1);union(1,2) -> parent [0,0,0,0], so find(2) rewrote nothing and a
path-compressing find passed the cell that names the mechanism.

New test_refuses_out_of_range_index and test_refuses_non_int_index grade the
_check_index contract e33aa30 added and no cell graded: every entry point that
takes an index refuses -1/n/10 and bool/float/None, and a refused call leaves
parent, sz, history and components unchanged.

Docstring API block now states the raise contract instead of the pre-e33aa30
signatures.

Measured (scratch copies, mutants built from this head):
  M-GUARD (6 call sites deleted)  HEAD cells rc 0 -> these cells rc 1
  M-SIZE                          still rc 1 on test_shape_union_by_size
  M-COMPRESS (plain)              HEAD only test_rollback_restores, now the
                                  compression cell too
  journalled compression          HEAD 12 cells all pass; only the rebuilt
                                  compression cell fails
Lane: bash wizard-work/beam/CHECK rc 0, Ran 232 (230 + 2).
2026-10-27 16:58:24 +00:00

165 lines
5.8 KiB
Python

import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import unittest
from dsu_rollback import DSU
class TestDSURollback(unittest.TestCase):
def test_basic_union_connected(self):
d = DSU(4)
self.assertTrue(d.union(0, 1))
self.assertTrue(d.connected(0, 1))
self.assertFalse(d.connected(0, 2))
self.assertEqual(d.size(0), 2)
def test_noop_union_returns_false_no_history(self):
d = DSU(3)
d.union(0, 1)
m = d.snapshot()
self.assertFalse(d.union(0, 1))
self.assertEqual(d.snapshot(), m)
def test_rollback_restores(self):
d = DSU(5)
d.union(0, 1); d.union(2, 3)
m = d.snapshot()
d.union(1, 2); d.union(3, 4)
self.assertTrue(d.connected(0, 4))
d.rollback(m)
self.assertFalse(d.connected(0, 4))
self.assertTrue(d.connected(0, 1))
self.assertTrue(d.connected(2, 3))
self.assertFalse(d.connected(1, 2))
def test_nested_snapshots(self):
d = DSU(6)
d.union(0, 1)
m1 = d.snapshot()
d.union(2, 3)
m2 = d.snapshot()
d.union(1, 2)
d.rollback(m2)
self.assertTrue(d.connected(0, 1) and d.connected(2, 3))
self.assertFalse(d.connected(0, 2))
d.rollback(m1)
self.assertTrue(d.connected(0, 1))
self.assertFalse(d.connected(2, 3))
def test_full_rollback_to_zero(self):
d = DSU(4)
d.union(0, 1); d.union(2, 3); d.union(1, 2)
d.rollback(0)
for i in range(4):
self.assertEqual(d.size(i), 1)
self.assertEqual(d.components(), 4)
def test_invalid_marker(self):
d = DSU(2)
with self.assertRaises(ValueError):
d.rollback(5)
with self.assertRaises(ValueError):
d.rollback(-1)
def test_no_path_compression_find_stable(self):
# Q94 bullet 2: the node asked about must sit TWO levels down, else a
# compressing find rewrites nothing and the cell is vacuous.
# union-by-size: union(0,1)==(1<-0), union(2,3)==(3<-2), union(1,2)==(2<-0)
d = DSU(4)
d.union(0, 1); d.union(2, 3); d.union(1, 2)
self.assertEqual(d.parent, [0, 0, 0, 2])
self.assertEqual(d.parent[3], 2) # precondition: depth 2
before = list(d.parent)
self.assertEqual(d.find(3), 0) # root via parent[3] -> 2 -> 0
self.assertEqual(d.parent, before) # a compressing find writes parent[3]=0
self.assertEqual(d.size(3), 4)
def test_size_after_rollback(self):
d = DSU(3)
d.union(0, 1)
m = d.snapshot()
d.union(0, 2)
self.assertEqual(d.size(0), 3)
d.rollback(m)
self.assertEqual(d.size(0), 2)
self.assertEqual(d.size(2), 1)
def test_components(self):
d = DSU(4)
self.assertEqual(d.components(), 4)
d.union(0, 1); d.union(2, 3)
self.assertEqual(d.components(), 2)
d.union(1, 2)
self.assertEqual(d.components(), 1)
def test_negative_n(self):
with self.assertRaises(ValueError):
DSU(-1)
def test_shape_union_by_size(self):
d = DSU(8)
d.union(0,1); d.union(2,3); d.union(4,5); d.union(6,7)
d.union(1,2); d.union(4,0)
# union by size: attach smaller root under larger -> parent[4]==0, depth 2
self.assertEqual(d.parent[4], 0)
# deepest depth
maxdepth = 0
for i in range(8):
depth=0; cur=i
while d.parent[cur]!=cur:
cur=d.parent[cur]; depth+=1
maxdepth=max(maxdepth,depth)
self.assertEqual(maxdepth, 2)
def test_rollback_marker_type(self):
d = DSU(2)
for bad in (True, False, 1.5, "x"):
with self.assertRaises((TypeError, ValueError)):
d.rollback(bad)
def _fingerprint(self, d):
return (list(d.parent), list(d.sz), list(d.history), d.components())
def test_refuses_out_of_range_index(self):
# Q94 bullet 3: every entry point that takes an index refuses a range
# violation, and a refused call mutates nothing.
d = DSU(3)
d.union(0, 1)
before = self._fingerprint(d)
for bad in (-1, 3, 10):
for name in ("find", "size"):
with self.assertRaises(ValueError, msg="%s(%r)" % (name, bad)):
getattr(d, name)(bad)
self.assertEqual(self._fingerprint(d), before)
for args in ((bad, 0), (0, bad)):
with self.assertRaises(ValueError, msg="union%r" % (args,)):
d.union(*args)
self.assertEqual(self._fingerprint(d), before)
with self.assertRaises(ValueError, msg="connected%r" % (args,)):
d.connected(*args)
self.assertEqual(self._fingerprint(d), before)
def test_refuses_non_int_index(self):
# Q94 bullet 4: a bool is an int subclass in Python; True must not alias
# index 1 and False must not merge index 0.
d = DSU(3)
before = self._fingerprint(d)
for bad in (True, False):
with self.assertRaises(TypeError, msg="find(%r)" % (bad,)):
d.find(bad)
with self.assertRaises(TypeError, msg="size(%r)" % (bad,)):
d.size(bad)
with self.assertRaises(TypeError, msg="union(%r, 1)" % (bad,)):
d.union(bad, 1)
with self.assertRaises(TypeError, msg="connected(%r, 0)" % (bad,)):
d.connected(bad, 0)
self.assertEqual(self._fingerprint(d), before)
with self.assertRaises(TypeError):
d.find(1.5)
with self.assertRaises(TypeError):
d.find(None)
if __name__ == "__main__":
unittest.main()