. "$BST_LIB" # BST-TIMEOUT: 1200 # ONE python3 start, and that is the whole cost. The probe's # # own work is bounded by construction -- a 0.5 s contention # # timeout, a 1 s child timeout and a 30 s WNOHANG reap loop # # that cannot block -- so the interpreter load dominates, as # # it does in 99-python3-tomli-w and 99-python3-flit-core. # # Matched to their 1200 rather than undercut: this suite has # # the same one-start shape and no reason to be tighter. # # NOT MEASURED ON TARGET YET -- this port's first on-target # # run is the batch verify, and the measured elapsed goes in # # the test plan rather than in this comment, which would cost # # an image build to keep the shipped suite byte-identical. # # python3-filelock 3.13.4 (meta-openembedded meta-python, # python3-filelock_3.13.4.bb, `inherit pypi python_hatchling`) -- THE PLATFORM # INDEPENDENT FILE LOCK, on the wasm32-hwjs glibc image. It is the inter-process # mutex the PEP 517 packaging tools take before writing a shared cache, which is # what puts it in this Epic. # # ============================================================================ # WHY THIS PORT IS POSSIBLE AT ALL, AND WHAT THE .so CASE IS REALLY ASSERTING. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly). A module that # carried a compiled extension therefore could not be imported here -- not "would # be slow", could not be imported. filelock is pure Python, which is the entire # reason it ports. # # So `module-no-so` is not boilerplate. It is a REGRESSION TRIPWIRE on a version # bump: filelock grows no extension today, and the day a release adds a compiled # speedup this case goes red at INSTALL time rather than at `import filelock` on # somebody else's ticket three rounds later. It is anchored on the package's own # dpkg file list as well as on a find, because the two fail differently -- a find # catches a file the package does not declare, the list catches a declared file # that is not a .py. # # ============================================================================ # THE BACKEND SELECTION IS THE CASE THIS SUITE EXISTS FOR. # ============================================================================ # # filelock/__init__.py picks its implementation at import: # # if has_fcntl: _FileLock = UnixFileLock # flock(2) # else: _FileLock = SoftFileLock # atomic create, warns once # # and `has_fcntl` is just whether `import fcntl` succeeded. A wasm32 image can get # this WRONG SILENTLY -- the warning goes into a warnings filter nobody reads, and # every caller still gets an object with the same API that does not serialise the # same way. On this arm fcntl is compiled INTO the interpreter (there is no # lib-dynload to put it in), so the import succeeds and UnixFileLock is selected; # the EMPTY python3-fcntl package is a packaging consequence of the static-module # decision, not a missing module, and python3_%.bbappend's ALLOW_EMPTY is what # lets it exist. `backend` and `has-fcntl` below are what say that held. # # THAT ALSO DECIDES WHAT THE CONTENTION CASES MEAN. A flock is held by the OPEN # FILE DESCRIPTION rather than by the process, so a second open() of one path # contends even from a single pid -- which is why `contend-sameproc` is a real # case and not a tautology. fcntl(F_SETLK) record locks are per-PROCESS and would # quietly succeed instead; they are also refused outright on this arm # (brintos/linux#16, which took libutempter's utmp layer down one ticket earlier). # filelock picked flock, and 79-lockdev's port turns on the same distinction. # # ============================================================================ # THE LOCK FILE IS NOT DELETED ON RELEASE. THE TICKET SAYS IT IS. THE TICKET IS # WRONG, AND THIS SUITE ASSERTS WHAT THE LIBRARY ACTUALLY DOES. # ============================================================================ # # BRINTOS-1842's problem statement ends "and the file is gone after the context # manager exits". UnixFileLock._release does flock(LOCK_UN) + close(2) and # deliberately leaves the file, with the reason in a comment above it: unlinking # races a process that has already open()ed the file and is about to flock it, # which hands the lock to two holders at once (py-filelock issue #31). The base # class says the same in its own docstring -- "the lock file itself is not # automatically deleted". # # `lockfile-after-release` and `cm-file-after` therefore assert the file SURVIVES. # Writing the ticket's expectation into the suite instead would have shipped a # case that reds on a correct library, and it would have red for a reason no # reader could distinguish from a broken release path. # # `soft-file-after` is the control that makes this a property of the BACKEND # rather than of this filesystem: SoftFileLock, shipped by the same package, DOES # unlink on release, and it is asserted here doing so. # # ============================================================================ # NOTHING IN THIS SUITE BLOCKS WITHOUT A BOUND, AND THAT IS DELIBERATE. # ============================================================================ # # A lock is the one library whose failure mode is a HANG rather than a wrong # answer, and a suite that wedges the guest costs every sibling on a shared batch # image its round -- brintos_local_verify quarantines the wedger and re-boots # without it. So every acquire in the probe carries an explicit `timeout=`, and # the forked child is collected with waitpid(WNOHANG) in a bounded poll loop # rather than a blocking wait. This arm has a live defect of exactly that shape: # a foreground child is never reaped in the sigsuspend idiom # (linux-in-the-browser#161), which is also why timeout(1) appears nowhere here. # An unreapable child costs one key set to `no`, not the guest. # # fork() and waitpid() are compiled into this interpreter -- HAVE_FORK, # HAVE_WAITPID and HAVE_FLOCK are all set in the target pyconfig this build # produced, checked against it rather than assumed -- and fork+exec is known to # work on this arm (gnupg spawns gpg-agent that way, 80-gnupg). popen/system do # NOT (brintos/linux#14), which is why the child is a raw fork and not subprocess. # # ============================================================================ # WHAT WAS MEASURED, AND THE ONE CASE THAT WAS NOT. # ============================================================================ # # EVERY VALUE ASSERTED BELOW WAS MEASURED, on x86-64, against THIS EXACT PROBE and # against THE BYTES THIS RECIPE BUILT -- python3-filelock_3.13.4-r0_wasm32.deb # unpacked and put on PYTHONPATH, not a pip copy and not upstream git (recorded # 2026-08-31; the ticket's design doc carries the transcript). What makes the # control transferable is that all of it is pure Python over POSIX calls this # image implements; none of it depends on pointer width or the platform string. # # THE ONE EXCEPTION, STATED AS AN EXCEPTION: `flock-rootfs`. Every other lock here # is taken on the suite's scratch dir, which is tmpfs. That case takes one on # /var/lib -- the virtiofs cloud mount that is this image's real filesystem, and # the kind of place the packaging tools this package serves actually lock. flock # over a FUSE-backed mount is a capability the server has to advertise, so it can # legitimately be refused there while tmpfs works; filelock turns that into # NotImplementedError when flock answers ENOSYS. 79-lockdev's port records # flock() working on this VFS, which is the evidence this case is asserted on -- # but it is the one assertion here that the x86-64 control cannot reproduce, # because an unprivileged /var/lib answers EACCES at open() and says nothing about # flock at all. If it reds, `flock-rootfs` carries the kernel's own errno and # `rootfs-fstype` says what it was taken on, which is enough to attribute it # without a second boot. # # ============================================================================ # WHAT IS NOT COVERED, ON PURPOSE. # ============================================================================ # # - THREADS. filelock's context is thread-local by default and the library is # reentrant across threads when asked. A started thread never runs on this arm # (linux-in-the-browser#85), so a threading case would be a statement about # that defect rather than about this port. # - WindowsFileLock. Shipped, unreachable, and asserted only as a file that is # present and as a name in the module's public surface. # - is_singleton. A 3.13 constructor flag with no bearing on whether the port # works; the reentrancy this suite drives is the counter, which is the # mechanism the ticket names. # # THIS PORT CLOSES ON THIS SUITE: non-interactive, no manual browser pass, no # follow-on ticket under BRINTOS-780. P=python3-filelock # THE CANARY, which asserts nothing about python3-filelock. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE IS # COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require` guards deliberately, # because a `require` that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages FL=$SITE/filelock DI=$SITE/filelock-3.13.4.dist-info PKGLIST=/var/lib/dpkg/info/python3-filelock.list D=$BST_DATA PROBE=$D/python3-filelock-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-filelock.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------ the payload --- # Where the module has to land. `python3 -c 'import filelock'` further down is the # functional statement; these are the ones that say WHICH copy answered, so a # module that imported out of some other path is distinguishable from one that # imported out of the package this image installed. check $P/site-packages-dir test -d "$SITE" check $P/module-dir test -d "$FL" check $P/module-init test -f "$FL/__init__.py" # The whole of the module, file by file rather than by count: a count moves on any # upstream refactor, while each of these is a named part of the library and a # release that dropped one would be a different library. _api is the base class # and the counter, _unix is the flock backend this image selects, _soft is the # fallback the selector would reach for and the control for the unlink behaviour, # _error carries Timeout, _util the directory/writability helpers, _windows the # unreachable third backend, and version.py the setuptools_scm pin. check $P/module-api test -f "$FL/_api.py" check $P/module-unix test -f "$FL/_unix.py" check $P/module-soft test -f "$FL/_soft.py" check $P/module-error test -f "$FL/_error.py" check $P/module-util test -f "$FL/_util.py" check $P/module-windows test -f "$FL/_windows.py" check $P/module-version test -f "$FL/version.py" # The PEP 561 marker. filelock ships it, and it is the one non-.py, non-metadata # regular file in the package -- so it is named here rather than discovered as a # surprise by pkg-only-python below. check $P/module-py-typed test -f "$FL/py.typed" # NO COMPILED EXTENSION, ANYWHERE UNDER THE MODULE. See the header: on this image # a .so could not be loaded at all, so this is the case that fails a version bump # which grew one -- loudly, here, instead of at `import` on a later ticket. check_eq $P/module-no-so "" \ "$(find "$FL" -name '*.so' -o -name '*.so.*' 2>/dev/null)" # ...and the same assertion from the PACKAGE's side rather than the filesystem's. # A find sees what is on disk; the dpkg list sees what the package CLAIMS. A # compiled extension that arrived without being declared shows up in the first, # one that is declared but landed elsewhere shows up in the second. # # The list is copied to tmpfs first and never redirected from the rootfs: a # top-level stdin redirect on the virtiofs root makes dup2(2) return 513 and ash # EXITS, ending the suite silently (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS UNDER site-packages IS PYTHON OR ITS # METADATA. The two cases above name the failure that is expected; this one # catches the shape nobody predicted -- a binary, a .pyd, a shared archive -- # without having to enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so a name-only # filter would report every directory in the package as an unexpected shape; each # entry is therefore tested for being a regular file first. The allowed classes # were checked against the built .deb: seven .py files, seven __pycache__ .pyc, # py.typed, and four dist-info files -- METADATA, RECORD, WHEEL and # licenses/LICENSE -- and nothing else. py.typed is allowed by its extension # rather than by name, matching 99-python3-flit-core, so a second PEP 561 marker # would not be a red on its own. _bad="" while IFS= read -r _f; do case "$_f" in "$SITE"/*) ;; *) continue ;; esac [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc|*.typed) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ---------------------------------------------------------- the dist-info ---- # filelock ships a TOP-LEVEL .dist-info -- like python3-tomli-w and unlike # python3-flit-core, which ships none. All four files are asserted because they # are what makes the installed module discoverable by importlib.metadata; a # package whose module landed but whose metadata did not would import fine here # and be invisible to any dependency resolver on the image. Note that the licence # is under a `licenses/` SUBDIRECTORY here rather than at the top of the # dist-info: that is the modern hatchling layout, and asserting the path it is # actually at is what makes this a check rather than a guess. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check $P/dist-info-record test -f "$DI/RECORD" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-license test -f "$DI/licenses/LICENSE" # THE VERSION THE RECIPE PINS, from the metadata rather than from the module. # Together with $P/version below -- which reads filelock.__version__ -- this is # two independent statements of the same fact, and the two disagreeing would mean # a mixed install: a dist-info from one version beside a module from another. The # directory name carries it a third time, in $DI itself. check_out $P/dist-info-version "Version: 3.13.4" cat "$DI/METADATA" # --------------------------------------------------------- the package ------ # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files, so this is the one that says # python3-filelock is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. # # The stanza is read from a tmpfs copy for the same reason as the file list. The # anchor is exact rather than a substring match, which is the habit this suite # keeps whether or not a colliding name exists today. sed -n '/^Package: python3-filelock$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE RUNTIME DEPENDENCIES, as dpkg recorded them. This recipe declares NO target # DEPENDS of its own -- every DEPENDS it carries is a -native and resolves on the # build host -- so what dpkg holds is the python3-core every python package gets # and nothing else. That is asserted rather than assumed because it is the fact # that makes this port a leaf: it waits on no other module ticket in this Epic. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" # ---------------------------------------------------------- the interpreter -- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If filelock cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable. check $P/import python3 -c 'import filelock' # fcntl, asserted separately and before the probe, because it is what DECIDES the # backend (see the header). A red here with a green import would mean the library # imported and quietly selected SoftFileLock, and this is the case that would say # why every contention case below had changed meaning. check $P/fcntl-present python3 -c 'import fcntl' # ------------------------------------------------------------- the probe ----- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and keeps # BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" "$W" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the anchors keep `version` distinct from # `dist-info-version`, and `rootfs_lock` from `rootfs_lockfile`. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading an # absent file: each would red with got:'' and the set would read exactly like a # broken lock, when what actually happened is that no measurement was taken. The # word `unscorable` is PARSED -- brintos_local_verify re-measures a suite that # prints it on a fresh guest rather than trusting a shared boot's numbers # (BRINTOS-1644) -- so it goes in this structural check, not in a comment. # # The cases below still print their reds: withdraw, do not hide. This case is what # says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # THE VERSION THE RECIPE PINS, read from the module. A mismatch here means the # recipe and what actually landed have gone out of step, and every expectation in # this file is then describing a different filelock. check_eq $P/version "3.13.4" "$(pv version)" # The module answered out of the package this image installed, not from a stray # copy on sys.path. This is what makes the payload cases above and the behaviour # cases below statements about the SAME filelock. check_eq $P/module-file "$FL/__init__.py" "$(pv module_file)" # THE PUBLIC SURFACE, WHOLE. Asserted as a set rather than by presence because a # release that grew or dropped a public name would be a different library than the # one this file describes. It is the module's real dir() rather than its __all__, # which is why the module-level imports appear in it: that is what a caller # actually sees, and pinning what was measured beats pinning what is tidy. check_eq $P/api-surface \ "AcquireReturnProxy,BaseFileLock,FileLock,SoftFileLock,TYPE_CHECKING,Timeout,UnixFileLock,WindowsFileLock,annotations,has_fcntl,sys,version,warnings" \ "$(pv api_surface)" # ----------------------------------------------------- THE BACKEND SELECTION # The header's argument, in three cases. `has-fcntl` is the input the selector # reads, `backend` is what it chose, and `lock-is-unix` says the object this probe # then drove is really that class rather than a name that happens to match. # fcntl-file records that the module is BUILT IN on this arm -- a static extension # has no __file__, and that absence is the expected value here, not a failure. check_eq $P/has-fcntl "yes" "$(pv has_fcntl)" check_eq $P/backend "UnixFileLock" "$(pv backend)" check_eq $P/lock-is-unix "yes" "$(pv lock_is_unix)" check_eq $P/fcntl-builtin "" "$(pv fcntl_file)" # The scratch dir this suite locks on is tmpfs, which is what makes every case # below except flock-rootfs a statement about the library rather than about the # cloud mount. A red here would not be a filelock defect and would explain any # lock case that reds beside it. check_eq $P/tmp-fstype "tmpfs" "$(pv tmp_fstype)" # ------------------------------------------------------------ acquire/release # The lifecycle the ticket names, one fact per case so a red says WHICH step # stopped working rather than that "the lock did not work". check_eq $P/lockfile-absent-before "no" "$(pv lockfile_pre_exists)" check_eq $P/locked-before "no" "$(pv locked_before)" check_eq $P/acquire "yes" "$(pv acquire_ok)" check_eq $P/is-locked-after-acquire "yes" "$(pv locked_after_acquire)" # ACQUIRING CREATES THE LOCK FILE -- the ticket's first requirement, and the one # that says the lock has a filesystem existence another process could find. check_eq $P/lockfile-created "yes" "$(pv lockfile_after_acquire)" check_eq $P/counter-after-acquire "1" "$(pv counter_after_acquire)" # RE-ENTRANT WITHIN ONE PROCESS. A second acquire on the SAME object counts rather # than deadlocking against itself, which is the whole reason the counter exists. check_eq $P/reacquire "yes" "$(pv reacquire_ok)" check_eq $P/counter-after-reacquire "2" "$(pv counter_after_reacquire)" check_eq $P/is-locked-reentrant "yes" "$(pv locked_after_reacquire)" # ...and the release is counted too. The FIRST release must NOT drop the lock: # a library that released on the inner exit would hand the file to another process # while the outer scope still believed it held it, which is a silent corruption # rather than a visible failure. check_eq $P/counter-after-release1 "1" "$(pv counter_after_release1)" check_eq $P/still-locked-after-release1 "yes" "$(pv locked_after_release1)" check_eq $P/counter-after-release2 "0" "$(pv counter_after_release2)" # RELEASING CLEARS THE LOCK -- the ticket's third requirement. check_eq $P/released "no" "$(pv locked_after_release2)" # THE FILE SURVIVES THE RELEASE. See the header: upstream leaves it deliberately, # against py-filelock issue #31, and the ticket's problem statement expects the # opposite. This case asserts the library's real contract. check_eq $P/lockfile-after-release "yes" "$(pv lockfile_after_release)" # ------------------------------------------------------- the context manager # The documented idiom, on a path of its own so it cannot be confused with the # explicit acquire/release above. check_eq $P/cm-locked-inside "yes" "$(pv cm_locked_inside)" check_eq $P/cm-file-inside "yes" "$(pv cm_file_inside)" check_eq $P/cm-released "no" "$(pv cm_locked_after)" # The same persistence fact, through the idiom the ticket asks about by name. check_eq $P/cm-file-after "yes" "$(pv cm_file_after)" # ------------------------------------------------- contention, same process # Two separate FileLock objects on one path contend, because a flock belongs to # the open file description and not to the process. See the header for why this is # a real case rather than a tautology, and for what fcntl record locking would # have done instead. check_eq $P/outer-holds "yes" "$(pv outer_locked)" check_eq $P/contend-sameproc "Timeout" "$(pv contend_sameproc)" # It WAITED rather than refusing instantly. A backend that raised Timeout without # polling would satisfy the case above and be a different behaviour. check_eq $P/contend-waited "yes" "$(pv contend_waited)" check_eq $P/contend-not-locked "no" "$(pv contend_rival_locked)" # The counter was UNWOUND by the failed acquire. acquire() increments before it # tries, so a library that forgot to decrement on the Timeout path would leave a # lock nobody can ever fully release -- silent, and visible only here. check_eq $P/contend-counter-unwound "0" "$(pv contend_rival_counter)" # blocking=False refuses immediately: the other documented entry point, and the # one a caller uses when it wants to do something else instead of waiting. check_eq $P/nonblocking "Timeout" "$(pv contend_nonblocking)" check_eq $P/nonblocking-fast "yes" "$(pv contend_nonblocking_fast)" # ------------------------------------------------------- contention, a child # THE TICKET'S OWN CASE: a second FileLock on the same path, in a FORKED CHILD, # times out with Timeout while the parent holds it. See the header for why the # child talks through a file and is reaped with WNOHANG. check_eq $P/parent-holds "yes" "$(pv parent_holds_for_child)" check_eq $P/fork "yes" "$(pv fork_supported)" check_eq $P/child-timeout "Timeout" "$(pv child_verdict)" # REAPING IS ASSERTED SEPARATELY FROM THE VERDICT, and that split is the point: on # this arm a child that answered correctly and then could not be collected is a # platform finding (linux-in-the-browser#161's neighbourhood), not a lock failure, # and the two must be able to red independently. check_eq $P/child-reaped "yes" "$(pv child_reaped)" check_eq $P/child-exit "0" "$(pv child_exit)" # THE LOCK IS ACTUALLY RELEASABLE AND THE PATH IS THEN FREE. Without this, every # Timeout above would also be satisfied by a lock that can never be taken at all # -- a broken mutex that passes a contention test. check_eq $P/outer-released "yes" "$(pv outer_released)" check_eq $P/acquire-after-release "ok" "$(pv acquire_after_release)" # ------------------------------------------------------ the lock on the ROOTFS # THE ONE CASE THE x86-64 CONTROL COULD NOT REPRODUCE -- see the header. Every # lock above is on tmpfs; this one is on /var/lib, the virtiofs cloud mount that # is this image's real filesystem and the kind of place the packaging tools this # package serves actually lock. If it reds it carries the kernel's own errno, and # rootfs-fstype says what it was taken on. check_eq $P/rootfs-fstype "virtiofs" "$(pv rootfs_fstype)" check_eq $P/flock-rootfs "ok" "$(pv rootfs_lock)" check_eq $P/rootfs-lockfile "yes" "$(pv rootfs_lockfile)" # ------------------------------------------------------------ the soft backend # The fallback the selector would reach for if fcntl ever went missing, exercised # once so that it is known to work -- and, more usefully, as the CONTROL that # makes lockfile-after-release above a property of the flock backend rather than # of this filesystem: SoftFileLock does unlink on release, on the same tmpfs, in # the same run. check_eq $P/soft-file-inside "yes" "$(pv soft_file_inside)" check_eq $P/soft-file-removed "no" "$(pv soft_file_after)" # The probe ran to the end. Every case above greps a key, so a probe that died # halfway would leave the remaining keys absent and each case would red with # got:'' -- this case is the one that says the cause was a truncated run. check_eq $P/probe-complete "yes" "$(pv done)" rm -rf "$W"