summaryrefslogtreecommitdiff
path: root/9ns/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-19 23:55:47 -0300
committerGabriel Schneider <[email protected]>2026-09-19 23:55:47 -0300
commit3e9f8805f293f622bb885cf849b5ce47dc062ad1 (patch)
tree0f31107fd20e7a9aa065826c891d2d326efc2309 /9ns/test
parentba996acfcad1698adbf4a1834fe50e73b1c6cab9 (diff)
downloadcloud9-3e9f8805f293f622bb885cf849b5ce47dc062ad1.tar.gz
cloud9-3e9f8805f293f622bb885cf849b5ce47dc062ad1.zip
9ns: --name and /mnt/9p/<name> mounts, qid.path as inode number, interrupts as Tflush
- --name NAME (default derived from the transport: socket basename, tcp-IP-PORT, spawned command, fdN) mounts at /mnt/9p/<name>; --mount still overrides. ensureMountpoint walks down and creates missing components, shadowing the deepest unwritable ancestor. NINE_MOUNT is the only exported variable. - The inode number reported to the kernel is the 9P qid.path for every node, root included; a server handing qid.path 1 to a file (Pardes /self) no longer collides with the root. - FUSE_INTERRUPT for the request in flight becomes Tflush; a blocked read returns EINTR when the server answers the flush, chunked transfers return short counts, other requests arriving meanwhile are stashed and served next. Servers ignoring Tflush still block until they answer. - 9ns-test now covers nine/bridge/fuse; new adv_bridge_interrupt suite (28); 9ns-itest grows to 88 checks. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9ns/test')
-rwxr-xr-x9ns/test/adv_bridge_hostile.py11
-rwxr-xr-x9ns/test/adv_bridge_hostile.sh6
-rwxr-xr-x9ns/test/adv_bridge_interrupt.sh136
-rwxr-xr-x9ns/test/adv_bridge_semantics.sh7
-rwxr-xr-x9ns/test/adv_bridge_stress.sh2
-rwxr-xr-x9ns/test/adversarial.sh2
-rwxr-xr-x9ns/test/integration.sh27
7 files changed, 180 insertions, 11 deletions
diff --git a/9ns/test/adv_bridge_hostile.py b/9ns/test/adv_bridge_hostile.py
index d353541..f676d2f 100755
--- a/9ns/test/adv_bridge_hostile.py
+++ b/9ns/test/adv_bridge_hostile.py
@@ -62,7 +62,8 @@ name_huge / has an entry with a 60000-byte name
name_dots / lists "." and ".." too
rerror_big Twalk to nope: Rerror with 65535 bytes of text
extra_reply Rread on /f: an unsolicited Rclunk (tag 9) precedes the real reply
-never Tread on /f: never reply (hang)
+never Tread on /f: never reply (hang); the server stops reading, so a Tflush is never seen
+never_flush Tread on /f: never reply, but keep serving: log every Tflush and answer Rflush
close_mid Tread on /f: close the socket without replying
renegotiate Tread on /f: an unsolicited Rversion precedes the real reply
length_max Tstat on /f: length = 2**64-1
@@ -142,6 +143,9 @@ class Server:
parent.children[name] = n
return n
+ def log(self, text):
+ print(text, flush=True)
+
# -- framing ---------------------------------------------------------
def frame(self, typ, tag, body):
return s32(7 + len(body)) + s8(typ) + s16(tag) + body
@@ -196,6 +200,8 @@ class Server:
self.fids[fid] = [self.root, False]
return self.frame(Rattach, tag, self.root.qid(self))
if typ == Tflush:
+ oldtag = r.u16()
+ self.log('Tflush tag=%d oldtag=%d' % (tag, oldtag))
return self.frame(Rflush, tag, b'')
if typ == Twalk:
fid, newfid, nw = r.u32(), r.u32(), r.u16()
@@ -292,6 +298,9 @@ class Server:
if m == 'never':
time.sleep(3600)
return None
+ if m == 'never_flush':
+ self.log('Tread tag=%d fid=%d hang' % (tag, fid))
+ return b''
if m == 'close_mid':
return None
if m == 'renegotiate':
diff --git a/9ns/test/adv_bridge_hostile.sh b/9ns/test/adv_bridge_hostile.sh
index f5ba871..20c05bf 100755
--- a/9ns/test/adv_bridge_hostile.sh
+++ b/9ns/test/adv_bridge_hostile.sh
@@ -37,7 +37,7 @@ start_server() { # mode
run() {
local mode=$1 script=$2; shift 2
start_server "$mode"
- OUT=$(timeout 30 "$NS" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr")
+ OUT=$(timeout 30 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$TMP/stderr")
RC=$?
STDERR=$(cat "$TMP/stderr")
}
@@ -146,7 +146,7 @@ echo "# a server that never replies"
start_server never
# SIGTERM is forwarded to the child; once the child is gone 9ns must leave the
# pending 9P reply behind and exit even though the server stays silent.
-timeout -s TERM 3 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
+timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
TPID=$!
sleep 4
if kill -0 "$TPID" 2>/dev/null; then
@@ -156,7 +156,7 @@ else
fi
wait "$TPID" 2>/dev/null
# Without a signal the mount hangs (documented v1 limitation) until the server dies.
-timeout 30 "$NS" --unix "$SOCK" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
+timeout 30 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "cat $M/f; echo unreachable" >"$TMP/never.out" 2>"$TMP/never.err" &
TPID=$!
sleep 1.5
if kill -0 "$TPID" 2>/dev/null; then
diff --git a/9ns/test/adv_bridge_interrupt.sh b/9ns/test/adv_bridge_interrupt.sh
new file mode 100755
index 0000000..e364143
--- /dev/null
+++ b/9ns/test/adv_bridge_interrupt.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env bash
+# Interrupt tests: a reader blocked in a 9P read the server never answers must
+# be releasable. Killing or Ctrl-C-ing it makes the kernel send FUSE_INTERRUPT,
+# which the bridge turns into a Tflush; a server that answers Rflush (the
+# hostile server's `never_flush` mode) unblocks the reader with EINTR and the
+# mount stays usable; a server that ignores everything (`never`) still blocks
+# the mount, but SIGTERM to 9ns ends the session as before.
+# Usage: bash 9ns/test/adv_bridge_interrupt.sh <9ns> <9proc-demo> (9proc unused; part of zig build 9ns-adv)
+set -u
+NS=$(realpath "${1:?path to 9ns}")
+HERE=$(cd "$(dirname "$0")" && pwd)
+SRV=$HERE/adv_bridge_hostile.py
+TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-int.XXXXXX")
+M=/mnt/9p
+FAILED=0
+PASSED=0
+SRVPID=
+
+cleanup() { [ -n "$SRVPID" ] && kill "$SRVPID" 2>/dev/null; pkill -f "adv_bridge_hostile.py $TMP" 2>/dev/null; rm -rf "$TMP"; }
+trap cleanup EXIT
+
+if ! unshare -Urm true 2>/dev/null || [ ! -c /dev/fuse ]; then echo "SKIP: no user namespaces or /dev/fuse"; exit 0; fi
+
+pass() { PASSED=$((PASSED + 1)); echo "ok - $1"; }
+fail() { FAILED=$((FAILED + 1)); echo "FAIL - $1"; shift; [ $# -gt 0 ] && printf ' %s\n' "$@"; }
+expect_eq() { if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi; }
+expect_contains() { case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac; }
+# expect_lt NAME ACTUAL LIMIT
+expect_lt() { if [ "$2" -lt "$3" ]; then pass "$1 ($2 < $3)"; else fail "$1" "expected < $3, got $2"; fi; }
+
+start_server() { # mode
+ [ -n "$SRVPID" ] && { kill "$SRVPID" 2>/dev/null; wait "$SRVPID" 2>/dev/null; }
+ SOCK=$TMP/$1.sock
+ SRVLOG=$TMP/$1.srv.out
+ rm -f "$SOCK"
+ python3 "$SRV" "$SOCK" "$1" >"$SRVLOG" 2>&1 </dev/null &
+ SRVPID=$!
+ for _ in $(seq 1 100); do [ -S "$SOCK" ] && return 0; sleep 0.02; done
+ echo "server for $1 did not start"; cat "$SRVLOG"; exit 1
+}
+
+# run MODE SCRIPT [extra 9ns args...]: starts the server, runs 9ns; sets OUT, RC, STDERR.
+run() {
+ local mode=$1 script=$2; shift 2
+ start_server "$mode"
+ OUT=$(timeout 60 "$NS" --unix "$SOCK" --mount "$M" "$@" -- sh -c "$script" 2>"$TMP/stderr")
+ RC=$?
+ STDERR=$(cat "$TMP/stderr")
+}
+
+no_crash() { # name
+ if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9ns exit $RC" "$STDERR"; return; fi
+ case "$STDERR" in *panic*|*"Segmentation"*|*"integer overflow"*|*"reached unreachable"*|*"index out of bounds"*) fail "$1: crash text in stderr" "$STDERR";; *) pass "$1: no crash (exit $RC)";; esac
+}
+
+# The shell snippet that times a command: prints "rc=N" and "ms=N".
+timed() { # command...
+ printf 's=$(date +%%s%%N); %s; echo rc=$?; e=$(date +%%s%%N); echo ms=$(( (e - s) / 1000000 ))' "$*"
+}
+field() { printf '%s\n' "$2" | sed -n "s/^$1=//p" | tail -1; }
+# Server-side fid count before and after the interrupted operation, measured in
+# the same run. Everything the scripts touch is looked up first, so the inode
+# fids the kernel keeps for f, d and g are in both samples and the comparison
+# is exact: any difference is a fid an interrupted operation left behind.
+BEFORE="stat $M/f $M/d/g >/dev/null; ls $M >/dev/null; echo before=\$(cat $M/fids)"
+AFTER="sleep 0.2; echo after=\$(cat $M/fids)"
+fids_same() { # name
+ local b a; b=$(field before "$OUT"); a=$(field after "$OUT")
+ case "$b" in ''|*[!0-9]*) fail "$1: fid count before is not numeric: '$b'"; return;; esac
+ expect_eq "$1: server-side fid count unchanged ($b)" "$b" "$a"
+}
+# Same for the bridge's own counter (--debug prints fids=N per request): the
+# first and the last READ traced are the two `cat fids`.
+bridge_fids_same() { # name
+ local reads; reads=$(printf '%s\n' "$STDERR" | sed -n 's/^9ns: <- read .*(fids=\([0-9]*\) .*/\1/p')
+ expect_eq "$1: bridge fid counter unchanged ($(printf '%s\n' "$reads" | head -1))" "$(printf '%s\n' "$reads" | head -1)" "$(printf '%s\n' "$reads" | tail -1)"
+}
+
+echo "# (a) SIGINT to a reader blocked in a read the server never answers"
+run never_flush "$BEFORE; $(timed "timeout -s INT 2 cat $M/f"); ls $M | tr '\n' ' '; echo; cat $M/d/g; $AFTER" --debug
+no_crash "never_flush/SIGINT"
+expect_eq "never_flush/SIGINT: cat was killed by the signal (timeout reports 124)" "124" "$(field rc "$OUT")"
+expect_lt "never_flush/SIGINT: the reader was released promptly" "$(field ms "$OUT")" 2500
+expect_contains "never_flush/SIGINT: the mount is still usable (ls)" "big d f fids" "$OUT"
+expect_contains "never_flush/SIGINT: the mount is still usable (read another file)" "in d" "$OUT"
+fids_same "never_flush/SIGINT"
+hung_tag=$(sed -n 's/^Tread tag=\([0-9]*\) .*hang$/\1/p' "$SRVLOG" | head -1)
+flush_oldtag=$(sed -n 's/^Tflush tag=[0-9]* oldtag=\([0-9]*\)$/\1/p' "$SRVLOG" | head -1)
+expect_eq "never_flush/SIGINT: exactly one Tflush reached the server" "1" "$(grep -c '^Tflush ' "$SRVLOG")"
+expect_eq "never_flush/SIGINT: Tflush.oldtag is the hung Tread's tag ($hung_tag)" "$hung_tag" "$flush_oldtag"
+expect_contains "never_flush/SIGINT: --debug shows the interrupt being forwarded" "sending Tflush" "$STDERR"
+expect_contains "never_flush/SIGINT: --debug shows EINTR going back to the kernel" "error EINTR" "$STDERR"
+bridge_fids_same "never_flush/SIGINT"
+
+echo "# (c) SIGKILL to the blocked reader"
+run never_flush "$BEFORE; $(timed "cat $M/f & p=\$!; sleep 0.5; kill -9 \$p; wait \$p"); cat $M/d/g; $AFTER"
+no_crash "never_flush/SIGKILL"
+expect_eq "never_flush/SIGKILL: reader died of SIGKILL (137)" "137" "$(field rc "$OUT")"
+expect_lt "never_flush/SIGKILL: released promptly" "$(field ms "$OUT")" 2000
+expect_contains "never_flush/SIGKILL: mount still usable" "in d" "$OUT"
+fids_same "never_flush/SIGKILL"
+expect_eq "never_flush/SIGKILL: one Tflush" "1" "$(grep -c '^Tflush ' "$SRVLOG")"
+
+echo "# a second blocked read after the first was interrupted"
+run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $(timed "timeout -s INT 1 cat $M/f"); cat $M/d/g; $AFTER"
+no_crash "never_flush/twice"
+expect_eq "never_flush/twice: both readers killed" "124 124" "$(printf '%s\n' "$OUT" | sed -n 's/^rc=//p' | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "never_flush/twice: two Tflush, no tag confusion" "2" "$(grep -c '^Tflush ' "$SRVLOG")"
+expect_contains "never_flush/twice: mount still usable" "in d" "$OUT"
+fids_same "never_flush/twice"
+
+echo "# an interrupted open+read through a lookup (walk+stat) leaves no fid behind"
+# `f` is looked up fresh each time (cache 0), so the LOOKUP's walk+stat and the
+# OPEN's clone+open all run before the read blocks; all their fids must go.
+run never_flush "$BEFORE; $(timed "timeout -s INT 1 cat $M/f"); $AFTER" --cache 0 --debug
+no_crash "never_flush/cache0"
+expect_eq "never_flush/cache0: reader killed" "124" "$(field rc "$OUT")"
+fids_same "never_flush/cache0"
+bridge_fids_same "never_flush/cache0"
+
+echo "# (b) a server that ignores Tflush too: the reader stays blocked, SIGTERM to 9ns still ends the session"
+start_server never
+timeout -s TERM 3 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "timeout -s INT 1 cat $M/f; echo unreachable-rc=\$?" >"$TMP/never.out" 2>"$TMP/never.err" &
+TPID=$!
+sleep 4
+if kill -0 "$TPID" 2>/dev/null; then
+ fail "never: SIGTERM did not end 9ns while the reader was stuck"; kill -9 "$TPID"
+else
+ pass "never: SIGTERM ends 9ns even though the server ignores the Tflush"
+fi
+wait "$TPID" 2>/dev/null
+expect_eq "never: the reader never came back (server ignores Tflush)" "" "$(grep unreachable "$TMP/never.out")"
+
+echo
+echo "passed=$PASSED failed=$FAILED"
+[ "$FAILED" -eq 0 ]
diff --git a/9ns/test/adv_bridge_semantics.sh b/9ns/test/adv_bridge_semantics.sh
index b38ce9c..526ebc8 100755
--- a/9ns/test/adv_bridge_semantics.sh
+++ b/9ns/test/adv_bridge_semantics.sh
@@ -24,7 +24,7 @@ SOCK=$TMP/i.sock
SRVPID=$!
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done
# Each run is a fresh session; state persists in the server, so tests clean up after themselves.
-run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
+run() { OUT=$(timeout 120 "$NS" --unix "$SOCK" --mount "$M" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
EXTRA=()
py() { run "python3 - <<'PYEOF'
$1
@@ -141,8 +141,9 @@ for n in os.listdir(d): os.unlink(f'{d}/{n}')
os.rmdir(d)
"
expect_eq "readdir of a directory that changes mid-iteration" "a True True" "$OUT"
-run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/..; cd $M/scratch && ls .. | grep -c scratch"
-expect_eq ".. of the root and of a subdir" $'ok\n1\n1\n1\n1' "$OUT"
+# The root's inode number is its 9P qid.path (not a fixed 1): all three views must agree.
+run "ls $M/.. > /dev/null && echo ok; stat -c %i $M $M/. $M/scratch/.. | sort -u | wc -l; cd $M/scratch && ls .. | grep -c scratch"
+expect_eq ".. of the root and of a subdir" $'ok\n1\n1' "$OUT"
run "cd $M && find . -type d | wc -l && find . -type f | head -1 && find $S -type f | wc -l"
expect_contains "find -type works" "./README" "$OUT"
run "stat -f -c '%T %S %l' $M; df -P $M | tail -1 | awk '{print \$1}'; sync -f $M && echo synced; sync && echo synced2"
diff --git a/9ns/test/adv_bridge_stress.sh b/9ns/test/adv_bridge_stress.sh
index b306b28..d49f453 100755
--- a/9ns/test/adv_bridge_stress.sh
+++ b/9ns/test/adv_bridge_stress.sh
@@ -21,7 +21,7 @@ SOCK=$TMP/i.sock
"$PROC" --unix "$SOCK" >"$TMP/srv.out" 2>&1 &
SRVPID=$!
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.02; done
-run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
+run() { OUT=$(timeout 600 "$NS" --unix "$SOCK" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
echo "# 100k+ 9P operations in one session; RSS must plateau"
# Each iteration: create+write+close, open+read+close, unlink, plus a failing lookup: ~15 RPCs.
diff --git a/9ns/test/adversarial.sh b/9ns/test/adversarial.sh
index 71c6f44..8575dcf 100755
--- a/9ns/test/adversarial.sh
+++ b/9ns/test/adversarial.sh
@@ -8,7 +8,7 @@ NS=${1:?path to 9ns}
PROC=${2:?path to 9proc-demo}
HERE=$(cd "$(dirname "$0")" && pwd)
status=0
-for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do
+for suite in adv_ns_process adv_bridge_hostile adv_bridge_interrupt adv_bridge_semantics adv_bridge_stress; do
echo "### $suite"
if bash "$HERE/$suite.sh" "$NS" "$PROC"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi
done
diff --git a/9ns/test/integration.sh b/9ns/test/integration.sh
index 5e0ad13..32d132a 100755
--- a/9ns/test/integration.sh
+++ b/9ns/test/integration.sh
@@ -10,6 +10,8 @@ TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-itest.XXXXXX")
PIDS=()
FAILED=0
PASSED=0
+# The suites pin the mountpoint with --mount; the default is /mnt/9p/<name>
+# (see the "# mount names" section below).
M=/mnt/9p
cleanup() {
@@ -39,8 +41,8 @@ wait_socket() { # path
return 1
}
-# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array).
-run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; }
+# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array), mounted on $M.
+run_in() { timeout 60 "$NS" "${TRANSPORT[@]}" --mount "$M" -- sh -c "$1" 2>"$TMP/stderr"; }
# --- scratch battery: works against any writable 9P tree rooted at $1 (relative to mount) ---
scratch_battery() { # label scratchdir
@@ -111,6 +113,26 @@ mkdir -p "$TMP/mnt"
expect_eq "--mount existing dir" "ok" "$(timeout 60 "$NS" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")"
expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$NS" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")"
expect_eq "--mount missing under /" "125" "$(timeout 60 "$NS" --unix "$SOCK" --mount /nonexistent-9ns-dir -- true 2>/dev/null; echo $?)"
+expect_eq "--mount beats --name" "$TMP/mnt" "$(timeout 60 "$NS" --unix "$SOCK" --name zz --mount "$TMP/mnt" -- sh -c 'echo $NINE_MOUNT')"
+
+echo "# mount names (default mountpoint /mnt/9p/<name>)"
+mkdir -p "$TMP/n"
+"$PROC" --unix "$TMP/n/foo.sock" &
+PIDS+=($!)
+wait_socket "$TMP/n/foo.sock" || echo "foo.sock missing"
+named() { timeout 60 "$NS" --unix "$TMP/n/foo.sock" "$@"; }
+expect_eq "unix socket foo.sock -> /mnt/9p/foo" "/mnt/9p/foo" "$(named -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "default mount is served" "ok" "$(named -- sh -c '[ -f /mnt/9p/foo/README ] && grep -q "^9ns /mnt/9p/foo fuse" /proc/self/mounts && echo ok')"
+expect_eq "default mount keeps /mnt entries" "$(ls -A /mnt | sort | tr '\n' ' ')" "$(named -- sh -c "ls -A /mnt | grep -v '^9p\$' | sort | tr '\n' ' '")"
+expect_eq "--name bar -> /mnt/9p/bar" "/mnt/9p/bar ok" "$(named --name bar -- sh -c 'echo $NINE_MOUNT; [ -f /mnt/9p/bar/README ] && echo ok' | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "--name=bar form" "/mnt/9p/bar" "$(named --name=bar -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "--name a/b is a usage error" "125" "$(named --name a/b -- true 2>/dev/null; echo $?)"
+expect_eq "--name . is a usage error" "125" "$(named --name . -- true 2>/dev/null; echo $?)"
+expect_eq "--name '' is a usage error" "125" "$(named --name '' -- true 2>/dev/null; echo $?)"
+expect_eq "nested, two names: both under /mnt/9p" "a b both /mnt/9p/b" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name b -- sh -c 'ls /mnt/9p | tr "\n" " "; [ -f /mnt/9p/a/README ] && [ -f /mnt/9p/b/README ] && echo both; echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "nested, same name: inner shadows outer" "2 /mnt/9p/a" "$(named --name a -- "$NS" --unix "$TMP/n/foo.sock" --name a -- sh -c 'grep -c "^9ns /mnt/9p/a fuse" /proc/self/mounts; [ -f /mnt/9p/a/README ] && echo $NINE_MOUNT' 2>/dev/null | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "--spawn default name is the command basename" "/mnt/9p/9proc-demo" "$(timeout 60 "$NS" --spawn "$PROC --stdio" -- sh -c 'echo $NINE_MOUNT')"
+expect_eq "host mount table untouched by named mounts" "no" "$(grep -q " /mnt/9p" /proc/self/mountinfo && echo yes || echo no)"
echo "# lifecycle"
START=$(date +%s)
@@ -131,6 +153,7 @@ PIDS+=($!)
sleep 0.3
TRANSPORT=(--tcp "127.0.0.1:$PORT")
expect_eq "tcp: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")"
+expect_eq "tcp: default name" "/mnt/9p/tcp-127.0.0.1-$PORT" "$(timeout 60 "$NS" --tcp "127.0.0.1:$PORT" -- sh -c 'echo $NINE_MOUNT')"
expect_eq "tcp: scratch" "tcp" "$(run_in "echo tcp > $M/scratch/t && cat $M/scratch/t && rm $M/scratch/t")"
echo "# server death"