null: artifact-seal — deterministic SHA-256 seal manifests with 44-check selftest
This commit is contained in:
1 parent
c3058d5e4b
commit
2a1ef2feaf
15 files changed
+1757
-99
No files matched your search
@@ -30,7 +30,12 @@ documented hole rather than an accident):
|
||||
set, and ordered ties would not be comparable) -- they are compared as multisets;
|
||||
* LIMIT/OFFSET only appear with an ORDER BY;
|
||||
* with GROUP BY, the select list is group keys plus aggregates only -- never a bare column
|
||||
of a multi-row group, whose value SQLite leaves unspecified.
|
||||
of a multi-row group, whose value SQLite leaves unspecified;
|
||||
* the NULL-tri-value families (`FALSE AND NULL`, `TRUE AND NULL`, `ON ... AND FALSE`) are
|
||||
reachable by the random walk, but the battery does not rely on that being true --
|
||||
`probes_null_logic.py` measures how often each fires, and it is the only thing standing
|
||||
between this file and an undetected blind spot of exactly the kind recorded in
|
||||
probe_and_null5.py.
|
||||
|
||||
Usage:
|
||||
python3 diff_sqlite.py [--queries N] [--seed S] [--mutants] [--max-print K] [--quiet]
|
||||
@@ -49,6 +54,15 @@ import minisql # noqa: E402
|
||||
|
||||
SHAPES = ("const", "scan", "where", "join", "group", "agg", "distinct")
|
||||
|
||||
# Every shape gets a FIXED slice of the run, not `i % len(SHAPES)` which needs a whole
|
||||
# multiple of 7 to be even. A thin slice is how the `like-case-sensitive` mutant went
|
||||
# unnoticed at 140 and 280 mutant queries: LIKE only appears inside a `predicate()` of
|
||||
# which 1 in 28 is LIKE, so with 20 queries per shape the mutant had ~1 chance in 14 to be
|
||||
# exercised at all. check_report() now refuses to pass unless every shape's slice is at
|
||||
# least this wide, so a thin shape can never hide a surviving mutant again.
|
||||
_QUERIES_PER_SHAPE = 20
|
||||
MIN_PER_SHAPE = 20
|
||||
|
||||
TEXT_POOL = ["apple", "banana", "cherry", "a", "ab", "", "A", "10", "20"]
|
||||
LIKE_POOL = ["%a%", "a%", "%b", "_", "ab", "A%", "%", "", "c_erry", "ap_le", "%rry"]
|
||||
NUM_LITERALS = ["0", "1", "-1", "2", "5", "10", "20", "3.5", "5.5", "0.0"]
|
||||
@@ -184,12 +198,11 @@ class Gen:
|
||||
def predicate(self, qualify, depth=0):
|
||||
r = self.rng.random()
|
||||
if depth >= 2:
|
||||
r = min(r, 0.59)
|
||||
r = min(r, 0.66)
|
||||
if r < 0.35:
|
||||
return self.comparison(qualify)
|
||||
if r < 0.45:
|
||||
return "(%s AND %s)" % (self.predicate(qualify, depth + 1),
|
||||
self.predicate(qualify, depth + 1))
|
||||
return self.conjunction(qualify)
|
||||
if r < 0.55:
|
||||
return "(%s OR %s)" % (self.predicate(qualify, depth + 1),
|
||||
self.predicate(qualify, depth + 1))
|
||||
@@ -218,6 +231,47 @@ class Gen:
|
||||
self.rng.choice(LIKE_POOL))
|
||||
return self.comparison(qualify)
|
||||
|
||||
def conjunction(self, qualify):
|
||||
"""`(X AND Y)` -- the shape where SQL's three-valued logic shows.
|
||||
|
||||
The right operand is an ordinary predicate most of the time, but two families are
|
||||
mixed in deliberately, because each is a place where an engine that gets the
|
||||
tri-value algebra wrong diverges from SQLite:
|
||||
|
||||
* a literal FALSE on the right (1 in 24) -- `FALSE AND NULL` is FALSE, so an
|
||||
engine where FALSE does not absorb NULL returns NULL and keeps a row in WHERE;
|
||||
* a NULL-producing predicate on the right (1 in 12) -- `TRUE AND NULL` is NULL, so
|
||||
an engine where NULL absorbs TRUE returns 1 and keeps a row SQLite drops.
|
||||
|
||||
Both are reachable by the random walk. probes_null_logic.py measures how often each
|
||||
actually fires instead of trusting the arithmetic.
|
||||
"""
|
||||
r = self.rng.random()
|
||||
if r < 1 / 24:
|
||||
right = "1 = 0"
|
||||
elif r < 1 / 24 + 1 / 12 and self.pool_all:
|
||||
right = self.null_predicate(qualify)
|
||||
else:
|
||||
right = self.predicate(qualify, 1)
|
||||
return "(%s AND %s)" % (self.predicate(qualify, 1), right)
|
||||
|
||||
def null_predicate(self, qualify):
|
||||
"""A predicate whose tri-value is NULL on rows where the referenced column is NULL:
|
||||
`x IS NULL`, `x IN (...)`, `x BETWEEN ... AND ...`, `x LIKE ...`."""
|
||||
tc = self.pick(self.pool_all)
|
||||
if tc is None:
|
||||
return self.comparison(qualify)
|
||||
r = self.rng.random()
|
||||
if r < 0.4:
|
||||
return "%s IS NULL" % self.ref(tc, qualify)
|
||||
if r < 0.6:
|
||||
items = ", ".join(self.num_literal() for _ in range(self.rng.randint(1, 2)))
|
||||
return "%s IN (%s)" % (self.ref(tc, qualify), items)
|
||||
if r < 0.8:
|
||||
return "%s BETWEEN %s AND %s" % (self.ref(tc, qualify),
|
||||
self.num_literal(), self.num_literal())
|
||||
return "%s LIKE '%s'" % (self.ref(tc, qualify), self.rng.choice(LIKE_POOL))
|
||||
|
||||
def comparison(self, qualify):
|
||||
"""The mixed-affinity forms are deliberate: they are where an engine that ignores
|
||||
SQLite's two affinity rules diverges from it."""
|
||||
@@ -337,6 +391,11 @@ class Gen:
|
||||
"%s.id = %s.id" % (t, u),
|
||||
"%s.id = %s.id AND %s" % (t, u, self.predicate(True)),
|
||||
"%s.%s = %s.id" % (t, rng.choice(cands), u),
|
||||
# ON = FALSE matches nothing, so every left row is NULL-extended. That is
|
||||
# the only place a LEFT JOIN can project a NULL that the ON clause did not
|
||||
# itself produce, and it is where a wrong three-valued AND is visible in a
|
||||
# query that has no WHERE at all.
|
||||
"%s.id = %s.id AND 1 = 0" % (t, u),
|
||||
])
|
||||
grouped = rng.random() < 0.5
|
||||
if grouped:
|
||||
@@ -445,7 +504,7 @@ def run_battery(queries, seed, max_print=5, quiet=False):
|
||||
rows_compared = nonempty = ordered_count = 0
|
||||
|
||||
for i in range(queries):
|
||||
shape = SHAPES[i % len(SHAPES)]
|
||||
shape = SHAPES[(i // _QUERIES_PER_SHAPE) % len(SHAPES)]
|
||||
spec, rows = make_schema(rng)
|
||||
con.close()
|
||||
con = sqlite3.connect(":memory:")
|
||||
@@ -476,7 +535,11 @@ def run_battery(queries, seed, max_print=5, quiet=False):
|
||||
report = {
|
||||
"queries": queries, "seed": seed, "shape_counts": shape_counts,
|
||||
"rows_compared": rows_compared, "nonempty": nonempty, "ordered": ordered_count,
|
||||
"mismatches": mismatches, "errors": errors,
|
||||
"mismatches": mismatches, "errors": errors, "mutant_queries": None,
|
||||
# run_battery() does not run mutants; it only measures engine-vs-oracle agreement.
|
||||
# Declaring that here is what lets check_report() enforce the floor when mutants
|
||||
# DO run without demanding a width a plain agreement run has no business carrying.
|
||||
"skip_mutant_floor": True,
|
||||
}
|
||||
if not quiet:
|
||||
print("minisql differential battery vs sqlite3 %s" % sqlite3.sqlite_version)
|
||||
@@ -510,6 +573,10 @@ def check_report(report):
|
||||
for shape in SHAPES:
|
||||
if report["shape_counts"][shape] == 0:
|
||||
bad.append("shape %s was never exercised" % shape)
|
||||
elif report["shape_counts"][shape] < MIN_PER_SHAPE:
|
||||
bad.append("shape %s got only %d queries (< %d) -- a thin slice can hide a "
|
||||
"surviving mutant"
|
||||
% (shape, report["shape_counts"][shape], MIN_PER_SHAPE))
|
||||
if report["nonempty"] < 0.5 * report["queries"]:
|
||||
bad.append("only %d/%d queries returned rows -- agreement would be too close to vacuous"
|
||||
% (report["nonempty"], report["queries"]))
|
||||
@@ -518,9 +585,41 @@ def check_report(report):
|
||||
% (report["rows_compared"], report["queries"]))
|
||||
if report["ordered"] < 0.2 * report["queries"]:
|
||||
bad.append("only %d ordered queries -- ORDER BY is barely covered" % report["ordered"])
|
||||
if report.get("skip_mutant_floor") is not True:
|
||||
if report.get("mutant_queries") is None:
|
||||
bad.append("report declares that it runs mutants but carries no mutant-queries "
|
||||
"width: without one of the two the floor is unenforced")
|
||||
else:
|
||||
bad.extend(check_mutant_queries(report["mutant_queries"]))
|
||||
return bad
|
||||
|
||||
|
||||
def mutant_floor():
|
||||
"""The smallest --mutant-queries that has been measured to catch every seeded defect.
|
||||
|
||||
This is not a constant someone guessed. Sweeping the slice at seed 20261022 with
|
||||
--queries 1400 gave, for the pre-fix engine: mq=140 -> 7/8 (and-null-absorbs-false
|
||||
survived), mq=280 and up -> 8/8. The post-fix engine catches 8/8 at every width
|
||||
measured, 140 through 1400. So the mutant slice is floored at the narrowest width
|
||||
that is known to catch all eight, and a caller asking for less is told the floor and
|
||||
refused -- otherwise the CLI default would be a configuration in which the battery
|
||||
passes on an engine that is broken, which is exactly the failure this floor exists to
|
||||
prevent. The floor is re-measured, not assumed: `measure_mutant_floor()` below sweeps
|
||||
candidate widths against the loaded module.
|
||||
"""
|
||||
return 280
|
||||
|
||||
|
||||
def check_mutant_queries(n):
|
||||
"""Failure strings for a mutant slice that is too thin to be trusted."""
|
||||
floor = mutant_floor()
|
||||
if n < floor:
|
||||
return ["--mutant-queries %d is below the measured floor %d: at the narrowest width "
|
||||
"measured to catch all seeded defects the floor is %d, and a thinner slice "
|
||||
"lets a surviving mutant read as PASS" % (n, floor, floor)]
|
||||
return []
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# mutation control: prove the battery can go RED
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -614,7 +713,9 @@ def main(argv=None):
|
||||
ap.add_argument("--seed", type=int, default=20261022)
|
||||
ap.add_argument("--mutants", action="store_true",
|
||||
help="also require every seeded engine defect to be caught")
|
||||
ap.add_argument("--mutant-queries", type=int, default=140)
|
||||
ap.add_argument("--mutant-queries", type=int, default=mutant_floor(),
|
||||
help="mutant slice width; floored at %d (measured: below that a "
|
||||
"surviving mutant reads as PASS)" % mutant_floor())
|
||||
ap.add_argument("--max-print", type=int, default=5)
|
||||
ap.add_argument("--quiet", action="store_true")
|
||||
args = ap.parse_args(argv)
|
||||
@@ -624,8 +725,9 @@ def main(argv=None):
|
||||
|
||||
mutants_caught = 0
|
||||
if args.mutants:
|
||||
bad.extend(check_mutant_queries(args.mutant_queries))
|
||||
if not args.quiet:
|
||||
print("\nmutation control (every seeded defect must be caught):")
|
||||
print("\nmutation control (every seeded engine defect must be caught):")
|
||||
results = run_mutants(args.mutant_queries, args.seed, quiet=args.quiet)
|
||||
survived = [n for n, _a, c in results if not c]
|
||||
mutants_caught = len(results) - len(survived)
|
||||
|
||||
Reference in new issue
Block a user