#!/usr/bin/env python3 """test_intervalkit.py -- verification harness for intervalkit (stdlib only). Run: python3 test_intervalkit.py exit 0 = every check passed What this harness establishes ----------------------------- 1. EXACT DIFFERENTIAL ORACLE. For integer-endpoint interval sets, membership over the integers is decided by ``in_set`` -- an independent 5-line predicate written in this file, not imported from the library. Every operation is then compared against the corresponding set operation on bitmaps over the universe [-24, 25). Because every operation builds its result from max/min combinations of input endpoints, integer endpoints stay integer, so the integer bitmap *is* the set: this comparison is exact, not sampled. 2. STRUCTURAL CONTRACT. Every result must be canonical (sorted, non-degenerate, strictly separated). 3. ALGEBRAIC LAWS. De Morgan (both directions), complement involution, idempotence, commutativity, associativity, a == (a n b) U (a \\ b), a n ~a == {}, a U ~a == the line, measure additivity. 4. HAND-CHECKED BOUNDARY CASES. Touching intervals, empty intersection at a shared endpoint, one-unit gaps, unbounded bounds, the full line, text round-trips, and the CLI as a subprocess. 5. NEGATIVE CONTROLS (teeth). Six deliberately broken implementations are run through the *same* oracle, which must report each one as detected, in the leg that is supposed to see it. A harness that cannot fail proves nothing. 6. ANTI-VACUITY GUARD. The runner counts executed comparisons and fuzz coverage, and refuses to exit 0 below the floors. """ from __future__ import annotations import os import random import subprocess import sys import time from collections import Counter from fractions import Fraction HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import intervalkit as ik # noqa: E402 TRIALS = 3000 SEED = 20261022 UNIVERSE = list(range(-24, 25)) UNIVERSE_SET = set(UNIVERSE) # Anti-vacuity floors: fail loudly if the fuzz degenerates into trivial inputs, # if checks silently stop executing comparisons, or if checks disappear. MIN_CHECKS = 12 MIN_SUBASSERTIONS = 100000 MIN_UNBOUNDED = 200 MIN_MERGES = 300 MIN_GAPS = 300 MIN_EMPTIES = 50 MIN_DISTINCT_SETS = 500 # -------------------------------------------------------------------------- # the oracle: independent membership predicate + bitmap # -------------------------------------------------------------------------- def in_set(a, x): """Independent half-open membership test. Deliberately NOT ik.contains.""" for lo, hi in a: if (lo is None or lo <= x) and (hi is None or x < hi): return True return False def bitmap(a): return frozenset(x for x in UNIVERSE if in_set(a, x)) def exact_measure(a): """Total length over the reals, or inf when any interval is unbounded.""" total = 0 for lo, hi in a: if lo is None or hi is None: return float("inf") total += hi - lo return total def brute_bitmap(ivs, window=None): """Bitmap by enumeration -- no sweep, no sorting, no library code. Unbounded bounds are clipped to ``window`` (a (lo, hi) pair) when given. """ out = set() for lo, hi in ivs: if window is not None: if lo is None: lo = window[0] if hi is None: hi = window[1] out.update(range(lo, hi)) return out # -------------------------------------------------------------------------- # tiny check runner with sub-assertion accounting # -------------------------------------------------------------------------- CHECKS = [] def check(name): def deco(fn): CHECKS.append((name, fn)) return fn return deco class Ctx: def __init__(self): self.sub = 0 self.notes = [] def ok(self, cond, msg): self.sub += 1 if not cond: raise AssertionError(msg) return True def eq(self, got, want, msg): self.sub += 1 if got != want: raise AssertionError("%s\n got: %r\n want: %r" % (msg, got, want)) return True def note(self, text): self.notes.append(text) # -------------------------------------------------------------------------- # fuzz: implementation shim, mutant implementations, differential driver # -------------------------------------------------------------------------- class Impl: """A namespace of set operations; defaults to the real library.""" def __init__(self, **over): self.normalize = over.get("normalize", ik.normalize) self.union = over.get("union", ik.union) self.intersection = over.get("intersection", ik.intersection) self.difference = over.get("difference", ik.difference) self.symmetric_difference = over.get("symmetric_difference", ik.symmetric_difference) self.complement = over.get("complement", ik.complement) self.measure = over.get("measure", ik.measure) self.contains = over.get("contains", ik.contains) self.is_canonical = over.get("is_canonical", ik.is_canonical) def _one(iv): """Canonicalize a single interval, raising if it is degenerate.""" n = ik.normalize([iv]) if not n: raise ik.IntervalError("degenerate interval %r" % (iv,)) return n[0] def _key(iv): return (0, 0) if iv[0] is None else (1, iv[0]) def mut_normalize_strict_merge(intervals): """BUG: joins neighbours only on strict overlap, so touching intervals survive unmerged and the result is not canonical.""" ivs = sorted((_one(iv) for iv in intervals), key=_key) if not ivs: return () out = [ivs[0]] for lo, hi in ivs[1:]: plo, phi = out[-1] if phi is None or lo < phi: # should be lo <= phi out[-1] = (plo, None if (phi is None or hi is None) else max(phi, hi)) else: out.append((lo, hi)) return tuple(out) def mut_intersection_always_advance_left(a, b): """BUG: advances the left cursor unconditionally, so it stops looking at the right set after the first hit.""" a, b = ik.normalize(a), ik.normalize(b) res = [] i = j = 0 while i < len(a) and j < len(b): alo, ahi = a[i] blo, bhi = b[j] lo = ik._max_lo(alo, blo) hi = ik._min_hi(ahi, bhi) if lo is None or hi is None or lo < hi: res.append((lo, hi)) i += 1 # should advance whichever interval ends first return tuple(res) def mut_complement_bounds_swapped(a): """BUG: writes each bound into the wrong slot.""" a = ik.normalize(a) out = [] for lo, hi in a: out.append((None, hi)) out.append((lo, None)) return ik.normalize(out) def mut_measure_counts_integer_slots(a): """BUG: counts integer slots (hi - lo - 1) instead of length.""" total = 0 for lo, hi in ik.normalize(a): if lo is None or hi is None: return float("inf") total += hi - lo - 1 return total def mut_contains_ignores_unbounded_lower(a, x): """BUG: drops the -inf case, so (-inf, h) is treated as empty.""" for lo, hi in a: if (lo is not None and lo <= x) and (hi is None or x < hi): return True return False def mut_union_unnormalized_concat(*sets): """BUG: concatenates normalized parts without re-normalizing the join.""" out = [] for s in sets: out.extend(ik.normalize(s)) return tuple(out) def mut_normalize_no_neginf_merge(intervals): """BUG: this is the pre-fix body of ik.normalize -- it compares a second -inf lower bound with ``<=`` and crashes with a TypeError. The differential fuzz found this in the real library; the mutant pins the regression.""" ivs = sorted((_one(iv) for iv in intervals), key=_key) if not ivs: return () out = [ivs[0]] for lo, hi in ivs[1:]: plo, phi = out[-1] if phi is None or lo <= phi: # crashes when lo is None out[-1] = (plo, None if (phi is None or hi is None) else max(phi, hi)) else: out.append((lo, hi)) return tuple(out) MUTANTS = [ ("normalize: strict-overlap merge (misses touching)", Impl(normalize=mut_normalize_strict_merge), "structural"), ("intersection: always advance left cursor", Impl(intersection=mut_intersection_always_advance_left), "semantic"), ("complement: bounds written to wrong slots", Impl(complement=mut_complement_bounds_swapped), "semantic"), ("measure: counts integer slots (hi - lo - 1)", Impl(measure=mut_measure_counts_integer_slots), "semantic"), ("contains: ignores unbounded lower bound", Impl(contains=mut_contains_ignores_unbounded_lower), "semantic"), ("union: unnormalized concatenation", Impl(union=mut_union_unnormalized_concat), "structural"), ("normalize: duplicate -inf lower bound crashes (pre-fix body)", Impl(normalize=mut_normalize_no_neginf_merge), "semantic"), ] REAL = Impl() def _rand_intervals(rng, n): """Random valid intervals with integer endpoints in [-10, 10], plus unbounded bounds and the occasional full line.""" ivs = [] for _ in range(n): style = rng.random() lo = None if style < 0.15 else rng.randint(-10, 10) hi = None if style > 0.85 else rng.randint(-10, 10) if lo is not None and hi is not None and lo >= hi: lo, hi = hi, lo if lo == hi: continue ivs.append((lo, hi)) return ivs def diff_failures(impl, trials=TRIALS, seed=SEED, with_coverage=False): """Run the differential oracle against ``impl``. Returns 'semantic' and 'structural' failure lists, a 'checks_run' count of comparisons actually performed, and 'coverage' when asked. """ rng = random.Random(seed) semantic, structural = [], [] cov = Counter() seen = set() n_checks = 0 def hit(leg, op, msg, raw_a, raw_b=None): entry = "%s(%r%s): %s" % (op, raw_a, "" if raw_b is None else ", %r" % (raw_b,), msg) (semantic if leg == "semantic" else structural).append(entry) for t in range(trials): raw_a = _rand_intervals(rng, rng.randint(0, 4)) raw_b = _rand_intervals(rng, rng.randint(0, 4)) ba, bb = bitmap(raw_a), bitmap(raw_b) can_a, can_b = ik.normalize(raw_a), ik.normalize(raw_b) cov["sets"] += 2 seen.add(can_a) seen.add(can_b) if len(raw_a) != len(can_a) or len(raw_b) != len(can_b): cov["merges"] += 1 if any(lo is None or hi is None for lo, hi in raw_a + raw_b): cov["unbounded"] += 1 if len(can_a) > 1 or len(can_b) > 1: cov["gaps"] += 1 if not can_a or not can_b: cov["empties"] += 1 # ---- normalize ------------------------------------------------- try: r = impl.normalize(raw_a) except Exception as exc: # noqa: BLE001 hit("semantic", "normalize", "raised %s: %s" % (type(exc).__name__, exc), raw_a) r = None if r is not None: n_checks += 2 if bitmap(r) != ba: hit("semantic", "normalize", "bitmap %r != %r" % (sorted(bitmap(r)), sorted(ba)), raw_a) if not impl.is_canonical(r): hit("structural", "normalize", "result %r is not canonical" % (r,), raw_a) # ---- binary set operations ------------------------------------ for op, want in ( ("union", lambda: ba | bb), ("intersection", lambda: ba & bb), ("difference", lambda: ba - bb), ("symmetric_difference", lambda: ba ^ bb), ): fn = getattr(impl, op) try: res = fn(raw_a, raw_b) except Exception as exc: # noqa: BLE001 hit("semantic", op, "raised %s: %s" % (type(exc).__name__, exc), raw_a, raw_b) continue n_checks += 2 if bitmap(res) != want(): hit("semantic", op, "bitmap %r != %r" % (sorted(bitmap(res)), sorted(want())), raw_a, raw_b) if not impl.is_canonical(res): hit("structural", op, "result %r is not canonical" % (res,), raw_a, raw_b) # ---- complement (over the whole line) -------------------------- try: res = impl.complement(can_a) except Exception as exc: # noqa: BLE001 hit("semantic", "complement", "raised %s: %s" % (type(exc).__name__, exc), can_a) else: n_checks += 2 want = UNIVERSE_SET - ba if bitmap(res) != want: hit("semantic", "complement", "bitmap %r != %r" % (sorted(bitmap(res)), sorted(want)), can_a) if not impl.is_canonical(res): hit("structural", "complement", "result %r is not canonical" % (res,), can_a) # ---- measure --------------------------------------------------- try: m = impl.measure(raw_a) except Exception as exc: # noqa: BLE001 hit("semantic", "measure", "raised %s: %s" % (type(exc).__name__, exc), raw_a) else: n_checks += 1 want_m = exact_measure(can_a) if m != want_m: hit("semantic", "measure", "got %r want %r" % (m, want_m), raw_a) elif want_m != float("inf") and len(ba) != want_m: raise AssertionError( "oracle self-check: integer bitmap size %d != real measure %r for %r" % (len(ba), want_m, raw_a) ) # ---- contains (every 4th trial, to keep the run quick) --------- if t % 4 == 0: for x in UNIVERSE: try: got = impl.contains(raw_a, x) except Exception as exc: # noqa: BLE001 hit("semantic", "contains", "raised %s: %s" % (type(exc).__name__, exc), raw_a) break n_checks += 1 if bool(got) != (x in ba): hit("semantic", "contains", "contains(%r) is %r, oracle says %r" % (x, got, x in ba), raw_a) break out = {"semantic": semantic, "structural": structural, "checks_run": n_checks} if with_coverage: cov["distinct_canonical_sets"] = len(seen) cov["trials"] = trials cov["contains_points_checked"] = (trials + 3) // 4 * len(UNIVERSE) out["coverage"] = cov return out # -------------------------------------------------------------------------- # 1. the real implementation must be clean, and the fuzz must have teeth # -------------------------------------------------------------------------- @check("fuzz: real implementation -- 0 semantic, 0 structural failures") def _c_real(ctx): t0 = time.time() res = diff_failures(REAL, TRIALS, SEED, with_coverage=True) cov = res["coverage"] ctx.sub += res["checks_run"] ctx.ok(not res["semantic"], "semantic failures: %d, first 3: %s" % (len(res["semantic"]), res["semantic"][:3])) ctx.ok(not res["structural"], "structural failures: %d, first 3: %s" % (len(res["structural"]), res["structural"][:3])) ctx.note("trials=%d, %d comparisons in %.1fs" % (TRIALS, res["checks_run"], time.time() - t0)) ctx.note("coverage: " + ", ".join("%s=%d" % kv for kv in sorted(cov.items()))) ctx.ok(cov["unbounded"] >= MIN_UNBOUNDED, "fuzz barely exercised unbounded bounds: %d < %d" % (cov["unbounded"], MIN_UNBOUNDED)) ctx.ok(cov["merges"] >= MIN_MERGES, "fuzz barely exercised merging: %d < %d" % (cov["merges"], MIN_MERGES)) ctx.ok(cov["gaps"] >= MIN_GAPS, "fuzz barely produced multi-interval sets: %d < %d" % (cov["gaps"], MIN_GAPS)) ctx.ok(cov["empties"] >= MIN_EMPTIES, "fuzz barely produced empty sets: %d < %d" % (cov["empties"], MIN_EMPTIES)) ctx.ok(cov["distinct_canonical_sets"] >= MIN_DISTINCT_SETS, "fuzz explored only %d distinct sets" % cov["distinct_canonical_sets"]) @check("teeth: all 7 mutants detected, each in the expected leg") def _c_mutants(ctx): for name, impl, leg in MUTANTS: res = diff_failures(impl, TRIALS, SEED) sem, stru = len(res["semantic"]), len(res["structural"]) ctx.note("mutant %-52s semantic=%-5d structural=%-5d (expected %s)" % (name, sem, stru, leg)) ctx.sub += res["checks_run"] ctx.ok(sem + stru > 0, "mutant NOT detected (harness has no teeth): %s" % name) ctx.ok((sem if leg == "semantic" else stru) > 0, "mutant detected, but not in the %s leg: %s" % (leg, name)) # -------------------------------------------------------------------------- # 2. algebraic laws, exact canonical equality # -------------------------------------------------------------------------- def _rand_sets(rng, n): for _ in range(n): yield ik.normalize(_rand_intervals(rng, rng.randint(0, 3))) @check("laws: De Morgan, involution, idempotence, commutativity, associativity") def _c_laws(ctx): rng = random.Random(SEED + 1) for a in _rand_sets(rng, 600): b = ik.normalize(_rand_intervals(rng, rng.randint(0, 3))) c = ik.normalize(_rand_intervals(rng, rng.randint(0, 3))) ctx.eq(ik.complement(ik.union(a, b)), ik.intersection(ik.complement(a), ik.complement(b)), "De Morgan (union) failed for %r, %r" % (a, b)) ctx.eq(ik.complement(ik.intersection(a, b)), ik.union(ik.complement(a), ik.complement(b)), "De Morgan (intersection) failed for %r, %r" % (a, b)) ctx.eq(ik.complement(ik.complement(a)), a, "involution failed for %r" % (a,)) ctx.eq(ik.union(a, a), a, "union idempotence failed for %r" % (a,)) ctx.eq(ik.intersection(a, a), a, "intersection idempotence failed for %r" % (a,)) ctx.eq(ik.union(a, b), ik.union(b, a), "union commutativity failed for %r, %r" % (a, b)) ctx.eq(ik.intersection(a, b), ik.intersection(b, a), "intersection commutativity failed for %r, %r" % (a, b)) ctx.eq(ik.symmetric_difference(a, b), ik.symmetric_difference(b, a), "symdiff commutativity failed for %r, %r" % (a, b)) ctx.eq(ik.union(ik.union(a, b), c), ik.union(a, ik.union(b, c)), "union associativity failed for %r, %r, %r" % (a, b, c)) ctx.eq(ik.intersection(ik.intersection(a, b), c), ik.intersection(a, ik.intersection(b, c)), "intersection associativity failed for %r, %r, %r" % (a, b, c)) ctx.eq(ik.union(ik.intersection(a, b), ik.difference(a, b)), a, "a == (a n b) U (a \\ b) failed for %r, %r" % (a, b)) ctx.eq(ik.intersection(a, ik.complement(a)), (), "a n ~a != {} for %r" % (a,)) ctx.eq(ik.union(a, ik.complement(a)), ((None, None),), "a U ~a != the whole line for %r" % (a,)) ctx.eq(ik.difference(a, a), (), "a \\ a != {} for %r" % (a,)) ctx.eq(ik.difference(a, ()), a, "a \\ {} != a for %r" % (a,)) ctx.eq(ik.difference((), a), (), "{} \\ a != {} for %r" % (a,)) ctx.eq(ik.union(a, ()), a, "a U {} != a for %r" % (a,)) ctx.eq(ik.intersection(a, ((None, None),)), a, "a n line != a for %r" % (a,)) ctx.eq(ik.symmetric_difference(a, a), (), "a ^ a != {} for %r" % (a,)) ma, mb = ik.measure(a), ik.measure(b) if ma != float("inf") and mb != float("inf"): ctx.eq(ik.measure(ik.union(a, b)) + ik.measure(ik.intersection(a, b)), ma + mb, "measure additivity failed for %r, %r" % (a, b)) ctx.ok(ik.is_canonical(a), "generated set %r is not canonical" % (a,)) @check("laws: normalize is idempotent and order-independent; bad input rejected") def _c_normalize(ctx): rng = random.Random(SEED + 2) for _ in range(600): raw = _rand_intervals(rng, rng.randint(0, 5)) n1 = ik.normalize(raw) ctx.eq(ik.normalize(n1), n1, "normalize not idempotent for %r" % (raw,)) ctx.ok(ik.is_canonical(n1), "normalize(%r) not canonical: %r" % (raw, n1)) ctx.eq(ik.assert_canonical(n1), n1, "assert_canonical rejected its own output for %r" % (raw,)) perm = list(raw) rng.shuffle(perm) ctx.eq(ik.normalize(perm), n1, "normalize depends on input order for %r" % (raw,)) if list(raw) != list(n1): ctx.ok(not ik.is_canonical(raw), "a non-normal-form %r passed is_canonical" % (raw,)) for bad in ([(1, 2), (2, 3)], [(2, 3), (1, 2)], [(1, 5), (2, 3)], [(None, 5), (1, 2)], [(1, 2), (None, 5)], [(None, None), (1, 2)]): ctx.ok(not ik.is_canonical(bad), "is_canonical accepted non-canonical %r" % (bad,)) for good in ([], [(1, 2)], [(1, 2), (3, 4)], [(None, 2), (3, None)], [(None, None)]): ctx.ok(ik.is_canonical(good), "is_canonical rejected canonical %r" % (good,)) for bad_iv in ([(3, 3)], [(5, 1)], [(1, 2, 3)], ["[1,2)"], [(None, None, None)], [(1,)]): try: ik.normalize(bad_iv) except ik.IntervalError: ctx.sub += 1 else: raise AssertionError("normalize accepted malformed input %r" % (bad_iv,)) # regressions for the two library bugs this suite found while being written ctx.eq(ik.union([(None, 1)], [(None, 2)]), ((None, 2),), "two -inf lower bounds must merge, not crash") ctx.eq(ik.union([(None, 1), (-9, -6)], [(None, 1), (-9, -6)]), ((None, 1),), "union of a set that reaches -inf with itself") ctx.eq(ik.symmetric_difference([(None, 0)], [(None, 1)]), ((0, 1),), "symmetric difference of two half-lines meeting at 0") ctx.eq(ik.union([(None, None)], [(1, 2)]), ((None, None),), "the full line absorbs anything") ctx.ok(ik.is_canonical([(None, 5), (1, 2)]) is False, "is_canonical must return False for an unsorted -inf set, not crash") # -------------------------------------------------------------------------- # 3. hand-checked boundary cases # -------------------------------------------------------------------------- @check("boundary: touching, gaps, shared endpoints, unbounded, full line") def _c_boundary(ctx): U, I, D, C, F = ik.union, ik.intersection, ik.difference, ik.complement, ik.format_set ctx.eq(F(U([(1, 3)], [(2, 5)])), "[1,5)", "overlapping union") ctx.eq(F(U([(1, 2)], [(2, 3)])), "[1,3)", "touching intervals must merge") ctx.eq(F(U([(1, 2)], [(3, 4)])), "[1,2) U [3,4)", "a one-unit gap must not merge") ctx.eq(F(U([(5, 6)], [(1, 2)])), "[1,2) U [5,6)", "unsorted union") ctx.eq(F(U([(1, 2)], [(2, 3)], [(3, 4)])), "[1,4)", "chain of touching intervals") ctx.eq(F(I([(1, 3)], [(3, 5)])), "{}", "a shared endpoint is in neither half-open interval") ctx.eq(F(I([(1, 3)], [(2, 3)])), "[2,3)", "nested intersection") ctx.eq(F(I([(1, 10)], [(3, 4), (6, 7)])), "[3,4) U [6,7)", "intersection against a split set") ctx.eq(F(I([(None, 5)], [(0, None)])), "[0,5)", "unbounded on both sides") ctx.eq(F(I()), "[-inf,+inf)", "intersection of nothing is the whole line") ctx.eq(F(D([(1, 5)], [(2, 3)])), "[1,2) U [3,5)", "difference splits an interval") ctx.eq(F(D([(1, 5)], [(1, 5)])), "{}", "difference with itself") ctx.eq(F(D([(1, 5)], [(0, 10)])), "{}", "difference with a superset") ctx.eq(F(D([(1, 5)], [(5, 6)])), "[1,5)", "difference with a disjoint neighbour") ctx.eq(F(C([(1, 3)])), "[-inf,1) U [3,+inf)", "complement of an interval") ctx.eq(F(C([])), "[-inf,+inf)", "complement of the empty set is the line") ctx.eq(F(C([(None, None)])), "{}", "complement of the line is empty") ctx.eq(F(C(C([(1, 3), (7, 9)]))), "[1,3) U [7,9)", "complement is an involution") ctx.eq(F(U([(None, 0)], [(0, None)])), "[-inf,+inf)", "half-lines meet at 0") ctx.eq(F(ik.symmetric_difference([(1, 5)], [(3, 7)])), "[1,3) U [5,7)", "symmetric difference") ctx.eq(ik.measure([(1, 5)]), 4, "measure of [1,5)") ctx.eq(ik.measure([(1, 2), (3, 4)]), 2, "measure sums over parts") ctx.eq(ik.measure([]), 0, "measure of the empty set") ctx.ok(ik.measure([(None, 1)]) == float("inf"), "measure of an unbounded set is inf") ctx.eq(ik.measure(U([(1, 5)], [(3, 9)])), 8, "measure of a merged union") ctx.eq(ik.from_points({0, 1, 2, 5}), ((0, 3), (5, 6)), "from_points groups runs") ctx.eq(ik.from_points(set()), (), "from_points of nothing") ctx.eq(ik.sample_points([(0, 3), (5, 7)], 0, 10), [0, 1, 2, 5, 6], "sample_points") ctx.ok(ik.contains([(1, 3)], 1) and not ik.contains([(1, 3)], 3), "contains is half-open at both ends") ctx.ok(not ik.contains([], 0), "the empty set contains nothing") @check("boundary: text parse / format round-trips and rejection") def _c_text(ctx): cases = [ ("[1,3) U [5,+inf)", ((1, 3), (5, None))), ("{}", ()), ("empty", ()), ("[-inf,0) | [2,3)", ((None, 0), (2, 3))), ("[1.5,2.5)", ((1.5, 2.5),)), ("[3,4) ; [1,2)", ((1, 2), (3, 4))), ("[1,2) U [2,3)", ((1, 3),)), ("[0,1) U [0,1) U [0,1)", ((0, 1),)), ] for text, want in cases: got = ik.parse(text) ctx.eq(got, want, "parse(%r)" % text) ctx.eq(ik.parse(ik.format_set(got)), got, "round-trip through format_set for %r" % text) ctx.eq(ik.format_set(ik.parse("[1,3) U [5,+inf)")), "[1,3) U [5,+inf)", "format round-trip") ctx.eq(ik.format_set(ik.parse("[1,2) U [2,3)")), "[1,3)", "parse canonicalizes touching") for bad in ("[1,2]", "(1,2)", "[1,2", "1,2", "[a,b)", "[2,1)", "[+inf,3)", "[1,-inf)", "[]", "junk", "[1,2) [3,4)"): try: ik.parse(bad) except ik.IntervalError: ctx.sub += 1 else: raise AssertionError("parse accepted invalid text %r" % (bad,)) @check("points: from_points / sample_points agree with the bitmap oracle") def _c_points(ctx): rng = random.Random(SEED + 5) for _ in range(300): pts = set(rng.sample(UNIVERSE, rng.randint(0, 12))) s = ik.from_points(pts) ctx.ok(ik.is_canonical(s), "from_points(%r) is not canonical: %r" % (sorted(pts), s)) ctx.eq(set(ik.sample_points(s, -24, 25)), pts, "from_points/sample_points round trip for %r" % (sorted(pts),)) ctx.eq(set(bitmap(s)), pts, "bitmap of from_points disagrees for %r" % (sorted(pts),)) ctx.eq(ik.measure(s), len(pts), "measure of a point set is its cardinality: %r" % (s,)) @check("endpoints: no invented endpoints; float and Fraction endpoints exact") def _c_endpoints(ctx): rng = random.Random(SEED + 4) def eps(a): return set(b for iv in ik.normalize(a) for b in iv if b is not None) for _ in range(400): ra = [(rng.randint(-40, 40) / 4.0, rng.randint(-40, 40) / 4.0) for _ in range(rng.randint(0, 3))] rb = [(rng.randint(-40, 40) / 4.0, rng.randint(-40, 40) / 4.0) for _ in range(rng.randint(0, 3))] ra = [iv for iv in ra if iv[0] < iv[1]] rb = [iv for iv in rb if iv[0] < iv[1]] src = eps(ra) | eps(rb) for res in (ik.union(ra, rb), ik.intersection(ra, rb), ik.difference(ra, rb), ik.complement(ra), ik.normalize(ra)): ctx.ok(eps(res) <= src, "invented endpoint in %r from %r / %r" % (res, ra, rb)) ctx.ok(ik.is_canonical(res), "result %r is not canonical" % (res,)) ctx.eq(ik.format_set(ik.union([(0.5, 1.5)], [(1.25, 2.0)])), "[0.5,2)", "float endpoints merge") ctx.eq(ik.measure([(0.5, 1.5)]), 1.0, "float measure") ctx.eq(ik.measure([(Fraction(1, 3), Fraction(2, 3))]), Fraction(1, 3), "measure is exact over Fractions") ctx.eq(ik.format_set(ik.union([(Fraction(1, 3), Fraction(1, 2))], [(Fraction(1, 2), Fraction(2, 3))])), "[1/3,2/3)", "Fraction endpoints merge exactly") ctx.eq(ik.measure(ik.difference([(Fraction(0), Fraction(1))], [(Fraction(1, 3), Fraction(2, 3))])), Fraction(2, 3), "difference measure over Fractions") @check("scale: sweep normalize/intersection match brute force at 1500 intervals") def _c_scale(ctx): rng = random.Random(SEED + 6) n, lo, hi = 1500, -300, 300 def raw(): out = [] for _ in range(n): a = rng.randint(lo, hi - 1) out.append((a, rng.randint(a + 1, hi))) return out t0 = time.time() A, B = raw(), raw() na, nb = ik.normalize(A), ik.normalize(B) ba, bb = brute_bitmap(A), brute_bitmap(B) ctx.ok(ik.is_canonical(na) and ik.is_canonical(nb), "normalize output is not canonical at scale") ctx.ok(len(na) <= n and len(nb) <= n, "canonical form is longer than the input") ctx.eq(brute_bitmap(na), ba, "normalize at scale disagrees with brute force") ctx.eq(brute_bitmap(nb), bb, "normalize at scale disagrees with brute force (B)") ctx.eq(ik.measure(na), len(ba), "measure at scale != integer count") ctx.eq(ik.measure(na), sum(h - l for l, h in na), "measure at scale != sum of part lengths") ctx.eq(brute_bitmap(ik.union(A, B)), ba | bb, "union at scale disagrees with brute force") ctx.eq(brute_bitmap(ik.intersection(A, B)), ba & bb, "intersection at scale disagrees with brute force") ctx.eq(brute_bitmap(ik.difference(A, B)), ba - bb, "difference at scale disagrees with brute force") ctx.eq(brute_bitmap(ik.symmetric_difference(A, B)), ba ^ bb, "symmetric difference at scale disagrees with brute force") ctx.eq(brute_bitmap(ik.complement(na), (lo, hi)), set(range(lo, hi)) - ba, "complement at scale disagrees with brute force") ctx.note("1500 x 1500 intervals over 600 points: %d comparisons in %.2fs" % (ctx.sub, time.time() - t0)) # -------------------------------------------------------------------------- # 4. the CLI as a subprocess: exit code and stdout are the interface # -------------------------------------------------------------------------- def _run_cli(*args): exe = os.path.join(HERE, "intervalkit.py") return subprocess.run([sys.executable, exe, *args], capture_output=True, text=True) @check("cli: subprocess smoke over real argv, stdout and exit codes") def _c_cli(ctx): p = _run_cli("union", "[1,3)", "[2,5)") ctx.eq(p.returncode, 0, "cli union rc (stderr=%r)" % p.stderr) ctx.eq(p.stdout.strip(), "[1,5)", "cli union stdout") p = _run_cli("difference", "[1,5)", "[2,3)") ctx.eq(p.returncode, 0, "cli difference rc") ctx.eq(p.stdout.strip(), "[1,2) U [3,5)", "cli difference stdout") p = _run_cli("complement", "[1,3)") ctx.eq(p.stdout.strip(), "[-inf,1) U [3,+inf)", "cli complement stdout") p = _run_cli("symdiff", "[1,5)", "[3,7)") ctx.eq(p.stdout.strip(), "[1,3) U [5,7)", "cli symdiff stdout") p = _run_cli("contains", "[1,3)", "2.5") ctx.eq(p.stdout.strip(), "true", "cli contains true") p = _run_cli("contains", "[1,3)", "3") ctx.eq(p.stdout.strip(), "false", "cli contains false at the open end") p = _run_cli("measure", "[1,5)") ctx.eq(p.stdout.strip(), "4", "cli measure") p = _run_cli("measure", "[-inf,1)") ctx.eq(p.stdout.strip(), "inf", "cli measure of an unbounded set") p = _run_cli("parse", "[-inf,0) U [2,+inf)") ctx.eq(p.stdout.strip(), "[-inf,0) U [2,+inf)", "cli parse") p = _run_cli("union", "[1,2]") ctx.ok(p.returncode != 0, "cli must reject closed upper bounds") ctx.ok(p.stderr.strip() != "", "cli rejection must explain itself on stderr") p = _run_cli("frobnicate", "[1,2)") ctx.ok(p.returncode != 0, "cli must reject an unknown op") p = _run_cli("--help") ctx.eq(p.returncode, 0, "cli --help rc") ctx.ok("usage:" in p.stdout, "cli --help prints usage") @check("cli: exit code 0 on a valid op, and the value on stdout") def _c_cli_exit(ctx): p = _run_cli("intersection", "[1,10)", "[3,4)", "[6,7)") ctx.eq(p.returncode, 0, "cli rc") ctx.eq(p.stdout.strip(), "{}", "intersection of [1,10) with [3,4) and [6,7)") p = _run_cli("parse", "[1,2) U [2,3) U [4,5)") ctx.eq(p.stdout.strip(), "[1,3) U [4,5)", "cli parse canonicalizes") # -------------------------------------------------------------------------- # 5. oracle self-checks (the harness must not be lying to itself) # -------------------------------------------------------------------------- @check("oracle self-check: in_set agrees with ik.contains; bitmaps match measures") def _c_oracle(ctx): rng = random.Random(SEED + 3) for _ in range(500): can = ik.normalize(_rand_intervals(rng, rng.randint(0, 4))) bm = bitmap(can) m = exact_measure(can) if m != float("inf"): ctx.eq(len(bm), m, "bitmap size must equal the real measure for integer endpoints: %r" % (can,)) for x in UNIVERSE: ctx.ok(in_set(can, x) == ik.contains(can, x), "in_set and ik.contains disagree at %r for %r" % (x, can)) ctx.eq(bitmap([(0, 1)]), frozenset({0}), "bitmap of [0,1) over the integers is {0}") ctx.eq(bitmap([(None, None)]), frozenset(UNIVERSE), "bitmap of the line is the universe") ctx.eq(bitmap([]), frozenset(), "bitmap of the empty set is empty") ctx.ok(in_set([(0, 2)], 1) and not in_set([(0, 2)], 2), "the oracle predicate is half-open") # -------------------------------------------------------------------------- # runner # -------------------------------------------------------------------------- def run_all(): passed = failed = 0 total_sub = 0 for name, fn in CHECKS: ctx = Ctx() try: fn(ctx) except Exception as exc: # noqa: BLE001 failed += 1 print("FAIL %s" % name) for line in str(exc).splitlines(): print(" %s" % line) else: passed += 1 print("ok %-72s %8d assertions" % (name, ctx.sub)) total_sub += ctx.sub for note in ctx.notes: print(" . %s" % note) print("-" * 90) print("checks: %d passed, %d failed (of %d) comparisons executed: %d" % (passed, failed, len(CHECKS), total_sub)) if len(CHECKS) < MIN_CHECKS: print("FAIL anti-vacuity: only %d checks registered (floor %d)" % (len(CHECKS), MIN_CHECKS)) return 1 if total_sub < MIN_SUBASSERTIONS: print("FAIL anti-vacuity: only %d comparisons executed (floor %d)" % (total_sub, MIN_SUBASSERTIONS)) return 1 if failed: return 1 print("RESULT: all checks passed") return 0 if __name__ == "__main__": sys.exit(run_all())