#!/usr/bin/env bash # Integration tests for 9ns: real user+mount namespaces, real FUSE, real 9P servers. # Usage: bash 9ns/test/integration.sh <9ns> <9proc-demo> (zig build 9ns-itest) # Exit 0 on success (or when the machine cannot run the tests), 1 on failure. set -u NS=$(realpath "${1:?path to 9ns}") PROC=$(realpath "${2:?path to 9proc-demo}") 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/ # (see the "# mount names" section below). 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 "" — 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 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 "# 9proc-demo over a Unix socket" SOCK=$TMP/9proc.sock "$PROC" --unix "$SOCK" & PIDS+=($!) wait_socket "$SOCK" || { echo "9proc-demo 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 $NINE_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 \"^9ns $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 "9proc-demo" scratch echo "# nested 9ns" expect_eq "nested mount" "$(zig version)" "$(run_in "$NS --unix $SOCK --mount $TMP/inner -- sh -c 'cat \$NINE_MOUNT/build/zig_version'")" echo "# --mount variants" 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/)" 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) 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 "$NS" --unix "$SOCK" -- sh -c 'kill -TERM $PPID; sleep 5; echo alive' >/dev/null 2>&1; echo $?)" echo "# --spawn transport" TRANSPORT=(--spawn "$PROC --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 )) "$PROC" --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: 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" "$PROC" --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 ]