blob: 32d132a5c0fa8c9e957e41370cfb40bdb4dec4ae (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|
#!/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/<name>
# (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 "<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
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/<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)
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 ]
|