summaryrefslogtreecommitdiff
path: root/9ns/test/adv_bridge_semantics.sh
diff options
context:
space:
mode:
Diffstat (limited to '9ns/test/adv_bridge_semantics.sh')
-rwxr-xr-x9ns/test/adv_bridge_semantics.sh205
1 files changed, 205 insertions, 0 deletions
diff --git a/9ns/test/adv_bridge_semantics.sh b/9ns/test/adv_bridge_semantics.sh
new file mode 100755
index 0000000..b38ce9c
--- /dev/null
+++ b/9ns/test/adv_bridge_semantics.sh
@@ -0,0 +1,205 @@
+#!/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" "${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\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 ]