summaryrefslogtreecommitdiff
path: root/9player/test
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
committerGabriel Schneider <[email protected]>2026-09-19 23:28:22 -0300
commitba996acfcad1698adbf4a1834fe50e73b1c6cab9 (patch)
tree282ba00ce5b10d7416aecb9f2f0f0a439340a57d /9player/test
parentb05abcba3ea09ea106ad28364c6e40a3ec31b890 (diff)
downloadcloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.tar.gz
cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.zip
Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web)
Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9player/test')
-rwxr-xr-x9player/test/adv_bridge_hostile.py487
-rwxr-xr-x9player/test/adv_bridge_hostile.sh181
-rwxr-xr-x9player/test/adv_bridge_semantics.sh205
-rwxr-xr-x9player/test/adv_bridge_stress.sh64
-rwxr-xr-x9player/test/adv_ns_process.sh202
-rwxr-xr-x9player/test/adversarial.sh15
-rwxr-xr-x9player/test/integration.sh160
7 files changed, 0 insertions, 1314 deletions
diff --git a/9player/test/adv_bridge_hostile.py b/9player/test/adv_bridge_hostile.py
deleted file mode 100755
index b842a1a..0000000
--- a/9player/test/adv_bridge_hostile.py
+++ /dev/null
@@ -1,487 +0,0 @@
-#!/usr/bin/env python3
-"""A scriptable, hostile 9P2000 server on a Unix socket (stdlib only).
-
-Usage: adv_bridge_hostile.py SOCKET MODE
-
-Serves a tiny in-memory tree:
- /f "hello world\\n"
- /d/g "in d\\n"
- /fids reading it returns the number of fids currently bound
- /big 1 MiB of pseudo-random bytes
-plus create/write/remove/wstat so the scratch battery can run in `ok` mode.
-
-MODE selects one misbehaviour (see MODES below). Everything not covered by
-the mode behaves normally, so 9player gets through version/attach/stat(root).
-"""
-import os
-import random
-import socket
-import struct
-import sys
-import time
-
-NOTAG = 0xFFFF
-NOFID = 0xFFFFFFFF
-QTDIR = 0x80
-DMDIR = 0x80000000
-
-Tversion, Rversion = 100, 101
-Tauth, Rauth = 102, 103
-Tattach, Rattach = 104, 105
-Rerror = 107
-Tflush, Rflush = 108, 109
-Twalk, Rwalk = 110, 111
-Topen, Ropen = 112, 113
-Tcreate, Rcreate = 114, 115
-Tread, Rread = 116, 117
-Twrite, Rwrite = 118, 119
-Tclunk, Rclunk = 120, 121
-Tremove, Rremove = 122, 123
-Tstat, Rstat = 124, 125
-Twstat, Rwstat = 126, 127
-
-MODES = """
-ok behave (qid paths are recycled LIFO after remove, like many servers)
-trunc Rread on /f: send half the frame, then close
-short_frame Rread on /f: frame whose size field is 3
-huge_frame Rread on /f: frame whose size field is msize+1
-wrong_tag Rread on /f: reply carries tag+1
-wrong_type Tstat on /f: answer with an Rwalk
-rread_big Rread on /f: count = requested+1
-rwalk_many Twalk to f: nwqid = nwname+1
-rwalk_zero Twalk to nope: Rwalk nwqid=0 instead of Rerror
-rstat_garbage Tstat on /f: random bytes as the stat
-rstat_overlong Tstat on /f: inner stat size disagrees with outer
-dir_split Tread on /: a stat record split across two Rreads
-dir_forever Tread on /: ignore offset, always return the same records
-qid_collide every file and dir shares qid.path 7 (root keeps its own)
-qid_zero every qid.path is 0, including the root
-name_slash / has an entry "a/b"
-name_empty / has an entry ""
-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)
-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
-iounit_one Ropen: iounit = 1
-rwrite_big Rwrite: count = requested+1
-msize_tiny Rversion msize = 64
-version_unknown Rversion "unknown"
-rename_fail Twstat with a new name always fails "file already exists"
-slow every reply delayed 20 ms (for interrupt tests)
-"""
-
-
-def s8(x): return struct.pack('<B', x)
-def s16(x): return struct.pack('<H', x)
-def s32(x): return struct.pack('<I', x)
-def s64(x): return struct.pack('<Q', x)
-def sstr(b):
- if isinstance(b, str):
- b = b.encode()
- return s16(len(b)) + b
-
-
-class Node:
- def __init__(self, name, isdir, path, content=b''):
- self.name = name
- self.isdir = isdir
- self.path = path
- self.content = bytearray(content)
- self.children = {}
- self.mode = 0o755 if isdir else 0o644
- self.mtime = int(time.time())
- self.removed = False
-
- def qid(self, srv):
- path = self.path
- if srv.mode == 'qid_zero':
- path = 0
- elif srv.mode == 'qid_collide' and self is not srv.root:
- path = 7
- return s8(QTDIR if self.isdir else 0) + s32(1) + s64(path)
-
- def stat_bytes(self, srv, name=None, length=None):
- if name is None:
- name = self.name
- if length is None:
- length = 0 if self.isdir else len(self.content)
- body = (s16(0) + s32(0) + self.qid(srv) + s32((DMDIR if self.isdir else 0) | self.mode)
- + s32(self.mtime) + s32(self.mtime) + s64(length)
- + sstr(name) + sstr('u') + sstr('g') + sstr('u'))
- return s16(len(body)) + body
-
-
-class Server:
- def __init__(self, mode):
- self.mode = mode
- self.next_path = 100
- self.free_paths = []
- self.root = Node('', True, 1)
- f = self.mk(self.root, 'f', False, b'hello world\n')
- d = self.mk(self.root, 'd', True)
- self.mk(d, 'g', False, b'in d\n')
- self.mk(self.root, 'fids', False)
- rnd = random.Random(4)
- self.mk(self.root, 'big', False, bytes(rnd.getrandbits(8) for _ in range(1 << 20)))
- self.fids = {}
- self.msize = 8192
- self.ops = 0
-
- def alloc_path(self):
- if self.free_paths:
- return self.free_paths.pop()
- self.next_path += 1
- return self.next_path
-
- def mk(self, parent, name, isdir, content=b''):
- n = Node(name, isdir, self.alloc_path(), content)
- parent.children[name] = n
- return n
-
- # -- framing ---------------------------------------------------------
- def frame(self, typ, tag, body):
- return s32(7 + len(body)) + s8(typ) + s16(tag) + body
-
- def err(self, tag, text):
- return self.frame(Rerror, tag, sstr(text))
-
- def serve(self, conn):
- buf = b''
- while True:
- if len(buf) >= 4:
- n = struct.unpack('<I', buf[:4])[0]
- if len(buf) >= n:
- msg, buf = buf[:n], buf[n:]
- out = self.handle(msg)
- if out is None:
- return # hang up / hang
- if self.mode == 'slow':
- time.sleep(0.02)
- conn.sendall(out)
- continue
- data = conn.recv(65536)
- if not data:
- return
- buf += data
-
- def handle(self, msg):
- typ = msg[4]
- tag = struct.unpack('<H', msg[5:7])[0]
- b = msg[7:]
- self.ops += 1
- r = Reader(b)
- if typ == Tversion:
- msize = r.u32()
- ver = r.str()
- self.msize = min(msize, 1 << 20)
- self.fids = {}
- if self.mode == 'msize_tiny':
- self.msize = 64
- if self.mode == 'version_unknown':
- return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('unknown'))
- return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('9P2000'))
- if typ == Tauth:
- return self.err(tag, 'authentication not required')
- if typ == Tattach:
- fid = r.u32()
- r.u32()
- r.str()
- r.str()
- if fid in self.fids:
- return self.err(tag, 'fid in use')
- self.fids[fid] = [self.root, False]
- return self.frame(Rattach, tag, self.root.qid(self))
- if typ == Tflush:
- return self.frame(Rflush, tag, b'')
- if typ == Twalk:
- fid, newfid, nw = r.u32(), r.u32(), r.u16()
- names = [r.str() for _ in range(nw)]
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- if newfid != fid and newfid in self.fids:
- return self.err(tag, 'fid in use')
- node = self.fids[fid][0]
- qids = b''
- n = 0
- for name in names:
- if not node.isdir:
- break
- if name == '..':
- nxt = node # root's parent is itself; good enough
- elif name in node.children:
- nxt = node.children[name]
- else:
- break
- node = nxt
- qids += node.qid(self)
- n += 1
- if n < nw and n == 0:
- if self.mode == 'rwalk_zero' and names == ['nope']:
- return self.frame(Rwalk, tag, s16(0))
- if self.mode == 'rerror_big' and names == ['nope']:
- return self.err(tag, 'x' * 65535)
- return self.err(tag, 'file does not exist')
- if n == nw:
- self.fids[newfid] = [node, False]
- if self.mode == 'rwalk_many' and names == ['f']:
- return self.frame(Rwalk, tag, s16(n + 1) + qids + node.qid(self))
- return self.frame(Rwalk, tag, s16(n) + qids)
- if typ == Topen:
- fid, mode = r.u32(), r.u8()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node = self.fids[fid][0]
- if node.isdir and (mode & 3) != 0:
- return self.err(tag, 'is a directory')
- if mode & 0x10 and not node.isdir:
- node.content = bytearray()
- self.fids[fid][1] = True
- iounit = 1 if self.mode == 'iounit_one' else 0
- return self.frame(Ropen, tag, node.qid(self) + s32(iounit))
- if typ == Tcreate:
- fid = r.u32()
- name = r.str()
- perm = r.u32()
- mode = r.u8()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- parent = self.fids[fid][0]
- if not parent.isdir:
- return self.err(tag, 'not a directory')
- if name in parent.children:
- return self.err(tag, 'file already exists')
- node = self.mk(parent, name, bool(perm & DMDIR))
- node.mode = perm & 0o777
- self.fids[fid] = [node, True]
- return self.frame(Rcreate, tag, node.qid(self) + s32(0))
- if typ == Tread:
- fid, off, count = r.u32(), r.u64(), r.u32()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node, opened = self.fids[fid]
- if not opened:
- return self.err(tag, 'fid not open')
- if node.isdir:
- return self.readdir(tag, node, off, count)
- if node.name == 'fids':
- data = ('%d\n' % len(self.fids)).encode()
- data = data[off:off + count]
- return self.frame(Rread, tag, s32(len(data)) + data)
- data = bytes(node.content[off:off + count])
- if node is self.root.children.get('f'):
- m = self.mode
- if m == 'trunc':
- fr = self.frame(Rread, tag, s32(len(data)) + data)
- self.conn.sendall(fr[:len(fr) // 2])
- return None
- if m == 'short_frame':
- return s32(3) + s8(Rread) + s16(tag)
- if m == 'huge_frame':
- return s32(self.msize + 1) + s8(Rread) + s16(tag) + s32(len(data)) + data
- if m == 'wrong_tag':
- return self.frame(Rread, (tag + 1) & 0xFFFF, s32(len(data)) + data)
- if m == 'rread_big':
- data = b'x' * (count + 1)
- return self.frame(Rread, tag, s32(len(data)) + data)
- if m == 'extra_reply':
- return self.frame(Rclunk, 9, b'') + self.frame(Rread, tag, s32(len(data)) + data)
- if m == 'never':
- time.sleep(3600)
- return None
- if m == 'close_mid':
- return None
- if m == 'renegotiate':
- return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('9P2000')) + self.frame(Rread, tag, s32(len(data)) + data)
- return self.frame(Rread, tag, s32(len(data)) + data)
- if typ == Twrite:
- fid, off = r.u32(), r.u64()
- data = r.data()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node, opened = self.fids[fid]
- if not opened or node.isdir:
- return self.err(tag, 'fid not open for write')
- if node.name == 'fids':
- return self.err(tag, 'permission denied')
- if off > len(node.content):
- node.content.extend(b'\0' * (off - len(node.content)))
- node.content[off:off + len(data)] = data
- node.mtime = int(time.time())
- n = len(data) + 1 if self.mode == 'rwrite_big' else len(data)
- return self.frame(Rwrite, tag, s32(n))
- if typ == Tclunk:
- fid = r.u32()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- del self.fids[fid]
- return self.frame(Rclunk, tag, b'')
- if typ == Tremove:
- fid = r.u32()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node = self.fids[fid][0]
- del self.fids[fid]
- if node is self.root:
- return self.err(tag, 'cannot remove root')
- if node.isdir and node.children:
- return self.err(tag, 'directory not empty')
- parent = self.find_parent(self.root, node)
- if parent is not None:
- del parent.children[node.name]
- self.free_paths.append(node.path)
- node.removed = True
- return self.frame(Rremove, tag, b'')
- if typ == Tstat:
- fid = r.u32()
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node = self.fids[fid][0]
- if node is self.root.children.get('f'):
- m = self.mode
- if m == 'wrong_type':
- return self.frame(Rwalk, tag, s16(0))
- if m == 'rstat_garbage':
- junk = bytes([0xAB] * 60)
- return self.frame(Rstat, tag, s16(len(junk)) + junk)
- if m == 'rstat_overlong':
- st = node.stat_bytes(self)
- inner = st[2:]
- return self.frame(Rstat, tag, s16(len(inner) + 5) + inner)
- if m == 'length_max':
- st = node.stat_bytes(self, length=2 ** 64 - 1)
- return self.frame(Rstat, tag, s16(len(st)) + st)
- st = node.stat_bytes(self)
- return self.frame(Rstat, tag, s16(len(st)) + st)
- if typ == Twstat:
- fid = r.u32()
- r.u16()
- st = r.bytes(r.u16())
- if fid not in self.fids:
- return self.err(tag, 'unknown fid')
- node = self.fids[fid][0]
- sr = Reader(st)
- sr.u16(); sr.u32(); sr.bytes(13)
- mode = sr.u32(); sr.u32(); mtime = sr.u32(); length = sr.u64()
- name = sr.str()
- if name and name != node.name:
- if self.mode == 'rename_fail':
- return self.err(tag, 'file already exists')
- parent = self.find_parent(self.root, node)
- if name in parent.children:
- return self.err(tag, 'file already exists')
- del parent.children[node.name]
- node.name = name
- parent.children[name] = node
- if mode != 0xFFFFFFFF:
- node.mode = mode & 0o777
- if mtime != 0xFFFFFFFF:
- node.mtime = mtime
- if length != 0xFFFFFFFFFFFFFFFF and not node.isdir:
- if length < len(node.content):
- del node.content[length:]
- else:
- node.content.extend(b'\0' * (length - len(node.content)))
- return self.frame(Rwstat, tag, b'')
- return self.err(tag, 'unsupported message')
-
- def find_parent(self, cur, node):
- for c in cur.children.values():
- if c is node:
- return cur
- if c.isdir:
- p = self.find_parent(c, node)
- if p is not None:
- return p
- return None
-
- def readdir(self, tag, node, off, count):
- recs = []
- if node is self.root:
- m = self.mode
- if m == 'name_slash':
- recs.append(node.stat_bytes(self, name='a/b'))
- if m == 'name_empty':
- recs.append(node.stat_bytes(self, name=''))
- if m == 'name_huge':
- recs.append(node.stat_bytes(self, name='h' * 60000))
- if m == 'name_dots':
- recs.append(node.stat_bytes(self, name='.'))
- recs.append(node.stat_bytes(self, name='..'))
- for c in node.children.values():
- recs.append(c.stat_bytes(self))
- blob = b''.join(recs)
- if node is self.root and self.mode == 'dir_forever':
- return self.frame(Rread, tag, s32(len(blob)) + blob)
- if node is self.root and self.mode == 'dir_split':
- # first read: up to the middle of the second record; second read: the rest
- cut = len(recs[0]) + len(recs[1]) // 2
- if off == 0:
- data = blob[:cut]
- elif off == cut:
- data = blob[cut:]
- else:
- data = b''
- return self.frame(Rread, tag, s32(len(data)) + data)
- # 9P rule: offset 0 or previous offset+count; never split a record.
- out = b''
- pos = 0
- for rec in recs:
- if pos >= off and len(out) + len(rec) <= count:
- out += rec
- elif pos >= off:
- break
- pos += len(rec)
- return self.frame(Rread, tag, s32(len(out)) + out)
-
-
-class Reader:
- def __init__(self, b):
- self.b = b
- self.i = 0
-
- def bytes(self, n):
- v = self.b[self.i:self.i + n]
- self.i += n
- return v
-
- def u8(self): return struct.unpack('<B', self.bytes(1))[0]
- def u16(self): return struct.unpack('<H', self.bytes(2))[0]
- def u32(self): return struct.unpack('<I', self.bytes(4))[0]
- def u64(self): return struct.unpack('<Q', self.bytes(8))[0]
- def str(self): return self.bytes(self.u16()).decode('utf-8', 'surrogateescape')
- def data(self): return self.bytes(self.u32())
-
-
-def main():
- if len(sys.argv) != 3:
- print(__doc__ + MODES)
- sys.exit(2)
- path, mode = sys.argv[1], sys.argv[2]
- if mode not in [l.split()[0] for l in MODES.strip().splitlines()]:
- print('unknown mode', mode)
- sys.exit(2)
- try:
- os.unlink(path)
- except FileNotFoundError:
- pass
- ls = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- ls.bind(path)
- ls.listen(8)
- while True:
- conn, _ = ls.accept()
- srv = Server(mode)
- srv.conn = conn
- try:
- srv.serve(conn)
- except (BrokenPipeError, ConnectionResetError):
- pass
- finally:
- conn.close()
-
-
-if __name__ == '__main__':
- main()
diff --git a/9player/test/adv_bridge_hostile.sh b/9player/test/adv_bridge_hostile.sh
deleted file mode 100755
index f1ee292..0000000
--- a/9player/test/adv_bridge_hostile.sh
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env bash
-# Hostile-server tests: 9player against 9player/test/adv_bridge_hostile.py in every
-# misbehaviour mode. 9player must never crash (panic/segfault) and must turn
-# each misbehaviour into an errno for the child.
-# Usage: bash 9player/test/adv_bridge_hostile.sh <9player> <introspect> (introspect unused; part of zig build 9player-adv)
-set -u
-PLAYER=$(realpath "${1:?path to 9player}")
-HERE=$(cd "$(dirname "$0")" && pwd)
-SRV=$HERE/adv_bridge_hostile.py
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-adv.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; }
-
-start_server() { # mode
- [ -n "$SRVPID" ] && { kill "$SRVPID" 2>/dev/null; wait "$SRVPID" 2>/dev/null; }
- SOCK=$TMP/$1.sock
- rm -f "$SOCK"
- python3 "$SRV" "$SOCK" "$1" >"$TMP/$1.srv.out" 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 "$TMP/$1.srv.out"; exit 1
-}
-
-# run MODE SCRIPT [extra 9player args...]: starts the server, runs 9player; sets OUT, RC, STDERR.
-run() {
- local mode=$1 script=$2; shift 2
- start_server "$mode"
- OUT=$(timeout 30 "$PLAYER" --unix "$SOCK" "$@" -- sh -c "$script" 2>"$TMP/stderr")
- RC=$?
- STDERR=$(cat "$TMP/stderr")
-}
-
-# 9player must not die of a signal or panic. RC 124 = timeout(1) fired.
-no_crash() { # name
- if [ "$RC" -ge 128 ] || [ "$RC" -eq 124 ]; then fail "$1: 9player 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
-}
-
-echo "# sanity: the hostile server behaves in 'ok' mode"
-run ok "cat $M/f"; expect_eq "ok: cat f" "hello world" "$OUT"
-run ok "cat $M/d/g"; expect_eq "ok: nested" "in d" "$OUT"
-run ok "cat $M/nope 2>&1 | sed 's/.*: //'"; expect_eq "ok: ENOENT" "No such file or directory" "$OUT"
-run ok "head -c 1048576 $M/big | wc -c | grep -q 1048576 && echo yes"; expect_eq "ok: 1 MiB read matches" "yes" "$OUT"
-
-echo "# unlink + recreate with a recycled qid.path"
-run ok "echo 1 > $M/a; rm $M/a; echo 2 > $M/a; cat $M/a; rm $M/a"; expect_eq "qid reuse: new content, not stale" "2" "$OUT"
-run ok "echo 1 > $M/x; rm $M/x; mkdir $M/x; stat -c %F $M/x; rmdir $M/x"; expect_eq "qid reuse: file→dir on the same path" "directory" "$OUT"
-
-echo "# fids do not grow with the number of operations"
-loop='i=0; while [ $i -lt N ]; do echo hi > M/t; cat M/t >/dev/null; mkdir M/dd; rmdir M/dd; rm M/t; i=$((i+1)); done; cat M/fids'
-run ok "$(echo "$loop" | sed "s|N|20|; s|M/|$M/|g")"; a=$OUT
-run ok "$(echo "$loop" | sed "s|N|200|; s|M/|$M/|g")"; b=$OUT
-expect_eq "fids after 20 == after 200 iterations ($a)" "$a" "$b"
-floop='i=0; while [ $i -lt N ]; do cat M/nope 2>/dev/null; echo x > M/fids 2>/dev/null; mkdir M/f 2>/dev/null; rm M/d 2>/dev/null; mv M/f M/d 2>/dev/null; i=$((i+1)); done; cat M/fids'
-run ok "$(echo "$floop" | sed "s|N|20|; s|M/|$M/|g")"; a=$OUT
-run ok "$(echo "$floop" | sed "s|N|200|; s|M/|$M/|g")"; b=$OUT
-expect_eq "fids after 20 == after 200 failing iterations ($a)" "$a" "$b"
-
-echo "# rename over an existing file must not lose the target when the rename fails"
-run rename_fail "echo A > $M/a; echo B > $M/b; mv $M/a $M/b 2>/dev/null; echo mv=\$?; cat $M/b; cat $M/a"
-expect_contains "rename_fail: mv reports failure" "mv=1" "$OUT"
-expect_contains "rename_fail: target b still has its content" "B" "$OUT"
-expect_contains "rename_fail: source a still has its content" "A" "$OUT"
-
-echo "# protocol violations on a data read must yield an error, not a crash"
-for mode in trunc short_frame huge_frame wrong_tag rread_big extra_reply close_mid renegotiate rwrite_big; do
- if [ "$mode" = rwrite_big ]; then script="dd if=/dev/zero of=$M/f bs=10 count=1 2>&1; echo status=\$?"; else script="cat $M/f 2>&1; echo status=\$?"; fi
- run "$mode" "$script"
- no_crash "$mode"
- expect_contains "$mode: child sees an error" "status=1" "$OUT"
- case "$OUT" in *"Input/output error"*|*"not connected"*) pass "$mode: EIO/ENOTCONN";; *) fail "$mode: errno text" "$OUT";; esac
-done
-
-echo "# protocol violations on lookup/stat"
-for mode in wrong_type rwalk_many rstat_garbage rstat_overlong; do
- run "$mode" "stat -c %s $M/f 2>&1; echo status=\$?"
- no_crash "$mode"
- expect_contains "$mode: child sees an error" "status=1" "$OUT"
-done
-run rwalk_zero "cat $M/nope 2>&1; echo status=\$?; cat $M/f 2>&1"
-no_crash "rwalk_zero"
-expect_contains "rwalk_zero: child sees an error" "status=1" "$OUT"
-run rerror_big "cat $M/nope 2>&1; echo status=\$?; cat $M/f"
-no_crash "rerror_big"
-expect_contains "rerror_big: child sees an error" "status=1" "$OUT"
-expect_contains "rerror_big: session survives a 64 KiB Rerror" "hello world" "$OUT"
-
-echo "# hostile stat contents"
-run length_max "stat -c '%s %b' $M/f 2>&1; echo status=\$?"
-no_crash "length_max"
-expect_contains "length_max: stat succeeds with a saturated block count" "status=0" "$OUT"
-run iounit_one "cat $M/f; head -c 3000 $M/big | wc -c"
-no_crash "iounit_one"
-expect_contains "iounit_one: read still complete" "hello world" "$OUT"
-expect_contains "iounit_one: 3000 bytes" "3000" "$OUT"
-
-echo "# hostile directory listings"
-run dir_split "ls $M 2>&1; echo status=\$?; cat $M/f"
-no_crash "dir_split"
-expect_contains "dir_split: readdir fails with EIO" "Input/output error" "$OUT"
-expect_contains "dir_split: session survives" "hello world" "$OUT"
-run dir_forever "ls $M 2>&1 | tail -c 200; echo status=\$?; cat $M/f; grep VmRSS /proc/\$PPID/status"
-no_crash "dir_forever"
-expect_contains "dir_forever: infinite directory is cut off with EIO" "Input/output error" "$OUT"
-expect_contains "dir_forever: session survives" "hello world" "$OUT"
-for mode in name_slash name_empty name_huge name_dots; do
- run "$mode" "ls -a $M | tr '\n' ' '; echo; cat $M/f"
- no_crash "$mode"
- expect_contains "$mode: listing still works" "big d f fids" "$OUT"
- expect_contains "$mode: file readable" "hello world" "$OUT"
- case "$mode" in
- name_slash) expect_eq "$mode: slash entry dropped" "" "$(printf '%s' "$OUT" | grep -o 'a/b')";;
- name_dots) expect_eq "$mode: exactly one . and one .." "1 1" "$(printf '%s %s' "$(printf '%s\n' "$OUT" | head -1 | tr ' ' '\n' | grep -c '^\.$')" "$(printf '%s\n' "$OUT" | head -1 | tr ' ' '\n' | grep -c '^\.\.$')")";;
- esac
-done
-
-echo "# qid collisions"
-run qid_collide "cat $M/f; cat $M/d/g; ls $M/d; stat -c %i $M/f $M/d 2>&1; echo status=\$?"
-no_crash "qid_collide"
-expect_contains "qid_collide: reads work" "hello world" "$OUT"
-run qid_zero "cat $M/f; ls $M | tr '\n' ' '; echo; cat $M/d/g; echo status=\$?"
-no_crash "qid_zero"
-expect_contains "qid_zero: file with root's qid.path is still a readable file" "hello world" "$OUT"
-expect_contains "qid_zero: root still lists" "big d f fids" "$OUT"
-expect_contains "qid_zero: nested file readable" "in d" "$OUT"
-
-echo "# version negotiation"
-run version_unknown "echo ran"
-expect_eq "version_unknown: 9player refuses (125)" "125" "$RC"
-run msize_tiny "cat $M/f 2>&1; echo status=\$?"
-no_crash "msize_tiny"
-
-echo "# a server that never replies"
-start_server never
-# SIGTERM is forwarded to the child; once the child is gone 9player must leave the
-# pending 9P reply behind and exit even though the server stays silent.
-timeout -s TERM 3 "$PLAYER" --unix "$SOCK" -- 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
- fail "never: SIGTERM did not end 9player while a reply was outstanding"; kill -9 "$TPID"
-else
- pass "never: SIGTERM ends 9player even while the server is silent"
-fi
-wait "$TPID" 2>/dev/null
-# Without a signal the mount hangs (documented v1 limitation) until the server dies.
-timeout 30 "$PLAYER" --unix "$SOCK" -- 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
- pass "never: mount hangs while the server is silent (documented v1 limitation)"
- kill "$SRVPID"; wait "$SRVPID" 2>/dev/null; SRVPID=
- for _ in $(seq 1 50); do kill -0 "$TPID" 2>/dev/null || break; sleep 0.1; done
- if kill -0 "$TPID" 2>/dev/null; then fail "never: 9player still alive after its server died"; kill -9 "$TPID"; else pass "never: killing the server unblocks 9player"; fi
-else
- wait "$TPID"; fail "never: 9player exited early ($?)" "$(cat "$TMP/never.err")"
-fi
-
-echo "# interrupting a slow read (INTERRUPT must not confuse reply matching)"
-run slow "cat $M/big > /dev/null; cat $M/f; cat $M/fids" --msize 8192
-base=$(printf '%s\n' "$OUT" | tail -1)
-run slow "(cat $M/big > /dev/null & sleep 0.3; kill -INT \$!; wait \$!) 2>/dev/null; cat $M/f; cat $M/fids" --msize 8192
-no_crash "slow"
-expect_contains "slow: read after interrupted read works" "hello world" "$OUT"
-expect_eq "slow: fids after an interrupted read == after a complete one ($base)" "$base" "$(printf '%s\n' "$OUT" | tail -1)"
-
-echo
-echo "passed=$PASSED failed=$FAILED"
-[ "$FAILED" -eq 0 ]
diff --git a/9player/test/adv_bridge_semantics.sh b/9player/test/adv_bridge_semantics.sh
deleted file mode 100755
index f13fd57..0000000
--- a/9player/test/adv_bridge_semantics.sh
+++ /dev/null
@@ -1,205 +0,0 @@
-#!/usr/bin/env bash
-# FUSE semantics through the bridge against introspect's /scratch tree.
-# Usage: bash 9player/test/adv_bridge_semantics.sh <9player> <introspect> (part of zig build 9player-adv)
-set -u
-PLAYER=$(realpath "${1:?path to 9player}")
-INTROSPECT=$(realpath "${2:?path to introspect}")
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-sem.XXXXXX")
-M=/mnt/9p
-S=$M/scratch
-FAILED=0
-PASSED=0
-SRVPID=
-cleanup() { [ -n "$SRVPID" ] && kill "$SRVPID" 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; }
-
-SOCK=$TMP/i.sock
-"$INTROSPECT" --unix "$SOCK" >"$TMP/srv.out" 2>&1 &
-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 "$PLAYER" --unix "$SOCK" "${EXTRA[@]}" -- sh -c "$1" 2>"$TMP/stderr"); RC=$?; STDERR=$(cat "$TMP/stderr"); }
-EXTRA=()
-py() { run "python3 - <<'PYEOF'
-$1
-PYEOF"; }
-
-echo "# open/create flags"
-py "
-import os, errno
-p='$S/excl'
-fd=os.open(p, os.O_CREAT|os.O_WRONLY, 0o644); os.write(fd, b'x'); os.close(fd)
-try:
- os.open(p, os.O_CREAT|os.O_EXCL|os.O_WRONLY, 0o644); print('no error')
-except OSError as e: print(errno.errorcode[e.errno])
-os.unlink(p)
-fd=os.open('$S/ro', os.O_CREAT|os.O_RDONLY, 0o644); print(os.read(fd, 10)); os.close(fd)
-print(os.path.exists('$S/ro')); os.unlink('$S/ro')
-"
-expect_eq "O_EXCL on an existing file is EEXIST" "EEXIST" "$(printf '%s\n' "$OUT" | sed -n 1p)"
-expect_eq "create with O_RDONLY works and reads empty" $'b\'\'\nTrue' "$(printf '%s\n' "$OUT" | sed -n 2,3p)"
-
-run "mkdir -m 700 $S/m7 && stat -c %a $S/m7; chmod 755 $S/m7 && stat -c %a $S/m7; rmdir $S/m7"
-expect_eq "mkdir -m 700 then chmod 755" $'700\n755' "$OUT"
-run "echo x > $S/c && chmod 600 $S/c && stat -c %a $S/c; chmod 444 $S/c && stat -c %a $S/c; rm -f $S/c"
-expect_eq "chmod on a file" $'600\n444' "$OUT"
-run "echo x > $S/t && touch -d @1000000000 $S/t && stat -c %Y $S/t; touch $S/t && [ \$(stat -c %Y $S/t) -gt 1000000000 ] && echo now; rm $S/t"
-expect_eq "utimes (explicit) and touch (now)" $'1000000000\nnow' "$OUT"
-run "echo abc > $S/tr && truncate -s 10 $S/tr && stat -c %s $S/tr && od -An -c $S/tr | tr -s ' ' | tr -d '\n'; echo; rm $S/tr"
-expect_eq "truncate to larger zero-fills" $'10\n a b c \\n \\0 \\0 \\0 \\0 \\0 \\0' "$OUT"
-run "echo a > $S/ap && echo b >> $S/ap && echo c >> $S/ap && cat $S/ap | tr '\n' ' '; rm $S/ap"
-expect_eq "shell append" "a b c " "$OUT"
-py "
-import os
-p='$S/ap2'
-f1=os.open(p, os.O_CREAT|os.O_WRONLY|os.O_APPEND, 0o644)
-f2=os.open(p, os.O_WRONLY|os.O_APPEND)
-os.write(f1, b'one '); os.write(f2, b'two '); os.write(f1, b'three')
-os.close(f1); os.close(f2)
-print(open(p).read()); os.unlink(p)
-"
-expect_eq "O_APPEND from two descriptors interleaves in order" "one two three" "$OUT"
-run "echo 0123456789 > $S/tt && (echo X > $S/tt) && cat $S/tt && stat -c %s $S/tt; rm $S/tt"
-expect_eq "O_TRUNC (atomic_o_trunc) truncates before write" $'X\n2' "$OUT"
-
-echo "# reads at the edges"
-run "printf hello > $S/e; dd if=$S/e bs=1 skip=100 count=5 2>/dev/null | wc -c; head -c 0 $S/e | wc -c; dd if=/dev/null of=$S/e bs=1 count=0 conv=notrunc 2>/dev/null; cat $S/e; echo; rm $S/e"
-expect_eq "read past EOF is 0 bytes; 0-byte read/write are no-ops" $'0\n0\nhello' "$OUT"
-head -c 4194304 /dev/urandom >"$TMP/four"
-SUM=$(sha256sum <"$TMP/four" | cut -d' ' -f1)
-run "dd if=$TMP/four of=$S/four bs=4M status=none && dd if=$S/four bs=4M status=none | sha256sum | cut -d' ' -f1; stat -c %s $S/four; rm $S/four"
-expect_eq "4 MiB single-request dd round trip" "$SUM"$'\n4194304' "$OUT"
-py "
-import os
-p='$S/lseek'
-open(p,'w').write('0123456789')
-f=open(p,'rb'); f.seek(0, 2); print(f.tell()); f.seek(-3, 2); print(f.read()); f.close()
-os.unlink(p)
-"
-expect_eq "lseek SEEK_END on a direct_io file" $'10\nb\'789\'' "$OUT"
-
-echo "# unlink of an open file"
-py "
-import os
-p='$S/unl'
-fd=os.open(p, os.O_CREAT|os.O_RDWR, 0o644)
-os.write(fd, b'before')
-os.unlink(p)
-print(os.path.exists(p))
-os.lseek(fd, 0, 0); print(os.read(fd, 100))
-os.write(fd, b'-after'); os.lseek(fd, 0, 0); print(os.read(fd, 100))
-os.close(fd)
-"
-expect_eq "read/write through the fd after unlink" $'False\nb\'before\'\nb\'before-after\'' "$OUT"
-
-echo "# rename"
-run "echo A > $S/ra; echo B > $S/rb; mv $S/ra $S/rb && cat $S/rb; ls $S | tr '\n' ' '; echo; rm $S/rb"
-expect_eq "rename over an existing file replaces it, no leftovers" $'A\nrb ' "$OUT"
-run "mkdir $S/rd1 && echo x > $S/rd1/f && mv $S/rd1 $S/rd2 && cat $S/rd2/f && ls $S/rd2; rm -r $S/rd2; ls $S | wc -l"
-expect_eq "rename of a directory" $'x\nf\n0' "$OUT"
-run "mkdir $S/e1 $S/e2 && mv -T $S/e1 $S/e2 && ls $S | tr '\n' ' '; rmdir $S/e2"
-expect_eq "rename dir over an empty dir" "e2 " "$OUT"
-run "mkdir $S/n1 $S/n2 && echo x > $S/n2/f && mv -T $S/n1 $S/n2 2>&1 | sed 's/.*: //'; rm -r $S/n1 $S/n2"
-expect_eq "rename dir over a non-empty dir is ENOTEMPTY" "Directory not empty" "$OUT"
-py "
-import os
-p='$S/same'; open(p,'w').write('x'); os.rename(p, p); print(open(p).read()); os.unlink(p)
-"
-expect_eq "rename onto itself is a no-op" "x" "$OUT"
-py "
-import os, ctypes, errno
-libc = ctypes.CDLL(None, use_errno=True)
-a, b = b'$S/nra', b'$S/nrb'
-open(a,'w').write('A'); open(b,'w').write('B')
-r = libc.renameat2(-100, a, -100, b, 1) # RENAME_NOREPLACE
-print('rc', r, errno.errorcode.get(ctypes.get_errno()))
-print(open(b).read())
-os.unlink(a); os.unlink(b)
-"
-expect_eq "RENAME_NOREPLACE keeps the target" $'rc -1 EEXIST\nB' "$OUT"
-
-echo "# directories"
-run "mkdir $S/many && cd $S/many && i=0; while [ \$i -lt 5000 ]; do : > f\$i; i=\$((i+1)); done; ls | wc -l; ls -l | wc -l; grep VmRSS /proc/\$PPID/status | awk '{print \$2}' > $TMP/rss1; rm -f $S/many/*; rmdir $S/many; ls | wc -l; grep VmRSS /proc/\$PPID/status | awk '{print \$2}' > $TMP/rss2"
-expect_eq "5000 entries: ls and ls -l" $'5000\n5001\n0' "$OUT"
-r1=$(cat "$TMP/rss1"); r2=$(cat "$TMP/rss2")
-if [ "$r2" -le $((r1 + 2048)) ]; then pass "RSS after cleanup ($r2 KiB) <= after listing ($r1 KiB)+2 MiB"; else fail "RSS grew after cleanup: $r1 -> $r2 KiB"; fi
-py "
-import os
-d='$S/chg'; os.mkdir(d)
-for i in range(50): open(f'{d}/a{i}','w').close()
-it = os.scandir(d); first = next(it).name
-for i in range(3000): open(f'{d}/b{i}','w').close()
-rest = [e.name for e in it]
-print(first[0], len(rest) >= 49, len(set(rest)) == len(rest))
-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"
-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"
-expect_eq "statfs, df, syncfs, sync" $'fuse 4096 255\n9player\nsynced\nsynced2' "$OUT"
-
-echo "# server refusals keep their errno through the error path"
-run "echo x > $M/build/zig_version; a=\$?; mkdir $M/build/x 2>/dev/null; b=\$?; rm $M/README 2>/dev/null; c=\$?; rmdir $M/build 2>/dev/null; d=\$?; echo \$a\$b\$c\$d" 2>/dev/null
-expect_eq "open-for-write / mkdir / rm / rmdir on read-only nodes all fail (errno preserved through error path)" "1111" "$(printf '%s\n' "$OUT" | tail -1)"
-
-echo "# unsupported operations fail cleanly"
-run "echo x > $S/l1; ln $S/l1 $S/l2 2>&1 | sed 's/.*: //'; ln -s l1 $S/l3 2>&1 | sed 's/.*: //'; mkfifo $S/p 2>&1 | sed 's/.*: //'; ls $S | tr '\n' ' '; echo; rm $S/l1"
-# The kernel turns ENOSYS from LINK into EPERM (fuse_link); symlink/mknod keep ENOSYS.
-expect_eq "link/symlink/mknod fail cleanly" $'Operation not permitted\nFunction not implemented\nFunction not implemented\nl1 ' "$OUT"
-run "echo x > $S/x1; setfattr -n user.a -v 1 $S/x1 2>&1 | sed 's/.*: //'; getfattr -n user.a $S/x1 2>&1 | sed 's/.*: //'; getfattr -d $S/x1 2>&1 | sed 's/.*: //'; rm $S/x1"
-expect_eq "xattr ops are EOPNOTSUPP" $'Operation not supported\nOperation not supported\nOperation not supported' "$OUT"
-py "
-import os, fcntl, mmap, errno
-p='$S/mm'; open(p,'w').write('mapme')
-fd=os.open(p, os.O_RDWR)
-fcntl.flock(fd, fcntl.LOCK_EX); fcntl.flock(fd, fcntl.LOCK_UN); fcntl.lockf(fd, fcntl.LOCK_EX); fcntl.lockf(fd, fcntl.LOCK_UN); print('locks ok')
-try:
- m = mmap.mmap(fd, 5); print('shared', bytes(m)); m.close()
-except OSError as e: print('shared', errno.errorcode[e.errno])
-try:
- m = mmap.mmap(fd, 5, flags=mmap.MAP_PRIVATE, prot=mmap.PROT_READ); print('private', bytes(m)); m.close()
-except OSError as e: print('private', errno.errorcode[e.errno])
-os.close(fd); os.unlink(p)
-"
-expect_contains "flock/lockf work (local locks)" "locks ok" "$OUT"
-case "$OUT" in *"shared ENODEV"*|*"shared b'mapme'"*) pass "shared mmap: clean result ($(printf '%s\n' "$OUT" | sed -n 2p))";; *) fail "shared mmap" "$OUT";; esac
-expect_contains "private mmap reads the file" "private b'mapme'" "$OUT"
-
-echo "# tools"
-mkdir -p "$TMP/tree/sub/deeper"; echo one > "$TMP/tree/a"; echo two > "$TMP/tree/sub/b"; head -c 70000 /dev/urandom > "$TMP/tree/sub/deeper/blob"; chmod 640 "$TMP/tree/a"
-run "cp -a $TMP/tree $S/tree 2>&1; diff -r $TMP/tree $S/tree && echo same; stat -c %a $S/tree/a; cp -a $S/tree $TMP/back && diff -r $TMP/tree $TMP/back && echo back; rm -r $S/tree"
-expect_eq "cp -a there and back" $'same\n640\nback' "$OUT"
-run "cd $TMP && tar cf $S/t.tar tree && cd $S && mkdir tx && tar xf t.tar -C tx && diff -r $TMP/tree tx/tree && echo tar-ok; rm -r $S/tx $S/t.tar"
-expect_eq "tar into and out of the mount" "tar-ok" "$OUT"
-run "rsync -a $TMP/tree/ $S/rs/ && diff -r $TMP/tree $S/rs && echo rsync-ok; sleep 1.1; echo mod > $TMP/tree/a; rsync -a $TMP/tree/ $S/rs/ && cat $S/rs/a; rm -r $S/rs"
-expect_eq "rsync -a twice" $'rsync-ok\nmod' "$OUT"
-run "cd $S && mkdir repo && cd repo && git init -q . && git config user.email a@b && git config user.name n && echo hi > f && git add f && git commit -qm init && git log --oneline | wc -l && git status --porcelain | wc -l; cd $S && rm -rf repo; ls $S | wc -l"
-expect_eq "git init/add/commit inside the mount" $'1\n0\n0' "$OUT"
-
-echo "# --no-direct-io"
-EXTRA=(--no-direct-io)
-run "cp $TMP/four $S/nd && cmp $TMP/four $S/nd && echo same; stat -c %s $S/nd; rm $S/nd"
-expect_eq "no-direct-io: 4 MiB round trip" $'same\n4194304' "$OUT"
-[ "$RC" -eq 0 ] || echo " stderr: $STDERR"
-# Buffered writes are per-page without a writeback cache (kernel behaviour); the
-# point here is only that a large buffered write is delivered intact.
-EXTRA=(--no-direct-io)
-head -c 262144 /dev/urandom > "$TMP/w"
-WSUM=$(sha256sum <"$TMP/w" | cut -d' ' -f1)
-run "cp $TMP/w $S/w && sha256sum < $S/w | cut -d' ' -f1; stat -c %s $S/w; rm $S/w"
-expect_eq "no-direct-io: 256 KiB buffered write is intact" "$WSUM"$'\n262144' "$OUT"
-EXTRA=()
-
-echo
-echo "passed=$PASSED failed=$FAILED"
-[ "$FAILED" -eq 0 ]
diff --git a/9player/test/adv_bridge_stress.sh b/9player/test/adv_bridge_stress.sh
deleted file mode 100755
index 3d1203a..0000000
--- a/9player/test/adv_bridge_stress.sh
+++ /dev/null
@@ -1,64 +0,0 @@
-#!/usr/bin/env bash
-# Resource and concurrency stress through the bridge against introspect.
-# Usage: bash 9player/test/adv_bridge_stress.sh <9player> <introspect> (~1-2 min; part of zig build 9player-adv)
-set -u
-PLAYER=$(realpath "${1:?path to 9player}")
-INTROSPECT=$(realpath "${2:?path to introspect}")
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-stress.XXXXXX")
-M=/mnt/9p
-S=$M/scratch
-FAILED=0
-PASSED=0
-SRVPID=
-cleanup() { [ -n "$SRVPID" ] && kill "$SRVPID" 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; }
-
-SOCK=$TMP/i.sock
-"$INTROSPECT" --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 "$PLAYER" --unix "$SOCK" -- 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.
-run "rss() { grep VmRSS /proc/\$PPID/status | awk '{print \$2}'; }
-i=0; while [ \$i -lt 8000 ]; do echo \$i > $S/s; cat $S/s > /dev/null; rm $S/s; cat $S/none 2>/dev/null; i=\$((i+1)); if [ \$i -eq 2000 ]; then rss; fi; done; rss; ls $S | wc -l"
-r1=$(printf '%s\n' "$OUT" | sed -n 1p); r2=$(printf '%s\n' "$OUT" | sed -n 2p); left=$(printf '%s\n' "$OUT" | sed -n 3p)
-expect_eq "scratch left clean" "0" "$left"
-if [ -n "$r1" ] && [ -n "$r2" ] && [ "$r2" -le $((r1 + 4096)) ]; then pass "RSS at 2000 iterations = $r1 KiB, at 8000 = $r2 KiB"; else fail "RSS grows: $r1 -> $r2 KiB" "$STDERR"; fi
-case "$STDERR" in *leak*) fail "allocator reported leaks" "$STDERR";; *) pass "no leak report from the debug allocator";; esac
-
-echo "# eight processes hammering the mount concurrently"
-run "mkdir $S/par; for p in 1 2 3 4 5 6 7 8; do (
- d=$S/par/p\$p; mkdir \$d; i=0; bad=0
- while [ \$i -lt 300 ]; do
- printf '%s-%s' \$p \$i > \$d/f\$((i % 7)); v=\$(cat \$d/f\$((i % 7))); [ \"\$v\" = \"\$p-\$i\" ] || bad=\$((bad+1))
- mkdir \$d/dd; rmdir \$d/dd; ls \$d > /dev/null; i=\$((i+1))
- done; rm -r \$d; echo \$p:\$bad ) & done; wait; ls $S/par | wc -l; rmdir $S/par"
-expect_eq "all workers verified their own data" "1:0 2:0 3:0 4:0 5:0 6:0 7:0 8:0" "$(printf '%s\n' "$OUT" | grep ':' | sort | tr '\n' ' ' | sed 's/ $//')"
-expect_eq "parallel tree fully removed" "0" "$(printf '%s\n' "$OUT" | grep -v ':')"
-
-echo "# a process killed mid-read and mid-write"
-run "head -c 8000000 /dev/urandom > $S/kb; (cat $S/kb > /dev/null & sleep 0.05; kill -9 \$!; wait \$!) 2>/dev/null; (cat /dev/zero > $S/kw & sleep 0.05; kill -9 \$!; wait \$!) 2>/dev/null; sha256sum < $S/kb | cut -c1-8 > /dev/null && echo readable; [ -f $S/kw ] && echo written; rm $S/kb $S/kw; ls $S | wc -l"
-expect_eq "survives SIGKILL mid-read/mid-write" $'readable\nwritten\n0' "$OUT"
-
-echo "# many open handles at once (fh counter, fid table)"
-run "python3 - <<'EOF'
-import os
-d='$S/fh'; os.mkdir(d)
-fds=[]
-for i in range(1500):
- fd=os.open(f'{d}/h{i%50}', os.O_CREAT|os.O_RDWR, 0o644); os.write(fd, b'z'); fds.append(fd)
-for fd in fds: os.close(fd)
-for i in range(50): os.unlink(f'{d}/h{i}')
-os.rmdir(d); print('ok')
-EOF"
-expect_eq "1500 simultaneous handles" "ok" "$OUT"
-
-echo
-echo "passed=$PASSED failed=$FAILED"
-[ "$FAILED" -eq 0 ]
diff --git a/9player/test/adv_ns_process.sh b/9player/test/adv_ns_process.sh
deleted file mode 100755
index 4ce0ae8..0000000
--- a/9player/test/adv_ns_process.sh
+++ /dev/null
@@ -1,202 +0,0 @@
-#!/usr/bin/env bash
-# Adversarial regression tests for 9player/src/ns.zig and 9player/src/main.zig: process,
-# namespace, signal and CLI handling. Real namespaces, real FUSE.
-# Usage: bash 9player/test/adv_ns_process.sh <9player> <introspect> (part of zig build 9player-adv)
-# Exit 0 on success (or when the machine cannot run the tests), 1 on failure.
-set -u
-
-PLAYER=$(realpath "${1:?path to 9player}")
-INTROSPECT=$(realpath "${2:?path to introspect}")
-# Unix socket paths are limited to ~107 bytes; keep the temp dir short.
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/9padv.XXXXXX")
-PIDS=()
-FAILED=0
-PASSED=0
-
-cleanup() {
- for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null; done
- rm -rf "$TMP"
-}
-trap cleanup EXIT
-
-if ! unshare -Urm true 2>/dev/null; then echo "SKIP: unprivileged user namespaces unavailable"; exit 0; fi
-if [ ! -c /dev/fuse ]; then echo "SKIP: /dev/fuse missing"; 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; }
-
-SOCK=$TMP/s
-"$INTROSPECT" --unix "$SOCK" &
-PIDS+=($!)
-for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; sleep 0.05; done
-[ -S "$SOCK" ] || { echo "introspect did not create $SOCK"; exit 1; }
-MI_BEFORE=$(grep -v " $TMP" /proc/self/mountinfo | sort)
-
-TIMEOUT=$(command -v timeout)
-run() { "$TIMEOUT" 60 "$PLAYER" --unix "$SOCK" "$@"; }
-
-echo "# CLI"
-expect_eq "--help goes to stdout, exit 0" "Usage: 9player" "$(run --help 2>/dev/null | head -1 | cut -c1-14; )"
-expect_eq "--help exit code" "0" "$("$PLAYER" --help >/dev/null 2>&1; echo $?)"
-expect_eq "--version on stdout" "9player" "$("$PLAYER" --version 2>/dev/null | cut -d' ' -f1)"
-expect_eq "single-dash typo is a usage error, not a program" "125" "$(run -mount /x -- true 2>/dev/null; echo $?)"
-expect_contains "single-dash typo message" "unknown option -mount" "$(run -mount /x -- true 2>&1)"
-expect_eq "--unix= empty is a usage error" "125" "$("$PLAYER" --unix= -- true 2>/dev/null; echo $?)"
-expect_contains "--unix= message" "socket path" "$("$PLAYER" --unix= -- true 2>&1)"
-expect_eq "--mount '' is a usage error" "125" "$(run --mount '' -- true 2>/dev/null; echo $?)"
-expect_eq "--msize huge rejected" "125" "$(run --msize 4294967295 -- true 2>/dev/null; echo $?)"
-expect_eq "--msize 16 MiB accepted" "ok" "$(run --msize 16777216 -- sh -c 'echo ok')"
-expect_contains "empty program name is reported" "empty program name" "$(run -- '' 2>&1)"
-expect_eq "empty program name exit" "125" "$(run -- '' 2>/dev/null; echo $?)"
-expect_eq "empty \$SHELL falls back to /bin/sh" "0" "$(SHELL= run -- </dev/null >/dev/null 2>&1; echo $?)"
-expect_eq "--fd with a closed descriptor fails early" "125" "$("$PLAYER" --fd 987 -- true 2>/dev/null; echo $?)"
-expect_contains "--fd bad descriptor message" "--fd 987: EBADF" "$("$PLAYER" --fd 987 -- true 2>&1)"
-
-echo "# exec failures"
-expect_eq "not found is 127" "127" "$(run -- no-such-program-9player 2>/dev/null; echo $?)"
-expect_eq "PATH element that is a file: still 127" "127" "$(PATH=/etc/passwd run -- true 2>/dev/null; echo $?)"
-expect_contains "PATH element that is a file: message" "exec true: E" "$(PATH=/etc/passwd run -- true 2>&1)"
-printf '#!/bin/sh\necho no\n' >"$TMP/nx"; chmod 644 "$TMP/nx"
-expect_eq "non-executable is 126" "126" "$(run -- "$TMP/nx" 2>/dev/null; echo $?)"
-mkdir -p "$TMP/p1" "$TMP/p2"; cp "$TMP/nx" "$TMP/p1/prog"; printf '#!/bin/sh\necho right\n' >"$TMP/p2/prog"; chmod 755 "$TMP/p2/prog"
-expect_eq "non-executable first in PATH, executable later" "right" "$(PATH=$TMP/p1:$TMP/p2 run -- prog)"
-expect_eq "non-executable only in PATH is 126" "126" "$(PATH=$TMP/p1 run -- prog 2>/dev/null; echo $?)"
-expect_eq "argv[0] preserved" "sh" "$(run -- sh -c 'echo $0')"
-expect_eq "PATH unset uses default" "ok" "$(env -u PATH "$PLAYER" --unix "$SOCK" -- sh -c 'echo ok')"
-
-echo "# fd hygiene"
-# 9player passes inherited descriptors through untouched, so compare with what a
-# plain child of this script sees (the runner may itself hold extra fds).
-FD_LIST='ls /proc/self/fd | grep -v "^3$" | sort -n | tr "\n" " " | sed "s/ $//"'
-FD_BASE=$(sh -c "$FD_LIST")
-expect_eq "no extra fds in the program (unix)" "$FD_BASE" "$(run -- sh -c "$FD_LIST")"
-expect_eq "no extra fds in the program (spawn)" "$FD_BASE" "$("$TIMEOUT" 60 "$PLAYER" --spawn "$INTROSPECT --stdio" -- sh -c "$FD_LIST")"
-expect_eq "--fd transport does not leak into the program" "0 1 2" "$(python3 - "$PLAYER" "$SOCK" <<'EOF'
-import socket, subprocess, sys, os
-s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(sys.argv[2])
-r = subprocess.run([sys.argv[1], "--fd", str(s.fileno()), "--", "sh", "-c",
- 'ls /proc/self/fd | grep -v "^3$" | sort -n | tr "\n" " " | sed "s/ $//"'],
- pass_fds=[s.fileno()], capture_output=True, text=True)
-print(r.stdout.strip())
-EOF
-)"
-
-echo "# signals"
-expect_eq "SIGTERM forwarded" "143" "$(run -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)"
-expect_eq "SIGHUP forwarded" "129" "$(run -- sh -c 'kill -HUP $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)"
-expect_eq "SIGINT to 9player is ignored while the child lives" "still-here" "$(run -- sh -c 'kill -INT $PPID; sleep 0.3; echo still-here')"
-# Ctrl-C from the tty must not kill the --spawn server (same process group).
-expect_eq "Ctrl-C on the tty leaves the --spawn server alive" "ok" "$(timeout 30 python3 - "$PLAYER" "$INTROSPECT" <<'EOF'
-import os, pty, sys, time, select
-P, I = sys.argv[1], sys.argv[2]
-prog = ["python3", "-c", """
-import os, signal, sys, time
-signal.signal(signal.SIGINT, lambda *a: None)
-m = os.environ['NINEPLAYER_MOUNT']
-open(m + '/build/optimize').read()
-sys.stdin.readline()
-try:
- open(m + '/build/optimize').read(); print('ok', flush=True)
-except Exception as e:
- print('mount dead:', e, flush=True)
-"""]
-pid, fd = pty.fork()
-if pid == 0:
- os.execv(P, [P, "--spawn", I + " --stdio", "--"] + prog)
-out = b""
-def rd(t):
- global out
- end = time.time() + t
- while time.time() < end:
- r, _, _ = select.select([fd], [], [], 0.1)
- if r:
- try: d = os.read(fd, 4096)
- except OSError: return
- if not d: return
- out += d
-rd(1.5); os.write(fd, b"\x03"); rd(0.7); os.write(fd, b"\n"); rd(3)
-os.waitpid(pid, 0)
-print(out.decode(errors="replace").replace("^C", "").strip().splitlines()[-1] if out.strip() else "no output")
-EOF
-)"
-# The --spawn server dying mid-session is reaped (no zombie) and does not end the session.
-OUT=$("$TIMEOUT" 60 "$PLAYER" --spawn "$INTROSPECT --stdio" -- sh -c 'srv=$(cat $NINEPLAYER_MOUNT/runtime/pid); kill -TERM $srv; sleep 0.5; st=$(ps -o stat= -p $srv 2>/dev/null); echo "${st:-gone}"; exit 5' 2>/dev/null); RC=$?
-expect_eq "server death mid-session: exit status still the child's, server reaped (no zombie)" "5 gone" "$RC $OUT"
-# A server that never answers: once the child is dead, SIGTERM must end 9player.
-cat >"$TMP/hang.py" <<'EOF'
-import struct, os, sys, time
-def rd(n):
- b = b""
- while len(b) < n:
- c = os.read(0, n - len(b))
- if not c: sys.exit(0)
- b += c
- return b
-while True:
- size, = struct.unpack("<I", rd(4)); body = rd(size - 4)
- typ, tag = struct.unpack("<BH", body[:3])
- if typ == 100:
- msize, = struct.unpack("<I", body[3:7]); v = b"9P2000"
- r = struct.pack("<BHI", 101, tag, msize) + struct.pack("<H", len(v)) + v
- elif typ == 104:
- r = struct.pack("<BH", 105, tag) + bytes([0x80]) + struct.pack("<IQ", 0, 0)
- else:
- time.sleep(3600)
- os.write(1, struct.pack("<I", 4 + len(r)) + r)
-EOF
-"$PLAYER" --spawn "python3 $TMP/hang.py" -- true 2>/dev/null &
-HP=$!
-sleep 1; kill -TERM $HP
-START=$(date +%s)
-for _ in $(seq 1 100); do kill -0 $HP 2>/dev/null || break; sleep 0.1; done
-if kill -0 $HP 2>/dev/null; then kill -KILL $HP; RC=hung; else wait $HP; RC=$?; fi
-expect_eq "hung server: one SIGTERM ends 9player once the child is dead (watchdog)" "143" "$RC"
-expect_eq "hung server: exit was prompt" "yes" "$([ $(( $(date +%s) - START )) -lt 8 ] && echo yes)"
-pkill -f "$TMP/hang.py" 2>/dev/null
-
-echo "# child/parent protocol"
-if command -v strace >/dev/null 2>&1 && strace -qq -e trace=none true 2>/dev/null; then
- expect_eq "child killed before handoff" "125" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>/dev/null; echo $?)"
- expect_contains "child killed before handoff: message" "child exited before reporting" "$(strace -f -qq -e trace=unshare -e inject=unshare:signal=KILL -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>&1)"
- expect_eq "status handoff fails" "125" "$(strace -f -qq -e trace=sendmsg -e inject=sendmsg:error=EPIPE -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- true 2>/dev/null; echo $?)"
- # recvmsg skipped (returns 1 without the fd): the child must be killed, not exec'd onto a dead mount.
- OUT=$(strace -f -qq -e trace=recvmsg -e inject=recvmsg:retval=1:when=1 -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" -- sh -c 'echo child-ran' 2>&1; echo "rc=$?")
- expect_contains "truncated fd handoff: child not exec'd" "rc=125" "$OUT"
- expect_eq "truncated fd handoff: program never ran" "no" "$(case "$OUT" in *child-ran*) echo yes;; *) echo no;; esac)"
- expect_contains "fuse mount failure is reported" "mount fuse: EPERM" "$(strace -f -qq -e trace=mount -e inject=mount:error=EPERM:when=2 -o /dev/null timeout 20 "$PLAYER" --unix "$SOCK" --mount "$TMP/mp" -- true 2>&1)"
-else
- echo "skip - strace unavailable (child failure injection)"
-fi
-expect_contains "fork failure is reported" "fork: E" "$(python3 -c "
-import resource, os
-resource.setrlimit(resource.RLIMIT_NPROC, (1, 1))
-os.execv('$PLAYER', ['$PLAYER', '--unix', '$SOCK', '--', 'true'])" 2>&1)"
-
-echo "# mountpoint policy"
-ln -s /nonexistent "$TMP/dangling"
-expect_contains "dangling symlink mountpoint" "dangling symlink" "$(run --mount "$TMP/dangling" -- true 2>&1)"
-expect_eq "refuse to shadow / via /proc/self/root" "125" "$(run --mount /proc/self/root/x9p -- true 2>/dev/null; echo $?)"
-expect_contains "refuse to shadow / via /proc/self/root: message" "refusing to shadow /" "$(run --mount /proc/self/root/x9p -- true 2>&1)"
-expect_eq "refuse to shadow under /proc" "125" "$(run --mount /proc/self/fd/x9p -- true 2>/dev/null; echo $?)"
-if [ "$(ls -A /usr/lib | wc -l)" -gt 4096 ]; then
- expect_contains "parent with >4096 entries refused" "more than 4096 entries" "$(run --mount /usr/lib/x9p -- true 2>&1)"
-else
- echo "skip - no root-owned directory with >4096 entries"
-fi
-expect_eq "shadowed /run keeps its entries" "$(ls -A /run | sort | tr '\n' ' ')" "$(run --mount /run/x9p -- sh -c 'ls -A /run | grep -v "^x9p$" | sort | tr "\n" " "')"
-expect_eq "mountpoint with spaces" "ok" "$(mkdir -p "$TMP/with space" && run --mount "$TMP/with space" -- sh -c '[ -f "$NINEPLAYER_MOUNT/README" ] && echo ok')"
-expect_eq "mountpoint is a file" "125" "$(run --mount "$TMP/nx" -- true 2>/dev/null; echo $?)"
-
-echo "# leaks"
-for i in $(seq 1 30); do run -- sh -c 'cat $NINEPLAYER_MOUNT/build/optimize >/dev/null' 2>/dev/null; done
-BG=(); for i in $(seq 1 10); do ( run -- sh -c 'cat $NINEPLAYER_MOUNT/build/optimize >/dev/null' 2>/dev/null ) & BG+=($!); done; wait "${BG[@]}" # not a bare wait: that would also wait for the server
-sleep 0.3
-expect_eq "no stray 9player processes" "" "$(pgrep -f "^$PLAYER " | tr '\n' ' ')"
-expect_eq "no stray --stdio servers" "" "$(pgrep -f "$INTROSPECT --stdio" | tr '\n' ' ')"
-expect_eq "host mount table untouched" "same" "$([ "$MI_BEFORE" = "$(grep -v " $TMP" /proc/self/mountinfo | sort)" ] && echo same || echo changed)"
-
-echo
-echo "passed=$PASSED failed=$FAILED"
-[ "$FAILED" -eq 0 ]
diff --git a/9player/test/adversarial.sh b/9player/test/adversarial.sh
deleted file mode 100755
index 857af14..0000000
--- a/9player/test/adversarial.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env bash
-# Runs every 9player adversarial suite in sequence (hostile servers, FUSE
-# semantics, process/namespace edge cases, stress). The suites aimed at the
-# introspect server itself live in introspect/test (zig build introspect-adv).
-# Usage: bash 9player/test/adversarial.sh <9player> <introspect> (zig build 9player-adv)
-set -u
-PLAYER=${1:?path to 9player}
-INTROSPECT=${2:?path to introspect}
-HERE=$(cd "$(dirname "$0")" && pwd)
-status=0
-for suite in adv_ns_process adv_bridge_hostile adv_bridge_semantics adv_bridge_stress; do
- echo "### $suite"
- if bash "$HERE/$suite.sh" "$PLAYER" "$INTROSPECT"; then echo "### $suite: ok"; else echo "### $suite: FAILED"; status=1; fi
-done
-exit $status
diff --git a/9player/test/integration.sh b/9player/test/integration.sh
deleted file mode 100755
index e12cb5f..0000000
--- a/9player/test/integration.sh
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env bash
-# Integration tests for 9player: real user+mount namespaces, real FUSE, real 9P servers.
-# Usage: bash 9player/test/integration.sh <9player> <introspect> (zig build 9player-itest)
-# Exit 0 on success (or when the machine cannot run the tests), 1 on failure.
-set -u
-
-PLAYER=$(realpath "${1:?path to 9player}")
-INTROSPECT=$(realpath "${2:?path to introspect}")
-TMP=$(mktemp -d "${TMPDIR:-/tmp}/9player-itest.XXXXXX")
-PIDS=()
-FAILED=0
-PASSED=0
-M=/mnt/9p
-
-cleanup() {
- for p in "${PIDS[@]:-}"; do [ -n "$p" ] && kill "$p" 2>/dev/null; done
- rm -rf "$TMP"
-}
-trap cleanup EXIT
-
-if ! unshare -Urm true 2>/dev/null; then
- echo "SKIP: unprivileged user namespaces unavailable"; exit 0
-fi
-if [ ! -c /dev/fuse ]; then
- echo "SKIP: /dev/fuse missing"; 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() { # name expected actual
- if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected: $(printf %q "$2")" "actual: $(printf %q "$3")"; fi
-}
-expect_contains() { # name needle haystack
- case "$3" in *"$2"*) pass "$1" ;; *) fail "$1" "missing: $(printf %q "$2")" "in: $(printf %q "$3")" ;; esac
-}
-
-wait_socket() { # path
- for _ in $(seq 1 100); do [ -S "$1" ] && return 0; sleep 0.05; done
- return 1
-}
-
-# run_in "<shell script>" — run inside a namespace with the current transport ($TRANSPORT array).
-run_in() { timeout 60 "$PLAYER" "${TRANSPORT[@]}" -- sh -c "$1" 2>"$TMP/stderr"; }
-
-# --- scratch battery: works against any writable 9P tree rooted at $1 (relative to mount) ---
-scratch_battery() { # label scratchdir
- local label=$1 S=$2
-
- expect_eq "$label: create+append+read" $'hello\nworld' "$(run_in "echo hello > $M/$S/a && echo world >> $M/$S/a && cat $M/$S/a")"
- expect_eq "$label: overwrite" "x" "$(run_in "echo x > $M/$S/a && cat $M/$S/a")"
- expect_eq "$label: stat size after overwrite" "2" "$(run_in "stat -c %s $M/$S/a")"
- expect_eq "$label: truncate" "0" "$(run_in "truncate -s 0 $M/$S/a && stat -c %s $M/$S/a")"
- expect_eq "$label: truncate extend" "10" "$(run_in "truncate -s 10 $M/$S/a && stat -c %s $M/$S/a")"
- expect_eq "$label: mkdir -p nested" "directory" "$(run_in "mkdir -p $M/$S/d1/d2/d3 && stat -c %F $M/$S/d1/d2/d3")"
- expect_eq "$label: rename within dir" "moved" "$(run_in "echo moved > $M/$S/d1/d2/f && mv $M/$S/d1/d2/f $M/$S/d1/d2/g && cat $M/$S/d1/d2/g")"
- # mv(1) silently falls back to copy+delete on EXDEV, so probe rename(2) directly.
- expect_contains "$label: rename across dirs is EXDEV" "EXDEV" "$(run_in "python3 -c 'import os,errno
-try: os.rename(\"$M/$S/d1/d2/g\", \"$M/$S/d1/g\")
-except OSError as e: print(errno.errorcode[e.errno])
-'")"
- expect_eq "$label: rm file" "gone" "$(run_in "rm $M/$S/d1/d2/g && [ ! -e $M/$S/d1/d2/g ] && echo gone")"
- expect_eq "$label: rmdir non-empty fails" "1" "$(run_in "rmdir $M/$S/d1 2>/dev/null; echo \$?")"
- expect_eq "$label: rmdir chain" "ok" "$(run_in "rmdir $M/$S/d1/d2/d3 $M/$S/d1/d2 $M/$S/d1 && echo ok")"
- expect_eq "$label: ENOENT" "1" "$(run_in "cat $M/$S/nope 2>/dev/null; echo \$?")"
- expect_eq "$label: ENOENT errno text" "No such file or directory" "$(run_in "cat $M/$S/nope 2>&1 | sed 's/.*: //'")"
-
- head -c 1048576 /dev/urandom >"$TMP/rand"
- local sum; sum=$(sha256sum <"$TMP/rand" | cut -d' ' -f1)
- expect_eq "$label: 1 MiB round trip (cp)" "$sum" "$(run_in "cp $TMP/rand $M/$S/big && sha256sum < $M/$S/big | cut -d' ' -f1")"
- expect_eq "$label: 1 MiB size" "1048576" "$(run_in "stat -c %s $M/$S/big")"
- expect_eq "$label: odd block sizes (dd bs=1000)" "$sum" "$(run_in "dd if=$M/$S/big of=$M/$S/big2 bs=1000 status=none && sha256sum < $M/$S/big2 | cut -d' ' -f1")"
- expect_eq "$label: partial read at offset" "$(tail -c 12345 "$TMP/rand" | sha256sum | cut -d' ' -f1)" "$(run_in "tail -c 12345 $M/$S/big | sha256sum | cut -d' ' -f1")"
- expect_eq "$label: many small files" "200" "$(run_in "mkdir $M/$S/many && for i in \$(seq 1 200); do echo \$i > $M/$S/many/f\$i; done; ls $M/$S/many | wc -l")"
- expect_eq "$label: find count" "201" "$(run_in "find $M/$S/many | wc -l")"
- expect_eq "$label: readdir contents" "f1 f100 f200" "$(run_in "cd $M/$S/many && ls f1 f100 f200 | tr '\n' ' ' | sed 's/ \$//'")"
- expect_eq "$label: cleanup many" "0" "$(run_in "rm -r $M/$S/many $M/$S/big $M/$S/big2 $M/$S/a; ls $M/$S | wc -l")"
-}
-
-# ============================================================================
-echo "# introspect over a Unix socket"
-SOCK=$TMP/introspect.sock
-"$INTROSPECT" --unix "$SOCK" &
-PIDS+=($!)
-wait_socket "$SOCK" || { echo "introspect did not create $SOCK"; exit 1; }
-TRANSPORT=(--unix "$SOCK")
-
-expect_eq "zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")"
-expect_eq "mount exported" "$M" "$(run_in 'echo $NINEPLAYER_MOUNT')"
-expect_contains "root listing" "build" "$(run_in "ls $M")"
-expect_contains "root listing has scratch" "scratch" "$(run_in "ls $M")"
-expect_eq "README size > 0" "yes" "$(run_in "[ \$(stat -c %s $M/README) -gt 0 ] && echo yes")"
-expect_eq "README readable" "yes" "$(run_in "[ -s $M/README ] && head -c 1 $M/README >/dev/null && echo yes")"
-expect_eq "fn/now numeric" "num" "$(run_in "cat $M/runtime/fn/now | grep -Eq '^[0-9]+\$' && echo num")"
-expect_eq "fn listing from comptime" "yes" "$(run_in "ls $M/runtime/fn | grep -q hostname && echo yes")"
-expect_eq "ctl round trip" "5" "$(run_in "echo 'add 2 3' > $M/runtime/ctl && cat $M/runtime/ctl")"
-expect_eq "ctl echo" "hi there" "$(run_in "echo 'echo hi there' > $M/runtime/ctl && cat $M/runtime/ctl")"
-expect_contains "comptime types" "Qid" "$(run_in "ls $M/comptime/types")"
-expect_eq "comptime size of Qid" "16" "$(run_in "cat $M/comptime/types/Qid/size")"
-expect_eq "runtime pid is server pid" "${PIDS[-1]}" "$(run_in "cat $M/runtime/pid")"
-expect_eq "exit status propagates" "7" "$(run_in 'exit 7'; echo $?)"
-expect_eq "mount is fuse" "yes" "$(run_in "grep -q \"^9player $M fuse\" /proc/mounts && echo yes")"
-expect_eq "host /mnt entries still visible" "$(ls -A /mnt | sort | tr '\n' ' ')" "$(run_in "ls -A /mnt | grep -v '^9p\$' | sort | tr '\n' ' '")"
-expect_eq "host mount table untouched" "no" "$(grep -q " $M " /proc/self/mountinfo && echo yes || echo no)"
-scratch_battery "introspect" scratch
-
-echo "# nested 9player"
-expect_eq "nested mount" "$(zig version)" "$(run_in "$PLAYER --unix $SOCK --mount $TMP/inner -- sh -c 'cat \$NINEPLAYER_MOUNT/build/zig_version'")"
-
-echo "# --mount variants"
-mkdir -p "$TMP/mnt"
-expect_eq "--mount existing dir" "ok" "$(timeout 60 "$PLAYER" --unix "$SOCK" --mount "$TMP/mnt" -- sh -c "[ -f $TMP/mnt/README ] && echo ok")"
-expect_eq "--mount relative" "ok" "$(cd "$TMP" && timeout 60 "$PLAYER" --unix "$SOCK" --mount rel -- sh -c "[ -f $TMP/rel/README ] && echo ok")"
-expect_eq "--mount missing under /" "125" "$(timeout 60 "$PLAYER" --unix "$SOCK" --mount /nonexistent-9player-dir -- true 2>/dev/null; echo $?)"
-
-echo "# lifecycle"
-START=$(date +%s)
-expect_eq "background grandchild does not block exit" "3" "$(run_in 'sleep 30 >/dev/null 2>&1 & exit 3'; echo $?)"
-expect_eq "exit was prompt" "yes" "$([ $(( $(date +%s) - START )) -lt 10 ] && echo yes)"
-expect_eq "SIGTERM forwarded" "143" "$(timeout 60 "$PLAYER" --unix "$SOCK" -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)"
-
-echo "# --spawn transport"
-TRANSPORT=(--spawn "$INTROSPECT --stdio")
-expect_eq "spawn: zig_version" "$(zig version)" "$(run_in "cat $M/build/zig_version")"
-expect_eq "spawn: ctl" "7" "$(run_in "echo 'add 3 4' > $M/runtime/ctl && cat $M/runtime/ctl")"
-expect_eq "spawn: stateful sequence in one session" 'hello world 12 moved 0' "$(run_in "cd $M/scratch && echo hello > a && echo world >> a && cat a && stat -c %s a && mkdir d && echo moved > d/f && mv d/f d/g && cat d/g && rm d/g && rmdir d && rm a && ls | wc -l" | tr '\n' ' ' | sed 's/ $//')"
-
-echo "# --tcp transport"
-PORT=$(( 20000 + RANDOM % 20000 ))
-"$INTROSPECT" --tcp "127.0.0.1:$PORT" &
-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: scratch" "tcp" "$(run_in "echo tcp > $M/scratch/t && cat $M/scratch/t && rm $M/scratch/t")"
-
-echo "# server death"
-"$INTROSPECT" --unix "$TMP/dying.sock" &
-DYING=$!
-wait_socket "$TMP/dying.sock"
-TRANSPORT=(--unix "$TMP/dying.sock")
-OUT=$(run_in "cat $M/build/optimize >/dev/null && kill $DYING && sleep 0.3; cat $M/build/optimize 2>&1 >/dev/null | sed 's/.*: //'; echo status=\$?")
-expect_contains "server death yields an error, not a hang" "status=0" "$OUT"
-expect_eq "server death errno text" "yes" "$(case "$OUT" in *"Input/output error"*|*"Transport endpoint is not connected"*) echo yes;; *) echo "no: $OUT";; esac)"
-
-echo "# plan9port ramfs (independent 9P2000 implementation)"
-if [ -x /usr/lib/plan9/bin/ramfs ]; then
- mkdir -p "$TMP/p9ns"
- NAMESPACE=$TMP/p9ns /usr/lib/plan9/bin/ramfs -s ramfs
- wait_socket "$TMP/p9ns/ramfs" || echo "ramfs socket missing"
- PIDS+=($(pgrep -f "9pserve -u unix!$TMP/p9ns/ramfs"))
- TRANSPORT=(--unix "$TMP/p9ns/ramfs")
- expect_eq "ramfs: mkdir scratch" "ok" "$(run_in "mkdir $M/scratch && echo ok")"
- scratch_battery "ramfs" scratch
-else
- echo "skip - plan9port ramfs not installed"
-fi
-
-echo
-echo "passed=$PASSED failed=$FAILED"
-[ "$FAILED" -eq 0 ]