a73x

0b12f4e3

test: real_path refuses a missing parent on perl too, and timeout has a pin

a73x   2026-09-04 10:16

Commit message
test: real_path refuses a missing parent on perl too, and timeout has a pin

`readlink -f` states the rule both arms owe: every component but the last
must exist. The perl arm only half kept it. When the whole path will not
resolve it fell back to Cwd::realpath on the parent and accepted any defined
answer — and Cwd::realpath is not the same function on every perl. On 5.42
with Cwd 3.94, realpath("/nope") is "/nope", so `real_path /nope/x` printed
the path and exited 0 where readlink -f prints nothing and exits 1. The
parent is now checked with -d, which is what "must exist" meant.

Round 2 said it had verified this against readlink -f and had not: its
missing-parent case was /nope/nope/x, two levels deep, where Cwd::realpath
does refuse. One level is where the arms part, and holding the depth at two
is how the check passed while the contract was broken.

Both branches are pinned now, so neither arm can drift again: an unwritten
leaf under a parent that exists must answer an absolute path ending in that
name, and a path whose parent does not exist must answer nothing with a
nonzero status.

`timeout` gets the five-case pin its comment always implied. It is GNU's
binary here and perl's alarm on a bare Mac, and nothing else committed calls
that arm, so it shipped unrun. The expiry case is the only one that waits and
every arm takes a fractional budget, so the whole block costs a third of a
second. The call sits in a redirected subshell that ends in `exit`, because
GNU timeout re-raises the signal that killed its child and the shell that
waited then prints "Terminated" into the middle of an e2e log.

The comment claiming the perl arm "is GNU timeout's contract and not an
approximation" now says what is true: it answers GNU's exit contract for
those five cases, and it is not GNU in three measured ways no caller here
reaches — 127 rather than 126 for a file that is not executable, no relay of
a signal sent to the wrapper, and no process group, so a grandchild outlives
the budget.

And the one assertion covering real_path could pass with both sides empty:
`pid_exe` and `real_path` were compared with `=` and no guard, so a box where
lsof and perl both went silent read ORACLE_OK.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/oracle_selftest.sh
Old New
@@ -138,9 +138,14 @@ time.sleep(300)' \
138 # real_path on both sides because a distribution that puts /bin on a 138 # real_path on both sides because a distribution that puts /bin on a
139 # symlink to /usr/bin, and a Darwin lsof that canonicalizes /tmp to 139 # symlink to /usr/bin, and a Darwin lsof that canonicalizes /tmp to
140 # /private/tmp, both answer the resolved path and not the spelling. 140 # /private/tmp, both answer the resolved path and not the spelling.
141 [ "$(pid_exe "$_okid")" = "$(real_path "$(command -v sleep)")" ] || { 141 _oexe=$(pid_exe "$_okid") || _oexe=''
142 echo "e2e FAIL: oracle: pid_exe of the subject's sleep is '$(pid_exe "$_okid")'," 142 _owant=$(real_path "$(command -v sleep)") || _owant=''
143 echo " want the resolved $(real_path "$(command -v sleep)")"; exit 1; } 143 # The non-empty guard is the point of the first test: with `=` alone, a
144 # box where lsof answered nothing AND perl answered nothing would compare
145 # "" with "" and read ORACLE_OK — two dead helpers agreeing.
146 [ -n "$_owant" ] && [ "$_oexe" = "$_owant" ] || {
147 echo "e2e FAIL: oracle: pid_exe of the subject's sleep is '$_oexe',"
148 echo " want the resolved '$_owant'"; exit 1; }
144 # Through a variable and a case rather than straight into test(1): a 149 # Through a variable and a case rather than straight into test(1): a
145 # helper that answered with nothing would otherwise abort the run with 150 # helper that answered with nothing would otherwise abort the run with
146 # a bare `[: -gt: unary operator expected` and no e2e FAIL line saying 151 # a bare `[: -gt: unary operator expected` and no e2e FAIL line saying
@@ -233,6 +238,72 @@ time.sleep(300)' \
233 echo "e2e FAIL: oracle: sha256_of hashes 'hello' to" 238 echo "e2e FAIL: oracle: sha256_of hashes 'hello' to"
234 echo " '$(sha256_of "$OUT.oracle.probe")'"; exit 1; } 239 echo " '$(sha256_of "$OUT.oracle.probe")'"; exit 1; }
235 240
241 # real_path's two branches, against the rule `readlink -f` states and
242 # both arms owe: every component but the LAST must exist.
243 #
244 # The unwritten leaf first. e2e_14_upgrade canonicalizes the name of a
245 # candidate binary before anything writes one, so an arm that refused a
246 # path it cannot stat would fail that leg — which is what a bare
247 # Cwd::realpath does.
248 _orpl=$(real_path "$OUT.oracle.notyet") || _orpl=''
249 case "$_orpl" in
250 /*.oracle.notyet) ;;
251 *) echo "e2e FAIL: oracle: real_path answers '$_orpl' for a name whose parent"
252 echo " exists and whose last component does not, where it owes an"
253 echo " absolute path ending in that name"; exit 1 ;;
254 esac
255 # And the missing PARENT, which must be no answer at all. This is the
256 # half only one arm gets for free: readlink -f refuses it, while
257 # Cwd::realpath on some perls hands back the spelling of a directory that
258 # is not there, so the Darwin arm has to check that the parent is a
259 # directory rather than that the call answered.
260 # The status kept through `||` and not read after the fact: an
261 # assignment whose command substitution fails IS a failing command, and
262 # under `set -e` this pin would end the run with no line at all rather
263 # than grade anything. stderr goes to /dev/null rather than into the
264 # value, because the contract is about what reaches STDOUT and a traced
265 # run would otherwise capture its own xtrace.
266 _orpmrc=0
267 _orpm=$(real_path "$OUT.oracle.nodir/x" 2>/dev/null) || _orpmrc=$?
268 [ "$_orpmrc" -ne 0 ] && [ -z "$_orpm" ] || {
269 echo "e2e FAIL: oracle: real_path answers '$_orpm' (rc $_orpmrc) for a path"
270 echo " whose PARENT does not exist, where it owes nothing and rc 1"
271 exit 1; }
272
273 # The `timeout` a group file spells, which is GNU's binary here, gtimeout
274 # on a Mac that has coreutils and perl's alarm on one that does not.
275 # Nothing else committed calls it on that last arm, so without these five
276 # lines the bare-Mac fallback ships unrun — and every one of these codes
277 # is one a caller reads: refuse() and half the daemon legs turn on
278 # telling 124 from 1.
279 #
280 # 0.3s for the expiry, because that is the only case that has to wait for
281 # the alarm and every arm takes a fractional budget; the whole block
282 # costs about a third of a second.
283 _otimeout_is() {
284 _otw=$1; shift
285 _otrc=0
286 # A redirected subshell that ENDS IN `exit`, and both halves matter.
287 # GNU timeout re-raises the signal that killed its child so as to
288 # report 128+N, and a shell whose foreground command died of a signal
289 # prints "Terminated" to its own stderr — a line that is not this
290 # suite's and would land in the middle of an e2e log. The subshell
291 # moves that report inside the redirection; the trailing `exit` stops
292 # the shell exec'ing timeout in place of the subshell, which would
293 # leave the signal to be reported by the shell outside it after all.
294 ( timeout "$@"; exit $? ) >/dev/null 2>&1 || _otrc=$?
295 [ "$_otrc" -eq "$_otw" ] || {
296 echo "e2e FAIL: oracle: timeout $* exited $_otrc, want $_otw"
297 echo " (124 the budget expiring, 127 a command that is not there,"
298 echo " 128+N a signal, anything else the child's own status)"
299 exit 1; }
300 }
301 _otimeout_is 0 5 true
302 _otimeout_is 124 0.3 sleep 5
303 _otimeout_is 7 5 sh -c 'exit 7'
304 _otimeout_is 127 5 /no/such/binary-for-the-oracle
305 _otimeout_is 143 5 sh -c 'kill -TERM $$'
306
236 # The clock, asked of the CALENDAR and then of itself. Thirteen digits is 307 # The clock, asked of the CALENDAR and then of itself. Thirteen digits is
237 # what epoch milliseconds have been since 2001 and will be until 2286, so 308 # what epoch milliseconds have been since 2001 and will be until 2286, so
238 # it separates milliseconds from the seconds and the nanoseconds either 309 # it separates milliseconds from the seconds and the nanoseconds either
@@ -282,7 +353,7 @@ time.sleep(300)' \
282 done 353 done
283 ! pid_alive "$_opid" || { 354 ! pid_alive "$_opid" || {
284 echo "e2e FAIL: oracle: pid_alive says a killed shell still lives"; exit 1; } 355 echo "e2e FAIL: oracle: pid_alive says a killed shell still lives"; exit 1; }
285 rm -f "$_osock" "$_oport_f" "$OUT.oracle.probe" 356 rm -f "$_osock" "$_oport_f" "$OUT.oracle.probe" "$OUT.oracle.notyet"
286 ok "oracle: the OS answers the helpers by name" 357 ok "oracle: the OS answers the helpers by name"
287 # One fixed word, on top of ok()'s sentence: a port is graded over ssh 358 # One fixed word, on top of ok()'s sentence: a port is graded over ssh
288 # and the run that grades it has one line to look for. 359 # and the run that grades it has one line to look for.
test/os_oracle.sh
Old New
@@ -180,9 +180,16 @@ Darwin)
180 # Cwd::realpath refuses a path whose last component does not exist, 180 # Cwd::realpath refuses a path whose last component does not exist,
181 # where `readlink -f` resolves it — and e2e_14_upgrade canonicalizes the 181 # where `readlink -f` resolves it — and e2e_14_upgrade canonicalizes the
182 # name of a candidate binary it has not written yet. So the whole path 182 # name of a candidate binary it has not written yet. So the whole path
183 # is tried first and the parent alone second, which is GNU's rule; a 183 # is tried first and the parent alone second, which is GNU's rule: every
184 # missing component ABOVE the last is still no answer, and both arms 184 # component but the last must exist.
185 # then return 1 and print nothing. 185 #
186 # The parent is checked with -d and not merely for a defined answer,
187 # because Cwd::realpath is not the same function on every perl. On 5.42
188 # with Cwd 3.94 it hands BACK the spelling of a directory that is not
189 # there — realpath("/nope") is "/nope" — so a `defined` test alone let
190 # real_path answer a path whose parent does not exist, where readlink -f
191 # answers nothing with rc 1. oracle_selftest pins both branches, because
192 # only the perl arm can get this wrong and only on some perls.
186 real_path() { 193 real_path() {
187 perl -e ' 194 perl -e '
188 use Cwd (); 195 use Cwd ();
@@ -191,7 +198,7 @@ my $r = Cwd::realpath($p);
191 unless (defined $r) { 198 unless (defined $r) {
192 my ($d, $b) = $p =~ m{^(.*)/([^/]*)$} ? ($1 eq "" ? "/" : $1, $2) : (".", $p); 199 my ($d, $b) = $p =~ m{^(.*)/([^/]*)$} ? ($1 eq "" ? "/" : $1, $2) : (".", $p);
193 my $rd = Cwd::realpath($d); 200 my $rd = Cwd::realpath($d);
194 exit 1 unless defined $rd; 201 exit 1 unless defined $rd && -d $rd;
195 $r = $rd eq "/" ? "/$b" : "$rd/$b"; 202 $r = $rd eq "/" ? "/$b" : "$rd/$b";
196 } 203 }
197 print "$r\n";' "$1" 204 print "$r\n";' "$1"
@@ -284,12 +291,22 @@ eval "$(now_ms_snippet)"
284 # so the last resort is perl, which every macOS ships. A group file keeps 291 # so the last resort is perl, which every macOS ships. A group file keeps
285 # spelling `timeout` through all three. 292 # spelling `timeout` through all three.
286 # 293 #
287 # The perl arm is GNU timeout's contract and not an approximation of it: 294 # The perl arm answers GNU's EXIT contract for the five cases this harness
288 # 124 when the alarm fired, 127 for a command that is not there, 128+N for 295 # asks, and oracle_selftest pins all five: 0 for a command that returns in
289 # one a signal took, and otherwise the command's own status. A harness that 296 # time, 124 when the budget expired, the child's own status when it exited,
290 # read a timeout as a pass, or a real failure as a timeout, would grade the 297 # 127 for a command that is not there, and 128+N for one a signal took. A
291 # wrong thing — `refuse()` and half the e2e's daemon legs turn on telling 298 # harness that read a timeout as a pass, or a real failure as a timeout,
292 # 124 from 1. 299 # would grade the wrong thing — `refuse()` and half the e2e's daemon legs
300 # turn on telling 124 from 1.
301 #
302 # It is not GNU timeout in three ways, each measured and none of them
303 # reached by a caller here: a command that exists but is not executable
304 # answers 127 where GNU answers 126; a signal sent to the WRAPPER is not
305 # relayed to the child, where GNU forwards TERM, INT and HUP; and the child
306 # is not put in a process group, so GNU's kill-the-group on expiry becomes
307 # kill-the-child and a grandchild outlives the budget. Every caller in this
308 # tree runs `timeout <seconds> CMD` in the foreground over a single mux or
309 # ptyclient child, with no flags.
293 if command -v timeout >/dev/null 2>&1; then 310 if command -v timeout >/dev/null 2>&1; then
294 : 311 :
295 elif command -v gtimeout >/dev/null 2>&1; then 312 elif command -v gtimeout >/dev/null 2>&1; then