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/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 |