8b70b1e0
test: the Darwin oracle answers on a bare macOS through perl, not coreutils
a73x 2026-09-04 10:16
Commit message
test/mac.sh
| Old | New | ||
|---|---|---|---|
| @@ -308,16 +308,14 @@ gssh 60 >/dev/null <<G || fail daemon "daemon $DPID does not hold $SOCK" | |||
| 308 | . ./os_oracle.sh | 308 | . ./os_oracle.sh |
| 309 | pid_holds_unix_sock $DPID $SOCK | 309 | pid_holds_unix_sock $DPID $SOCK |
| 310 | G | 310 | G |
| 311 | # canonicalised WITHOUT the oracle's real_path: that spelling is greadlink, | 311 | # Both sides canonical, and both through the oracle: pid_exe answers what |
| 312 | # and a pristine macOS has no GNU coreutils on it — installing them to | 312 | # lsof reports, which resolves every symlink on the way, so a home directory |
| 313 | # answer one question would make the guest something other than the bare | 313 | # or a $TMPDIR reached through one would fail a correct exec unless the |
| 314 | # Mac this gate is about. `cd -P` resolves a symlinked directory the same | 314 | # expected path is resolved the same way. |
| 315 | # way, and lsof (which is what pid_exe reads) reports the same canonical | ||
| 316 | # form for the last component. | ||
| 317 | DEXE=$(gssh 60 <<'G' | 315 | DEXE=$(gssh 60 <<'G' |
| 318 | . ./os_oracle.sh | 316 | . ./os_oracle.sh |
| 319 | echo "is=$(pid_exe $(pgrep -x mux | head -1))" | 317 | echo "is=$(pid_exe $(pgrep -x mux | head -1))" |
| 320 | echo "want=$(cd -P -- ./ && pwd -P)/mux" | 318 | echo "want=$(real_path ./mux)" |
| 321 | G | 319 | G |
| 322 | ) | 320 | ) |
| 323 | _is=$(printf '%s\n' "$DEXE" | sed -n 's/^is=//p') | 321 | _is=$(printf '%s\n' "$DEXE" | sed -n 's/^is=//p') |
test/oracle_selftest.sh
| Old | New | ||
|---|---|---|---|
| @@ -262,9 +262,9 @@ time.sleep(300)' \ | |||
| 262 | 262 | ||
| 263 | # And the TEXT of that clock, run in a shell this one did not configure, | 263 | # And the TEXT of that clock, run in a shell this one did not configure, |
| 264 | # which is exactly how the throughput legs and the agent-mute leg use it. | 264 | # which is exactly how the throughput legs and the agent-mute leg use it. |
| 265 | # On Darwin it names gdate, so a Mac with no coreutils fails HERE rather | 265 | # On Darwin it is a perl one-liner, so a Mac whose perl has lost |
| 266 | # than inside a $(( )) partway through a group file, which is where an | 266 | # Time::HiRes fails HERE rather than inside a $(( )) partway through a |
| 267 | # unpinned snippet would have failed. | 267 | # group file, which is where an unpinned snippet would have failed. |
| 268 | _osnip=$(sh -c "$(now_ms_snippet); now_ms" 2>&1 || true) | 268 | _osnip=$(sh -c "$(now_ms_snippet); now_ms" 2>&1 || true) |
| 269 | _osnipsaid="$_osnip" | 269 | _osnipsaid="$_osnip" |
| 270 | case "$_osnip" in ''|*[!0-9]*) _osnip='' ;; esac | 270 | case "$_osnip" in ''|*[!0-9]*) _osnip='' ;; esac |
test/os_oracle.sh
| Old | New | ||
|---|---|---|---|
| @@ -171,27 +171,51 @@ Darwin) | |||
| 171 | file_mode() { stat -f %Lp "$1"; } | 171 | file_mode() { stat -f %Lp "$1"; } |
| 172 | file_size() { stat -f %z "$1"; } | 172 | file_size() { stat -f %z "$1"; } |
| 173 | sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; } | 173 | sha256_of() { shasum -a 256 "$1" | cut -d' ' -f1; } |
| 174 | # GNU coreutils under their g-names, which is a dependency this harness | 174 | # perl, and not GNU coreutils under their g-names. A pristine macOS has |
| 175 | # already has on Darwin: a bare Mac has no `timeout` either, and the | 175 | # no Homebrew on it and this arm has to answer there — test/mac.sh runs |
| 176 | # gtimeout fallback at the bottom of this file is how that is met. | 176 | # the oracle INSIDE a freshly cloned guest, and installing a package to |
| 177 | # Taking them here rather than reimplementing BSD spellings keeps the | 177 | # answer one question would make that guest something other than the |
| 178 | # two arms asking one question — greadlink -f resolves a path whose last | 178 | # bare Mac the gate is about. perl ships with macOS and needs nothing. |
| 179 | # component does not exist yet, which e2e_14_upgrade hands it and a | ||
| 180 | # realpath(3) would refuse. | ||
| 181 | real_path() { greadlink -f "$1"; } | ||
| 182 | # gdate and not the system date, even though macOS 26's /bin/date does | ||
| 183 | # answer %N (measured, and older ones do not). A `date +%s%N` here would | ||
| 184 | # make the suite's clock depend on how new the OS is, and its failure on | ||
| 185 | # an older one is a literal N inside an arithmetic expansion partway | ||
| 186 | # through a run. gdate is the same GNU date the other arm calls and is | ||
| 187 | # already on any box this harness runs on, for gtimeout's sake. | ||
| 188 | # | 179 | # |
| 189 | # An interpreter instead of either was measured and rejected: python3 | 180 | # Cwd::realpath refuses a path whose last component does not exist, |
| 190 | # costs 21.6ms a call and perl 4.5 against gdate's 2.1 (2026-09-03), and | 181 | # where `readlink -f` resolves it — and e2e_14_upgrade canonicalizes the |
| 191 | # the tightest thing a now_ms delta is graded against is the throughput | 182 | # name of a candidate binary it has not written yet. So the whole path |
| 192 | # gate's 10ms ceiling around a 6ms leg — so a python3 start would be | 183 | # is tried first and the parent alone second, which is GNU's rule; a |
| 193 | # most of the reading rather than noise around it. | 184 | # missing component ABOVE the last is still no answer, and both arms |
| 194 | now_ms_snippet() { echo 'now_ms() { echo $(( $(gdate +%s%N) / 1000000 )); }'; } | 185 | # then return 1 and print nothing. |
| 186 | real_path() { | ||
| 187 | perl -e ' | ||
| 188 | use Cwd (); | ||
| 189 | my $p = $ARGV[0]; | ||
| 190 | my $r = Cwd::realpath($p); | ||
| 191 | unless (defined $r) { | ||
| 192 | my ($d, $b) = $p =~ m{^(.*)/([^/]*)$} ? ($1 eq "" ? "/" : $1, $2) : (".", $p); | ||
| 193 | my $rd = Cwd::realpath($d); | ||
| 194 | exit 1 unless defined $rd; | ||
| 195 | $r = $rd eq "/" ? "/$b" : "$rd/$b"; | ||
| 196 | } | ||
| 197 | print "$r\n";' "$1" | ||
| 198 | } | ||
| 199 | # macOS 26's /bin/date does answer %N and older ones do not, so spelling | ||
| 200 | # the system date here would make the suite's clock depend on how new | ||
| 201 | # the OS is — and its failure on an older one is a literal N inside an | ||
| 202 | # arithmetic expansion partway through a run. Time::HiRes is core perl | ||
| 203 | # and answers on every version. | ||
| 204 | # | ||
| 205 | # It costs 4.5ms a call against gdate's 2.1 and python3's 21.6 (measured | ||
| 206 | # 2026-09-03). That is inside the noise of every bracket in the e2e, | ||
| 207 | # whose tightest budget is 1500ms. It is NOT inside the noise of the | ||
| 208 | # throughput gate's 10ms ceiling around a 6ms leg, so a `make throughput` | ||
| 209 | # run on a Mac will need SOLO_MAX_MS raised — which that gate already | ||
| 210 | # takes from the environment for exactly this kind of reason. | ||
| 211 | # | ||
| 212 | # No backslash anywhere in the snippet, hence -l rather than a printed | ||
| 213 | # "\n": this text is TYPED into a session by the throughput and | ||
| 214 | # agent-mute legs, and the pty fixture reads a backslash as the start of | ||
| 215 | # an escape rather than as a byte to send. | ||
| 216 | now_ms_snippet() { | ||
| 217 | echo 'now_ms() { perl -MTime::HiRes -le "print int(Time::HiRes::time() * 1000)"; }' | ||
| 218 | } | ||
| 195 | ;; | 219 | ;; |
| 196 | *) | 220 | *) |
| 197 | pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; } | 221 | pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; } |
| @@ -255,6 +279,43 @@ esac | |||
| 255 | # cannot drift into two answers on one box. | 279 | # cannot drift into two answers on one box. |
| 256 | eval "$(now_ms_snippet)" | 280 | eval "$(now_ms_snippet)" |
| 257 | 281 | ||
| 258 | # GNU timeout is a binary here; a box without one names it gtimeout, and a | 282 | # GNU timeout is a binary on Linux. A Mac has it only if somebody installed |
| 259 | # group file keeps spelling `timeout` either way. | 283 | # coreutils, where it answers to gtimeout, and a pristine one has neither — |
| 260 | command -v timeout >/dev/null 2>&1 || timeout() { gtimeout "$@"; } | 284 | # so the last resort is perl, which every macOS ships. A group file keeps |
| 285 | # spelling `timeout` through all three. | ||
| 286 | # | ||
| 287 | # The perl arm is GNU timeout's contract and not an approximation of it: | ||
| 288 | # 124 when the alarm fired, 127 for a command that is not there, 128+N for | ||
| 289 | # one a signal took, and otherwise the command's own status. A harness that | ||
| 290 | # read a timeout as a pass, or a real failure as a timeout, would grade the | ||
| 291 | # wrong thing — `refuse()` and half the e2e's daemon legs turn on telling | ||
| 292 | # 124 from 1. | ||
| 293 | if command -v timeout >/dev/null 2>&1; then | ||
| 294 | : | ||
| 295 | elif command -v gtimeout >/dev/null 2>&1; then | ||
| 296 | timeout() { gtimeout "$@"; } | ||
| 297 | else | ||
| 298 | timeout() { | ||
| 299 | _tsecs=$1; shift | ||
| 300 | # waitpid is restarted around the handler rather than trusted once: | ||
| 301 | # perl defers a signal to a safe point and the wait returns EINTR | ||
| 302 | # when it does, and a status read from that return is not the | ||
| 303 | # child's. | ||
| 304 | perl -e ' | ||
| 305 | use POSIX (); | ||
| 306 | use Time::HiRes (); | ||
| 307 | my $secs = shift @ARGV; | ||
| 308 | my $pid = fork(); | ||
| 309 | defined $pid or exit 125; | ||
| 310 | unless ($pid) { exec { $ARGV[0] } @ARGV; exit 127; } | ||
| 311 | my $fired = 0; | ||
| 312 | $SIG{ALRM} = sub { $fired = 1; kill "TERM", $pid }; | ||
| 313 | Time::HiRes::alarm($secs); | ||
| 314 | my $r; | ||
| 315 | do { $r = waitpid($pid, 0) } while ($r == -1 && $! == POSIX::EINTR()); | ||
| 316 | my $st = $?; | ||
| 317 | Time::HiRes::alarm(0); | ||
| 318 | exit 124 if $fired; | ||
| 319 | exit(($st & 127) ? 128 + ($st & 127) : $st >> 8);' "$_tsecs" "$@" | ||
| 320 | } | ||
| 321 | fi | ||