summaryrefslogtreecommitdiff
path: root/9ns/test/adv_bridge_semantics.sh
blob: 526ebc86c2cec869244e1236a742c3c505382fcd (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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/usr/bin/env bash
# FUSE semantics through the bridge against 9proc-demo's /scratch tree.
# Usage: bash 9ns/test/adv_bridge_semantics.sh <9ns> <9proc-demo>   (part of zig build 9ns-adv)
set -u
NS=$(realpath "${1:?path to 9ns}")
PROC=$(realpath "${2:?path to 9proc-demo}")
TMP=$(mktemp -d "${TMPDIR:-/tmp}/9ns-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
"$PROC" --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 "$NS" --unix "$SOCK" --mount "$M" "${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"
# 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"
expect_eq "statfs, df, syncfs, sync" $'fuse 4096 255\n9ns\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 ]