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

125 lines
4.7 KiB
Python

"""Cells for interval_overlap.py -- collected by `unittest discover` in beam/.
Covers half-open adjacency, zero-length intervals, the end<start refusal, an
exhaustive cross-check of the sweep against the brute-force definition, and
`has_any_overlap` in both directions -- including the structural claim that it
does not build the pair list to answer a boolean.
"""
import itertools
import random
import time
import unittest
from unittest import mock
import interval_overlap as mod
def _domain(low=-2, high=3):
return [(s, e) for s in range(low, high + 1) for e in range(s, high + 1)]
class IntervalOverlapTest(unittest.TestCase):
def test_half_open_adjacency_both_orders(self):
self.assertEqual(mod.find_overlapping_pairs([(1, 2), (2, 3)]), [])
self.assertEqual(mod.find_overlapping_pairs([(2, 3), (1, 2)]), [])
def test_zero_length_against_wide_interval(self):
self.assertEqual(mod.find_overlapping_pairs([(5, 5), (1, 10)]), [])
self.assertEqual(mod.find_overlapping_pairs([(1, 10), (5, 5)]), [])
def test_zero_length_touching_boundary(self):
self.assertEqual(mod.find_overlapping_pairs([(2, 2), (1, 2)]), [])
self.assertEqual(mod.find_overlapping_pairs([(1, 2), (2, 2)]), [])
def test_zero_length_pair_is_empty(self):
self.assertEqual(mod.find_overlapping_pairs([(3, 3), (3, 3)]), [])
self.assertEqual(mod.find_overlapping_pairs([(0, 0)]), [])
def test_nested_and_disjoint_pairs(self):
self.assertEqual(
mod.find_overlapping_pairs([(1, 10), (2, 3), (4, 5)]),
[(0, 1), (0, 2)],
)
self.assertEqual(
mod.find_overlapping_pairs([(1, 3), (2, 4)]),
[(0, 1)],
)
def test_valueerror_when_end_before_start(self):
with self.assertRaises(ValueError):
mod.find_overlapping_pairs([(3, 1)])
with self.assertRaises(ValueError):
mod.has_any_overlap([(0, 5), (9, 2)])
def test_exhaustive_multisets_against_brute_force(self):
intervals = _domain()
self.assertEqual(len(intervals), 21)
cases = 0
for size in range(4):
for case in itertools.combinations_with_replacement(intervals, size):
cases += 1
ivs = list(case)
self.assertEqual(
mod.find_overlapping_pairs(ivs),
mod.brute_force_pairs(ivs),
msg="case %r" % (ivs,),
)
# Pins the enumeration itself: 21 multisets of size 0-3 -> 2024 cases.
self.assertEqual(cases, 2024)
def test_randomised_against_brute_force(self):
rng = random.Random(42)
for _ in range(2000):
ivs = []
for _ in range(rng.randint(0, 12)):
a = rng.randint(-5, 5)
ivs.append((a, a + rng.randint(0, 6)))
self.assertEqual(
mod.find_overlapping_pairs(ivs),
mod.brute_force_pairs(ivs),
msg="case %r" % (ivs,),
)
def test_has_any_overlap_true_and_false(self):
self.assertTrue(mod.has_any_overlap([(1, 3), (2, 4)]))
self.assertTrue(mod.has_any_overlap([(1, 10), (2, 3)]))
self.assertFalse(mod.has_any_overlap([(1, 2), (2, 3)]))
self.assertFalse(mod.has_any_overlap([(5, 5), (1, 10)]))
self.assertFalse(mod.has_any_overlap([]))
self.assertFalse(mod.has_any_overlap([(1, 2)]))
def test_has_any_overlap_agrees_with_pairs(self):
rng = random.Random(7)
for _ in range(500):
ivs = []
for _ in range(rng.randint(0, 10)):
a = rng.randint(-4, 4)
ivs.append((a, a + rng.randint(0, 5)))
self.assertEqual(
mod.has_any_overlap(ivs),
bool(mod.find_overlapping_pairs(ivs)),
msg="case %r" % (ivs,),
)
def test_has_any_overlap_does_not_build_pair_list(self):
# The "cheaper" claim is structural: answering the boolean must not
# route through the pair-list function. If it does, this cell dies.
with mock.patch.object(
mod, "find_overlapping_pairs", side_effect=AssertionError("materialised pairs")
):
self.assertTrue(mod.has_any_overlap([(0, 1), (0, 1)]))
def test_has_any_overlap_large_overlapping_input_is_cheap(self):
# 2000 intervals all sharing [0, 1): one comparison answers it. The
# pair-list function takes tens of seconds on this input.
ivs = [(0, 1000)] * 2000
start = time.perf_counter()
self.assertTrue(mod.has_any_overlap(ivs))
elapsed = time.perf_counter() - start
self.assertLess(elapsed, 5.0, "took %.2fs" % elapsed)
if __name__ == "__main__":
unittest.main(verbosity=2)