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).
165 lines
5.8 KiB
Python
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()
|