check: uncatchable evaluation errors abort the run instead of failing a case #17
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Bug
builtins.tryEvalcatchesthrowand a failedassertand nothing else, so apredicate that hits a type error,
builtins.head [], division by zero orinfinite recursion aborts the whole run rather than failing one case. The README
says so since #15; what it does not say is how often that happens, which decides
whether the current design is good enough.
Measurement
34 nixpkgs
libfunctions, one process per property, each asked only whether itproduces a value on input of the shape it declares. Outcomes classified by exit
code:
PASS,REPORTED(a counterexample came back),ABORT(the evaluationdied).
gen.anythingOf the properties that did not pass, 3 of 8 aborted with typed generators and
29 of 31 — 94% — with
gen.anything.So testing well-typed code works today. Sweeping an unfamiliar API, which is
what
examples/nixpkgs-lib-comprehensive.nixis for and what the README callsthe "run nixcheck against the whole surface" target, mostly does not: the
interesting outcomes are almost all aborts with no counterexample and no
shrinking. The curated examples abort zero times only because they were written
to pass.
The sharpest part is an asymmetry the caller cannot predict:
lib.head,lib.taillib.init,lib.last[]Same failure, opposite behaviour, purely because the latter pair use
throwwhile the former call a builtin. Whether a user gets a counterexample depends on
an implementation detail of the function under test.
Suggested fix
Diagnose out of process, on demand. An uncatchable error is invisible from
inside the evaluator but perfectly visible from outside it, and shrink-tree
nodes are already addressable as
(case index, path of child indices):peekreturns a node'svalueand child count without applying thepredicate, so it always evaluates.
judgeapplies the predicate and lets the process die; the exit code is theverdict.
Greedy descent over those verdicts reproduces shrinking at about one
nix evalper candidate, run only after a run has already aborted. Verified on a
division-by-zero predicate:
[1,0,1,4,4,4,4,4,1]shrank in 7 steps and 8judging processes.
This needs no library change —
sampleTreeplus the{ value; shrinks; }shapeis enough for a driver to walk the tree from the expression it passes to
nix eval.Note that the shrunk result above is
[0,0,0,0,0,0], not[0]. That is theexisting greedy shrinker finding a local minimum, as the README already
describes; out-of-process diagnosis makes it more visible but does not cause it.
If that does not work
Host the runner in Rust on the Nix C API. Catching a predicate's evaluation
error requires being the host, which is the same reason nix-unit links the
evaluator rather than shelling out to
nix eval.This is not a thin adapter: the predicate application, the case loop, the shrink
walk and the label/cover accumulation all move out, so
lib/property.nixgetsreimplemented. Generators, ranges, trees and the rng stay in Nix either way —
they are the user-facing API and they are good because they are Nix values.
The C API is the smaller surface than nix-unit's C++ route, but the Nix manual
still calls it in development and explicitly not stable, so this is a bet rather
than a free win. Only take it if the diagnosis driver proves unworkable in
practice.