diff --git a/wizard-work/null/intervalkit/README.md b/wizard-work/null/intervalkit/README.md new file mode 100644 index 0000000..4ba3eb0 --- /dev/null +++ b/wizard-work/null/intervalkit/README.md @@ -0,0 +1,149 @@ +# intervalkit — canonical half-open interval-set algebra + +Stdlib only, no dependencies. Two files: the library and its harness. + + intervalkit.py the library + a small CLI + test_intervalkit.py the verification harness (run it: exit 0 = correct) + +## The model + +A *set* is a finite sequence of half-open intervals `[lo, hi)`. `lo` and `hi` +are numbers or `None`; `None` on the left is `-inf`, on the right is `+inf`. +The empty set is the empty sequence. + +A sequence is **canonical** iff it is sorted by `lo`, every interval is +non-degenerate (`lo < hi`), and consecutive intervals are strictly separated +(`next.lo > prev.hi`). Every set has exactly one canonical form. + +Touching intervals merge (`[1,2) U [2,3) == [1,3)`); a one-unit gap does not +(`[1,2) U [3,4)` stays two intervals). + +## API + + normalize(a) unique canonical form; idempotent, order-blind + union(*sets) canonical union + intersection(*sets) canonical intersection + difference(a, b) a minus b + symmetric_difference(a, b) points in exactly one + complement(a) over the whole line (-inf, +inf); an involution + contains(a, x) half-open membership + measure(a) total length; inf when unbounded + from_points(points) integer runs -> canonical set + sample_points(a, lo, hi) integers in the window that are in the set + is_canonical(a) structural predicate + assert_canonical(a) raises AssertionError with detail + parse(text) / format_set(a) "[1,3) U [5,+inf)" <-> canonical set + main(argv) CLI entry point + +CLI: + + $ python3 intervalkit.py union "[1,3)" "[2,5)" + [1,5) + $ python3 intervalkit.py complement "[1,3)" + [-inf,1) U [3,+inf) + $ python3 intervalkit.py contains "[1,3)" 3 + false + $ python3 intervalkit.py measure "[-inf,1)" + inf + +## Guarantees (and what is deliberately excluded) + +* `normalize` is idempotent and order-independent; it is the unique normal form. +* `union`, `intersection`, `difference`, `symmetric_difference` and `complement` + are total, closed, and always return canonical sets. +* No operation mutates its input, and none invents an endpoint: every result is + built from max/min combinations of input endpoints. Integer endpoints stay + integer, and `Fraction` endpoints stay exact (`measure` of `[1/3, 2/3)` is + exactly `1/3`). +* **Degenerate intervals (single points) are not representable.** `[x, x)` is + rejected. That is the price of a half-open model, and it is what makes every + operation above total and closed. `{0, 2}` is therefore not expressible; + `{0, 1, 2}` is, as `[0, 3)`. Malformed input raises `IntervalError` — it is + never silently coerced. + +## Verification + + python3 test_intervalkit.py # exit code IS the verdict: 0 = correct + +Last run: **12/12 checks passed, 641,123 comparisons executed, ~13 s.** + +### Why the differential test is exact, not sampled + +Membership over the integers is decided by `in_set` — an independent five-line +predicate written in the harness, not imported from the library. Operations are +compared against set operations on bitmaps over the universe `[-24, 25)`. + +This is exact because the algebra is endpoint-based: results are max/min +combinations of input endpoints, so integer endpoints never become +non-integers, and a half-open interval with integer endpoints covers exactly +its integer points. The integer bitmap therefore *is* the set, with no sampling +error and no tolerance. + +The real-line claim (dense, unbounded domain) rests on the same endpoint +arithmetic plus the density argument, not on a separate test. The library is +declared over the reals; the oracle pins the integer-endpoint case. + +### What the harness checks + +| # | check | volume | +|---|-------|--------| +| 1 | fuzz: real implementation, 0 semantic / 0 structural failures | 3000 trials, 75,750 comparisons | +| 2 | teeth: 7/7 mutants detected, each in the expected leg | 7 x 3000 trials | +| 3 | laws: De Morgan (both ways), involution, idempotence, commutativity, associativity, `a == (a n b) U (a \ b)`, `a n ~a == {}`, `a U ~a == line`, measure additivity | 600 random triples | +| 4 | laws: `normalize` idempotent + order-independent; malformed input rejected; regression cases | 600 random sets | +| 5 | boundary: touching, gaps, shared endpoints, unbounded bounds, full line, measure, points | 30 hand-checked cases | +| 6 | boundary: text parse/format round-trips, 11 rejection cases | 29 cases | +| 7 | points: `from_points`/`sample_points` vs the bitmap oracle | 300 point sets | +| 8 | endpoints: no invented endpoints; float and `Fraction` exactness | 400 random pairs | +| 9 | scale: 1500 x 1500 intervals vs brute-force enumeration | 600-point universe | +| 10 | CLI: subprocess smoke over real argv, stdout, exit codes, rejection | 16 cases | +| 11 | CLI: exit code 0 and the value on stdout | 3 cases | +| 12 | oracle self-check: `in_set` vs `ik.contains`; bitmap size vs real measure | 500 sets x 49 points | + +Fuzz coverage floors (anti-vacuity): 2049 trials with unbounded bounds, 2288 +with a merge, 830 with multi-interval results, 1113 with an empty set, 1052 +distinct canonical sets. + +### The teeth + +A harness that cannot fail proves nothing. Seven deliberately broken +implementations are pushed through the *same* oracle; all seven must be caught, +and in the leg that is supposed to see them (semantic = bitmap mismatch or +crash, structural = result not canonical): + +| mutant | semantic | structural | expected | +|--------|---------:|-----------:|----------| +| `normalize`: strict-overlap merge (misses touching) | 145 | 118 | structural | +| `intersection`: always advance the left cursor | 270 | 0 | semantic | +| `complement`: bounds written to wrong slots | 3000 | 0 | semantic | +| `measure`: counts integer slots (`hi - lo - 1`) | 1032 | 0 | semantic | +| `contains`: ignores the unbounded lower bound | 197 | 0 | semantic | +| `union`: unnormalized concatenation | 0 | 1835 | structural | +| `normalize`: pre-fix body, duplicate `-inf` lower bound crashes | 145 | 0 | semantic | + +The last mutant is the pre-fix body of `ik.normalize`; the fuzz found that crash +in the real library. Two bugs were found this way while the suite was being +written, and both are now pinned by named regression cases in check 4: + +1. `normalize` compared a second `-inf` lower bound with `<=` and raised + `TypeError`. Reachable from `union`/`symmetric_difference`, which concatenate + normal forms and re-normalize. Fix: treat `lo is None` as "always joins". +2. `is_canonical([(None, 5), (1, 2)])` raised `TypeError` instead of returning + `False`. Fix: only compare `plo <= lo` when `plo` is not `None`. + +### Anti-vacuity guard + +The runner counts executed comparisons and refuses to exit 0 if fewer than +12 checks are registered or fewer than 100,000 comparisons were actually +executed. A test run that silently stops asserting cannot report success. + +## Limits, stated plainly + +* The exact differential oracle is over integer endpoints on a bounded + universe. Unbounded and float/`Fraction` behaviour is covered by hand-checked + cases, algebraic laws and endpoint-preservation properties, not by the bitmap + oracle. +* `contains` and `measure` are linear scans; `measure` normalizes first so it + cannot double-count overlapping parts. There is no interval tree here — this + is an algebra, not an index. +* Degenerate (single-point) intervals are out of scope by design, as above. diff --git a/wizard-work/null/intervalkit/intervalkit.py b/wizard-work/null/intervalkit/intervalkit.py new file mode 100644 index 0000000..ec09892 --- /dev/null +++ b/wizard-work/null/intervalkit/intervalkit.py @@ -0,0 +1,463 @@ +#!/usr/bin/env python3 +"""intervalkit -- canonical algebra of half-open interval sets over the reals. + +Stdlib only, no third-party dependencies. + +Model +===== +A *set* is a finite sequence of half-open intervals ``[lo, hi)`` where ``lo`` +and ``hi`` are numbers or ``None``. ``None`` in the ``lo`` slot means +-unbounded (-inf); ``None`` in the ``hi`` slot means +unbounded (+inf). The +empty set is the empty sequence. + +A sequence is *canonical* iff it is sorted by ``lo``, every interval is +non-degenerate (``lo < hi``), and consecutive intervals are strictly separated +(``next.lo > prev.hi``). Every set has exactly one canonical form, computed by +:func:`normalize`. + +Guarantees +========== +* ``normalize`` is idempotent and returns the unique canonical form. +* ``union``, ``intersection``, ``difference``, ``symmetric_difference`` and + ``complement`` are total, closed, and always return canonical sets. +* No operation mutates its inputs and none invents endpoints: every result is + built from max/min combinations of input endpoints, so integer endpoints stay + integers. +* ``complement`` is taken over the whole line (-inf, +inf) and is an involution. +* Touching intervals merge: ``[1,2) U [2,3) == [1,3)``. A one-unit gap does + not: ``[1,2) U [3,4)`` stays two intervals. + +Deliberate limit +================ +Degenerate intervals (single points, ``[x, x)``) are not representable. That +is the price of a half-open model, and it is what makes every operation above +total and closed. A point set such as {0, 2} is therefore *not* expressible; +{0, 1, 2} is, as ``[0, 3)``. +""" + +from __future__ import annotations + +import math +import re +import sys + +__all__ = [ + "IntervalError", + "normalize", + "canonical", + "is_canonical", + "assert_canonical", + "union", + "intersection", + "difference", + "symmetric_difference", + "complement", + "contains", + "measure", + "sample_points", + "from_points", + "parse", + "format_set", + "main", +] + + +class IntervalError(ValueError): + """Raised for malformed intervals, sets or interval text.""" + + +# -------------------------------------------------------------------------- +# bounds helpers. In this module None in the lo slot means -inf and None in +# the hi slot means +inf. Two different combination rules follow from that. +# -------------------------------------------------------------------------- + +def _none_loses(a, b, fn): + """Combine bounds where None means -inf (max) or +inf (min): None loses.""" + if a is None: + return b + if b is None: + return a + return fn(a, b) + + +def _max_lo(a, b): + return _none_loses(a, b, max) + + +def _min_hi(a, b): + return _none_loses(a, b, min) + + +def _max_hi(a, b): + """Combine upper bounds where None means +inf: None wins.""" + if a is None or b is None: + return None + return max(a, b) + + +def _valid_bounds(lo, hi): + if lo is None or hi is None: + return True + try: + return bool(lo < hi) + except TypeError: + raise IntervalError("endpoints are not comparable: %r, %r" % (lo, hi)) + + +def _bounds(iv): + """Validate one interval and return it as a ``(lo, hi)`` tuple.""" + if isinstance(iv, (str, bytes)): + raise IntervalError("interval must be a (lo, hi) pair, got %r" % (iv,)) + try: + n = len(iv) + except TypeError: + raise IntervalError("interval must be a (lo, hi) pair, got %r" % (iv,)) + if n != 2: + raise IntervalError("interval must have exactly 2 endpoints, got %d in %r" % (n, iv)) + lo, hi = iv[0], iv[1] + if not _valid_bounds(lo, hi): + raise IntervalError( + "empty or ill-ordered interval %r; the empty set is the empty sequence" % (iv,) + ) + return lo, hi + + +def _sort_key(iv): + lo = iv[0] + return (0, 0) if lo is None else (1, lo) + + +# -------------------------------------------------------------------------- +# core +# -------------------------------------------------------------------------- + +def normalize(intervals): + """Return the unique canonical form of ``intervals``. + + Accepts any sequence of valid (possibly overlapping, unsorted) intervals. + Idempotent. Raises :class:`IntervalError` on a malformed interval. + """ + if intervals is None: + raise IntervalError("expected a sequence of intervals, got None") + items = [] + for iv in intervals: + items.append(_bounds(iv)) + if not items: + return () + try: + items.sort(key=_sort_key) + except TypeError: + raise IntervalError("endpoints are not mutually comparable: %r" % (items,)) + out = [items[0]] + for lo, hi in items[1:]: + plo, phi = out[-1] + # Sorted by lo, so this interval starts at or after the open one. It + # joins when it starts no later than the open interval ends; a second + # -inf lower bound (lo is None) always joins, since -inf <= anything. + if phi is None or lo is None or lo <= phi: + out[-1] = (plo, _max_hi(phi, hi)) + else: + out.append((lo, hi)) + return tuple(out) + + +#: Public alias: ``canonical`` reads better at call sites that just want form. +canonical = normalize + + +def is_canonical(intervals): + """True iff ``intervals`` is exactly the canonical form of its own set.""" + try: + items = [_bounds(iv) for iv in intervals] + except IntervalError: + return False + for k in range(1, len(items)): + plo, phi = items[k - 1] + lo, hi = items[k] + if lo is None: + return False # an unbounded-below interval must be first + if phi is None: + return False # nothing can follow an unbounded-above interval + if plo is not None and not (plo <= lo): + return False # not sorted + if not (lo > phi): + return False # touching or overlapping: not separated + return True + + +def assert_canonical(intervals): + """Raise ``AssertionError`` with detail unless ``intervals`` is canonical.""" + if not is_canonical(intervals): + raise AssertionError( + "not canonical: %r (normal form is %r)" % (tuple(intervals), normalize(intervals)) + ) + return tuple(intervals) + + +def union(*sets): + """Canonical union of any number of sets.""" + acc = [] + for s in sets: + acc.extend(normalize(s)) + return normalize(acc) + + +def _intersect_two(a, b): + res = [] + i = j = 0 + while i < len(a) and j < len(b): + alo, ahi = a[i] + blo, bhi = b[j] + lo = _max_lo(alo, blo) + hi = _min_hi(ahi, bhi) + if lo is None or hi is None or lo < hi: + res.append((lo, hi)) + # advance whichever interval ends first (on a tie, advance the left) + if ahi is None: + j += 1 + elif bhi is None: + i += 1 + elif ahi <= bhi: + i += 1 + else: + j += 1 + return tuple(res) + + +def intersection(*sets): + """Canonical intersection of any number of sets.""" + if not sets: + return ((None, None),) # intersection of nothing is the whole line + acc = normalize(sets[0]) + for s in sets[1:]: + acc = _intersect_two(acc, normalize(s)) + if not acc: + return () + return acc + + +def complement(a): + """Complement of ``a`` over the whole line (-inf, +inf). Involution.""" + a = normalize(a) + out = [] + if a and a[0][0] is not None: + out.append((None, a[0][0])) + for k in range(len(a) - 1): + out.append((a[k][1], a[k + 1][0])) + if a and a[-1][1] is not None: + out.append((a[-1][1], None)) + if not a: + out.append((None, None)) + return normalize(out) + + +def difference(a, b): + """``a`` minus ``b``, canonical.""" + return _intersect_two(normalize(a), complement(b)) + + +def symmetric_difference(a, b): + """Points in exactly one of ``a``, ``b``, canonical.""" + return normalize(list(difference(a, b)) + list(difference(b, a))) + + +def contains(a, x): + """True iff the number ``x`` is in the set ``a``. + + Works on any valid sequence of intervals, canonical or not. + """ + for lo, hi in a: + lo, hi = _bounds((lo, hi)) + if (lo is None or lo <= x) and (hi is None or x < hi): + return True + return False + + +def measure(a): + """Total length of ``a``; ``inf`` when any interval is unbounded.""" + total = 0 + for lo, hi in normalize(a): + if lo is None or hi is None: + return math.inf + total += hi - lo + return total + + +def sample_points(a, lo, hi): + """The integers ``x`` with ``lo <= x < hi`` that are in the set ``a``.""" + return [x for x in range(lo, hi) if contains(a, x)] + + +def from_points(points): + """Canonical set from integer points; runs of consecutive integers merge.""" + xs = sorted(set(points)) + if not xs: + return () + ivs = [] + start = prev = xs[0] + for x in xs[1:]: + if x == prev + 1: + prev = x + continue + ivs.append((start, prev + 1)) + start = prev = x + ivs.append((start, prev + 1)) + return normalize(ivs) + + +# -------------------------------------------------------------------------- +# text: "[1,3) U [5,+inf)" <-> canonical sets +# -------------------------------------------------------------------------- + +_TOKEN_RE = re.compile(r"\[\s*([^,\]]+?)\s*,\s*([^,\]]+?)\s*\)") +_SEP_RE = re.compile(r"\s*(?:U|\u222a|\||;)\s*") +_EMPTY_WORDS = ("", "{}", "empty", "EMPTY", "none", "NONE") + + +def _num_lo(s): + t = s.strip().lower() + if t in ("-inf", "-infinity", "-oo"): + return None + if t in ("+inf", "inf", "infinity", "+oo", "oo"): + raise IntervalError("a lower bound cannot be +inf: %r" % s) + return _number(t, s) + + +def _num_hi(s): + t = s.strip().lower() + if t in ("+inf", "inf", "infinity", "+oo", "oo"): + return None + if t in ("-inf", "-infinity", "-oo"): + raise IntervalError("an upper bound cannot be -inf: %r" % s) + return _number(t, s) + + +def _number(t, original): + if re.fullmatch(r"[+-]?\d+", t): + return int(t) + try: + return float(t) + except ValueError: + raise IntervalError("bad endpoint: %r" % original) + + +def parse(text): + """Parse interval text into a canonical set. + + Accepts ``[1,3)``, ``(-inf,2)`` as ``[-inf,2)``-style unbounded bounds + spelled ``-inf`` / ``+inf``, separated by ``U``, ``|`` or ``;``, plus the + empty-set spellings ``{}`` / ``empty``. + """ + if not isinstance(text, str): + raise IntervalError("expected interval text, got %r" % (text,)) + s = text.strip() + if s in _EMPTY_WORDS: + return () + ivs = [] + for piece in _SEP_RE.split(s): + piece = piece.strip() + if not piece: + continue + m = _TOKEN_RE.fullmatch(piece) + if not m: + raise IntervalError( + "not a half-open interval: %r (this algebra is half-open: " + "lower '[' , upper ')' , or use -inf / +inf)" % piece + ) + ivs.append((_num_lo(m.group(1)), _num_hi(m.group(2)))) + if not ivs: + raise IntervalError("no interval found in %r" % text) + return normalize(ivs) + + +def _fmt_num(x): + if isinstance(x, float) and x.is_integer(): + return str(int(x)) + return repr(x) if isinstance(x, float) else str(x) + + +def format_set(a): + """Canonical set as text: ``[1,3) U [5,+inf)``, or ``{}`` when empty.""" + a = normalize(a) + if not a: + return "{}" + parts = [] + for lo, hi in a: + parts.append( + "[%s,%s)" + % ("-inf" if lo is None else _fmt_num(lo), "+inf" if hi is None else _fmt_num(hi)) + ) + return " U ".join(parts) + + +# -------------------------------------------------------------------------- +# CLI +# -------------------------------------------------------------------------- + +_USAGE = """\ +usage: intervalkit.py OP ARGS... + + union A B [C ...] canonical union + intersection A B [..] canonical intersection + difference A B A minus B + symdiff A B points in exactly one of A, B + complement A complement over (-inf, +inf) + contains A X print true / false + measure A print total length (inf when unbounded) + parse TEXT parse TEXT and print its canonical form + +A set is written "[1,3) U [5,+inf)" or "{}" for the empty set. +Endpoints are numbers, -inf or +inf; the algebra is half-open. +""" + +_OPS = ("union", "intersection", "difference", "symdiff", "complement", + "contains", "measure", "parse") + + +def _need(args, n, op): + if len(args) < n: + raise IntervalError("%s needs at least %d argument(s)" % (op, n)) + + +def main(argv=None): + argv = list(sys.argv[1:] if argv is None else argv) + if not argv or argv[0] in ("-h", "--help", "help"): + sys.stdout.write(_USAGE) + return 0 + op, args = argv[0], argv[1:] + try: + if op not in _OPS: + raise IntervalError("unknown op %r" % op) + if op == "parse": + _need(args, 1, op) + out = format_set(parse(" ".join(args))) + elif op == "union": + _need(args, 1, op) + out = format_set(union(*[parse(a) for a in args])) + elif op == "intersection": + _need(args, 1, op) + out = format_set(intersection(*[parse(a) for a in args])) + elif op == "difference": + _need(args, 2, op) + out = format_set(difference(parse(args[0]), parse(args[1]))) + elif op == "symdiff": + _need(args, 2, op) + out = format_set(symmetric_difference(parse(args[0]), parse(args[1]))) + elif op == "complement": + _need(args, 1, op) + out = format_set(complement(parse(args[0]))) + elif op == "contains": + _need(args, 2, op) + out = "true" if contains(parse(args[0]), _number(args[1].strip().lower(), args[1])) else "false" + else: # measure + _need(args, 1, op) + out = str(measure(parse(args[0]))) + except IntervalError as exc: + sys.stderr.write("error: %s\n\n%s" % (exc, _USAGE)) + return 2 + sys.stdout.write(str(out) + "\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wizard-work/null/intervalkit/test_intervalkit.py b/wizard-work/null/intervalkit/test_intervalkit.py new file mode 100644 index 0000000..b1944df --- /dev/null +++ b/wizard-work/null/intervalkit/test_intervalkit.py @@ -0,0 +1,815 @@ +#!/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())