125 lines
4.7 KiB
Python
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)
|