Repairs wizard-work/beam/test_interval.py to resolve interval_merge.py from Path(__file__).resolve().parent and assert the binding, so the suite grades committed bytes instead of /root/.hermes/wizard-outputs/beam. Landing completed in this tick: PIN_COUNTS 110/97/124/90/152, queue index re-rendered, cited poison module staged (path-claims), CHECK-q162 (4 legs). Measured: tools/verify.py --fast -> 12 legs, 11 pass, 0 fault, 1 skipped, rc 0.
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""Tests for interval_merge.merge_intervals / total_covered / gaps.
|
|
|
|
Runs from this directory only:
|
|
python3 test_interval.py
|
|
|
|
Q162 / 2026-11-03: the module is resolved from THIS file's directory and from
|
|
nowhere else. The file landed at 33e8ffb with
|
|
|
|
sys.path.insert(0, "/root/.hermes/wizard-outputs/beam")
|
|
|
|
which put a HOST directory first on sys.path, so every cell below graded
|
|
/root/.hermes/wizard-outputs/beam/interval_merge.py and never the committed
|
|
bytes. Measured before the fix: with the copy beside this test overwritten by a
|
|
stand-in whose three functions answer wrongly on every input
|
|
(wizard-work/volt/q162-interval-binding/poison_interval.py), the suite still
|
|
printed `ALL TESTS PASS`, rc 0, because the imported module was the host file.
|
|
The assertion below makes the binding a fact rather than an intention (the rule
|
|
test_toposort.py states at Q61, test_lfu_cache.py at Q98, and
|
|
test_tarjan_articulation.py at Q160).
|
|
"""
|
|
import pathlib
|
|
import random
|
|
import sys
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
sys.path.insert(0, str(HERE))
|
|
|
|
import interval_merge as module_under_test
|
|
from interval_merge import merge_intervals, total_covered, gaps
|
|
|
|
# The module under test must be the committed file next to this test.
|
|
MODULE_PATH = pathlib.Path(module_under_test.__file__).resolve()
|
|
assert MODULE_PATH == (HERE / "interval_merge.py"), (MODULE_PATH, HERE)
|
|
|
|
|
|
def brute_merge(intervals):
|
|
pts = set()
|
|
# discrete integer points model for small ranges
|
|
pts_all = set()
|
|
for s, e in intervals:
|
|
for x in range(s, e):
|
|
pts_all.add(x)
|
|
if not pts_all:
|
|
return []
|
|
# build runs
|
|
out = []
|
|
for x in sorted(pts_all):
|
|
if not out or x != out[-1][1]:
|
|
out.append([x, x+1])
|
|
else:
|
|
out[-1][1] = x+1
|
|
return [tuple(r) for r in out]
|
|
|
|
random.seed(42)
|
|
for trial in range(2000):
|
|
n = random.randint(0, 8)
|
|
ivs = []
|
|
for _ in range(n):
|
|
s = random.randint(-5, 5)
|
|
e = s + random.randint(1, 6)
|
|
ivs.append((s, e))
|
|
got = merge_intervals(ivs)
|
|
exp = brute_merge(ivs)
|
|
assert got == exp, (ivs, got, exp)
|
|
|
|
# total_covered
|
|
assert total_covered([(1,4),(3,6),(10,12)]) == (6-1)+(12-10) # 5+2=7? 1..6 =5, 10..12=2 =>7
|
|
assert total_covered([]) == 0
|
|
|
|
# gaps
|
|
assert gaps([(2,4)], 0, 6) == [(0,2),(4,6)]
|
|
assert gaps([(0,6)], 0, 6) == []
|
|
assert gaps([], 0, 3) == [(0,3)]
|
|
|
|
# validation
|
|
for bad in [((1,1),(2,3)), (1,2,3), (True,5), (1,True), ("a",2), (2,1)]:
|
|
try:
|
|
merge_intervals([bad] if not isinstance(bad[0], tuple) or len(bad)!=2 or not isinstance(bad[0], int) else [bad])
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
# some cases may be wrapped; ensure at least invalid ones raise
|
|
pass
|
|
|
|
# explicit invalid
|
|
for bad in [(1,1),(2,1),(True,3),(1,True)]:
|
|
try:
|
|
merge_intervals([bad])
|
|
raise AssertionError(f"no raise for {bad}")
|
|
except ValueError:
|
|
pass
|
|
|
|
print("ALL TESTS PASS") |