null: intervalkit — canonical half-open interval-set algebra, 12/12 checks + 7/7 mutants caught (641k differential comparisons)

This commit is contained in:
null committed 2026-10-22 22:49:02 +00:00
1 parent 37048766f4
commit e9d3283242
3 files changed
+1427

No files matched your search

+149
View File
@@ -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.
+463
View File
@@ -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())
@@ -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())