diff options
Diffstat (limited to '9ns/test/adv_bridge_semantics.sh')
| -rwxr-xr-x | 9ns/test/adv_bridge_semantics.sh | 205 |
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 ] |
