summaryrefslogtreecommitdiff
path: root/lib9p/tests
diff options
context:
space:
mode:
Diffstat (limited to 'lib9p/tests')
-rw-r--r--lib9p/tests/client_config/config.h19
-rwxr-xr-xlib9p/tests/runtest50
-rw-r--r--lib9p/tests/test_compile.c1255
-rwxr-xr-xlib9p/tests/test_compile.c.gen19
-rw-r--r--lib9p/tests/test_compile_config/config.h31
-rw-r--r--lib9p/tests/test_server/CMakeLists.txt45
-rw-r--r--lib9p/tests/test_server/config/config.h67
-rw-r--r--lib9p/tests/test_server/fs_flush.c131
-rw-r--r--lib9p/tests/test_server/fs_flush.h27
-rw-r--r--lib9p/tests/test_server/fs_shutdown.c104
-rw-r--r--lib9p/tests/test_server/fs_shutdown.h23
-rw-r--r--lib9p/tests/test_server/fs_whoami.c153
-rw-r--r--lib9p/tests/test_server/fs_whoami.h20
-rw-r--r--lib9p/tests/test_server/main.c168
-rw-r--r--lib9p/tests/test_server/static/Documentation/x.txt7
-rw-r--r--lib9p/tests/test_server/static/README.md7
-rwxr-xr-xlib9p/tests/testclient-p9p65
-rw-r--r--lib9p/tests/testclient-p9p.explog106
-rw-r--r--lib9p/tests/testclient-sess.c243
-rw-r--r--lib9p/tests/testclient-sess.explog156
20 files changed, 2696 insertions, 0 deletions
diff --git a/lib9p/tests/client_config/config.h b/lib9p/tests/client_config/config.h
new file mode 100644
index 0000000..bcf030d
--- /dev/null
+++ b/lib9p/tests/client_config/config.h
@@ -0,0 +1,19 @@
+/* config.h - Compile-time configuration for lib9p test clients
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _CONFIG_H_
+#define _CONFIG_H_
+
+#define CONFIG_9P_MAX_ERR_SIZE 128
+#define CONFIG_9P_MAX_9P2000_e_WELEM 16
+
+#define CONFIG_9P_ENABLE_9P2000 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_u 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_e 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_L 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_p9p 1 /* bool */
+
+#endif /* _CONFIG_H_ */
diff --git a/lib9p/tests/runtest b/lib9p/tests/runtest
new file mode 100755
index 0000000..6883391
--- /dev/null
+++ b/lib9p/tests/runtest
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# lib9p/tests/runtest - Test harness for the 9P `test_server`
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+set -euE -o pipefail
+
+build_aux=$(realpath --canonicalize-missing -- "${BASH_SOURCE[0]}/../../../build-aux")
+
+if [[ $# != 2 ]]; then
+ echo >&2 "Usage: $0 CLIENTSCRIPT EXPLOG"
+ exit 2
+fi
+clientscript="$1"
+explog="$2"
+
+cleanup=()
+cleanup() {
+ { set +x; } &>/dev/null
+ local i
+ for ((i = ${#cleanup[@]} - 1; i >= 0; i--)); do
+ eval "set -x; ${cleanup[$i]}"
+ { set +x; } &>/dev/null
+ done
+}
+trap cleanup EXIT
+
+logfile=$(mktemp -t lib9p-log.XXXXXXXXXX)
+cleanup+=("rm -f -- ${logfile@Q}")
+
+port=$(python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
+
+set -x
+
+"${build_aux}/valgrind" ./tests/test_server/test_server "$port" "$logfile" &
+server_pid=$!
+cleanup+=("kill $server_pid || true; wait $server_pid || true")
+while [[ -d /proc/$server_pid ]] && ! (readlink /proc/$server_pid/fd/* 2>/dev/null | grep -q ^socket:); do sleep 0.1; done
+
+if [[ "$(head -c2 -- "$clientscript")" == '#!' ]]; then
+ "$clientscript" "$port"
+else
+ "${build_aux}/valgrind" "$clientscript" "$port"
+fi
+
+wait "$server_pid"
+cleanup=("${cleanup[@]::1}")
+
+diff -u -- <(grep -e '^[<>]' -- "$explog") "$logfile"
diff --git a/lib9p/tests/test_compile.c b/lib9p/tests/test_compile.c
new file mode 100644
index 0000000..e53b738
--- /dev/null
+++ b/lib9p/tests/test_compile.c
@@ -0,0 +1,1255 @@
+/* lib9p/tests/test_compile.c - Generated by lib9p/tests/test_compile.c.gen. DO NOT EDIT! */
+
+#include <lib9p/core.h>
+int main(void) {
+ [[gnu::unused]] uint64_t x;
+#ifdef LIB9P_B4_FALSE
+ x = LIB9P_B4_FALSE;
+#endif
+#ifdef LIB9P_B4_TRUE
+ x = LIB9P_B4_TRUE;
+#endif
+#ifdef LIB9P_DM_APPEND
+ x = LIB9P_DM_APPEND;
+#endif
+#ifdef LIB9P_DM_AUTH
+ x = LIB9P_DM_AUTH;
+#endif
+#ifdef LIB9P_DM_DEVICE
+ x = LIB9P_DM_DEVICE;
+#endif
+#ifdef LIB9P_DM_DIR
+ x = LIB9P_DM_DIR;
+#endif
+#ifdef LIB9P_DM_EXCL
+ x = LIB9P_DM_EXCL;
+#endif
+#ifdef LIB9P_DM_GROUP_R
+ x = LIB9P_DM_GROUP_R;
+#endif
+#ifdef LIB9P_DM_GROUP_W
+ x = LIB9P_DM_GROUP_W;
+#endif
+#ifdef LIB9P_DM_GROUP_X
+ x = LIB9P_DM_GROUP_X;
+#endif
+#ifdef LIB9P_DM_OTHER_R
+ x = LIB9P_DM_OTHER_R;
+#endif
+#ifdef LIB9P_DM_OTHER_W
+ x = LIB9P_DM_OTHER_W;
+#endif
+#ifdef LIB9P_DM_OTHER_X
+ x = LIB9P_DM_OTHER_X;
+#endif
+#ifdef LIB9P_DM_OWNER_R
+ x = LIB9P_DM_OWNER_R;
+#endif
+#ifdef LIB9P_DM_OWNER_W
+ x = LIB9P_DM_OWNER_W;
+#endif
+#ifdef LIB9P_DM_OWNER_X
+ x = LIB9P_DM_OWNER_X;
+#endif
+#ifdef LIB9P_DM_PERM_MASK
+ x = LIB9P_DM_PERM_MASK;
+#endif
+#ifdef LIB9P_DM_PIPE
+ x = LIB9P_DM_PIPE;
+#endif
+#ifdef LIB9P_DM_SETGID
+ x = LIB9P_DM_SETGID;
+#endif
+#ifdef LIB9P_DM_SETUID
+ x = LIB9P_DM_SETUID;
+#endif
+#ifdef LIB9P_DM_SOCKET
+ x = LIB9P_DM_SOCKET;
+#endif
+#ifdef LIB9P_DM_TMP
+ x = LIB9P_DM_TMP;
+#endif
+#ifdef LIB9P_DT_BLOCK_DEV
+ x = LIB9P_DT_BLOCK_DEV;
+#endif
+#ifdef LIB9P_DT_CHAR_DEV
+ x = LIB9P_DT_CHAR_DEV;
+#endif
+#ifdef LIB9P_DT_DIRECTORY
+ x = LIB9P_DT_DIRECTORY;
+#endif
+#ifdef LIB9P_DT_PIPE
+ x = LIB9P_DT_PIPE;
+#endif
+#ifdef LIB9P_DT_REGULAR
+ x = LIB9P_DT_REGULAR;
+#endif
+#ifdef LIB9P_DT_SOCKET
+ x = LIB9P_DT_SOCKET;
+#endif
+#ifdef LIB9P_DT_SYMLINK
+ x = LIB9P_DT_SYMLINK;
+#endif
+#ifdef LIB9P_DT_UNKNOWN
+ x = LIB9P_DT_UNKNOWN;
+#endif
+#ifdef LIB9P_ERRNO_L_E2BIG
+ x = LIB9P_ERRNO_L_E2BIG;
+#endif
+#ifdef LIB9P_ERRNO_L_EACCES
+ x = LIB9P_ERRNO_L_EACCES;
+#endif
+#ifdef LIB9P_ERRNO_L_EADDRINUSE
+ x = LIB9P_ERRNO_L_EADDRINUSE;
+#endif
+#ifdef LIB9P_ERRNO_L_EADDRNOTAVAIL
+ x = LIB9P_ERRNO_L_EADDRNOTAVAIL;
+#endif
+#ifdef LIB9P_ERRNO_L_EADV
+ x = LIB9P_ERRNO_L_EADV;
+#endif
+#ifdef LIB9P_ERRNO_L_EAFNOSUPPORT
+ x = LIB9P_ERRNO_L_EAFNOSUPPORT;
+#endif
+#ifdef LIB9P_ERRNO_L_EAGAIN
+ x = LIB9P_ERRNO_L_EAGAIN;
+#endif
+#ifdef LIB9P_ERRNO_L_EALREADY
+ x = LIB9P_ERRNO_L_EALREADY;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADE
+ x = LIB9P_ERRNO_L_EBADE;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADF
+ x = LIB9P_ERRNO_L_EBADF;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADFD
+ x = LIB9P_ERRNO_L_EBADFD;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADMSG
+ x = LIB9P_ERRNO_L_EBADMSG;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADR
+ x = LIB9P_ERRNO_L_EBADR;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADRQC
+ x = LIB9P_ERRNO_L_EBADRQC;
+#endif
+#ifdef LIB9P_ERRNO_L_EBADSLT
+ x = LIB9P_ERRNO_L_EBADSLT;
+#endif
+#ifdef LIB9P_ERRNO_L_EBFONT
+ x = LIB9P_ERRNO_L_EBFONT;
+#endif
+#ifdef LIB9P_ERRNO_L_EBUSY
+ x = LIB9P_ERRNO_L_EBUSY;
+#endif
+#ifdef LIB9P_ERRNO_L_ECANCELED
+ x = LIB9P_ERRNO_L_ECANCELED;
+#endif
+#ifdef LIB9P_ERRNO_L_ECHILD
+ x = LIB9P_ERRNO_L_ECHILD;
+#endif
+#ifdef LIB9P_ERRNO_L_ECHRNG
+ x = LIB9P_ERRNO_L_ECHRNG;
+#endif
+#ifdef LIB9P_ERRNO_L_ECOMM
+ x = LIB9P_ERRNO_L_ECOMM;
+#endif
+#ifdef LIB9P_ERRNO_L_ECONNABORTED
+ x = LIB9P_ERRNO_L_ECONNABORTED;
+#endif
+#ifdef LIB9P_ERRNO_L_ECONNREFUSED
+ x = LIB9P_ERRNO_L_ECONNREFUSED;
+#endif
+#ifdef LIB9P_ERRNO_L_ECONNRESET
+ x = LIB9P_ERRNO_L_ECONNRESET;
+#endif
+#ifdef LIB9P_ERRNO_L_EDEADLK
+ x = LIB9P_ERRNO_L_EDEADLK;
+#endif
+#ifdef LIB9P_ERRNO_L_EDESTADDRREQ
+ x = LIB9P_ERRNO_L_EDESTADDRREQ;
+#endif
+#ifdef LIB9P_ERRNO_L_EDOM
+ x = LIB9P_ERRNO_L_EDOM;
+#endif
+#ifdef LIB9P_ERRNO_L_EDOTDOT
+ x = LIB9P_ERRNO_L_EDOTDOT;
+#endif
+#ifdef LIB9P_ERRNO_L_EDQUOT
+ x = LIB9P_ERRNO_L_EDQUOT;
+#endif
+#ifdef LIB9P_ERRNO_L_EEXIST
+ x = LIB9P_ERRNO_L_EEXIST;
+#endif
+#ifdef LIB9P_ERRNO_L_EFAULT
+ x = LIB9P_ERRNO_L_EFAULT;
+#endif
+#ifdef LIB9P_ERRNO_L_EFBIG
+ x = LIB9P_ERRNO_L_EFBIG;
+#endif
+#ifdef LIB9P_ERRNO_L_EHOSTDOWN
+ x = LIB9P_ERRNO_L_EHOSTDOWN;
+#endif
+#ifdef LIB9P_ERRNO_L_EHOSTUNREACH
+ x = LIB9P_ERRNO_L_EHOSTUNREACH;
+#endif
+#ifdef LIB9P_ERRNO_L_EHWPOISON
+ x = LIB9P_ERRNO_L_EHWPOISON;
+#endif
+#ifdef LIB9P_ERRNO_L_EIDRM
+ x = LIB9P_ERRNO_L_EIDRM;
+#endif
+#ifdef LIB9P_ERRNO_L_EILSEQ
+ x = LIB9P_ERRNO_L_EILSEQ;
+#endif
+#ifdef LIB9P_ERRNO_L_EINPROGRESS
+ x = LIB9P_ERRNO_L_EINPROGRESS;
+#endif
+#ifdef LIB9P_ERRNO_L_EINTR
+ x = LIB9P_ERRNO_L_EINTR;
+#endif
+#ifdef LIB9P_ERRNO_L_EINVAL
+ x = LIB9P_ERRNO_L_EINVAL;
+#endif
+#ifdef LIB9P_ERRNO_L_EIO
+ x = LIB9P_ERRNO_L_EIO;
+#endif
+#ifdef LIB9P_ERRNO_L_EISCONN
+ x = LIB9P_ERRNO_L_EISCONN;
+#endif
+#ifdef LIB9P_ERRNO_L_EISDIR
+ x = LIB9P_ERRNO_L_EISDIR;
+#endif
+#ifdef LIB9P_ERRNO_L_EISNAM
+ x = LIB9P_ERRNO_L_EISNAM;
+#endif
+#ifdef LIB9P_ERRNO_L_EKEYEXPIRED
+ x = LIB9P_ERRNO_L_EKEYEXPIRED;
+#endif
+#ifdef LIB9P_ERRNO_L_EKEYREJECTED
+ x = LIB9P_ERRNO_L_EKEYREJECTED;
+#endif
+#ifdef LIB9P_ERRNO_L_EKEYREVOKED
+ x = LIB9P_ERRNO_L_EKEYREVOKED;
+#endif
+#ifdef LIB9P_ERRNO_L_EL2HLT
+ x = LIB9P_ERRNO_L_EL2HLT;
+#endif
+#ifdef LIB9P_ERRNO_L_EL2NSYNC
+ x = LIB9P_ERRNO_L_EL2NSYNC;
+#endif
+#ifdef LIB9P_ERRNO_L_EL3HLT
+ x = LIB9P_ERRNO_L_EL3HLT;
+#endif
+#ifdef LIB9P_ERRNO_L_EL3RST
+ x = LIB9P_ERRNO_L_EL3RST;
+#endif
+#ifdef LIB9P_ERRNO_L_ELIBACC
+ x = LIB9P_ERRNO_L_ELIBACC;
+#endif
+#ifdef LIB9P_ERRNO_L_ELIBBAD
+ x = LIB9P_ERRNO_L_ELIBBAD;
+#endif
+#ifdef LIB9P_ERRNO_L_ELIBEXEC
+ x = LIB9P_ERRNO_L_ELIBEXEC;
+#endif
+#ifdef LIB9P_ERRNO_L_ELIBMAX
+ x = LIB9P_ERRNO_L_ELIBMAX;
+#endif
+#ifdef LIB9P_ERRNO_L_ELIBSCN
+ x = LIB9P_ERRNO_L_ELIBSCN;
+#endif
+#ifdef LIB9P_ERRNO_L_ELNRNG
+ x = LIB9P_ERRNO_L_ELNRNG;
+#endif
+#ifdef LIB9P_ERRNO_L_ELOOP
+ x = LIB9P_ERRNO_L_ELOOP;
+#endif
+#ifdef LIB9P_ERRNO_L_EMEDIUMTYPE
+ x = LIB9P_ERRNO_L_EMEDIUMTYPE;
+#endif
+#ifdef LIB9P_ERRNO_L_EMFILE
+ x = LIB9P_ERRNO_L_EMFILE;
+#endif
+#ifdef LIB9P_ERRNO_L_EMLINK
+ x = LIB9P_ERRNO_L_EMLINK;
+#endif
+#ifdef LIB9P_ERRNO_L_EMSGSIZE
+ x = LIB9P_ERRNO_L_EMSGSIZE;
+#endif
+#ifdef LIB9P_ERRNO_L_EMULTIHOP
+ x = LIB9P_ERRNO_L_EMULTIHOP;
+#endif
+#ifdef LIB9P_ERRNO_L_ENAMETOOLONG
+ x = LIB9P_ERRNO_L_ENAMETOOLONG;
+#endif
+#ifdef LIB9P_ERRNO_L_ENAVAIL
+ x = LIB9P_ERRNO_L_ENAVAIL;
+#endif
+#ifdef LIB9P_ERRNO_L_ENETDOWN
+ x = LIB9P_ERRNO_L_ENETDOWN;
+#endif
+#ifdef LIB9P_ERRNO_L_ENETRESET
+ x = LIB9P_ERRNO_L_ENETRESET;
+#endif
+#ifdef LIB9P_ERRNO_L_ENETUNREACH
+ x = LIB9P_ERRNO_L_ENETUNREACH;
+#endif
+#ifdef LIB9P_ERRNO_L_ENFILE
+ x = LIB9P_ERRNO_L_ENFILE;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOANO
+ x = LIB9P_ERRNO_L_ENOANO;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOBUFS
+ x = LIB9P_ERRNO_L_ENOBUFS;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOCSI
+ x = LIB9P_ERRNO_L_ENOCSI;
+#endif
+#ifdef LIB9P_ERRNO_L_ENODATA
+ x = LIB9P_ERRNO_L_ENODATA;
+#endif
+#ifdef LIB9P_ERRNO_L_ENODEV
+ x = LIB9P_ERRNO_L_ENODEV;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOENT
+ x = LIB9P_ERRNO_L_ENOENT;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOEXEC
+ x = LIB9P_ERRNO_L_ENOEXEC;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOKEY
+ x = LIB9P_ERRNO_L_ENOKEY;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOLCK
+ x = LIB9P_ERRNO_L_ENOLCK;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOLINK
+ x = LIB9P_ERRNO_L_ENOLINK;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOMEDIUM
+ x = LIB9P_ERRNO_L_ENOMEDIUM;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOMEM
+ x = LIB9P_ERRNO_L_ENOMEM;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOMSG
+ x = LIB9P_ERRNO_L_ENOMSG;
+#endif
+#ifdef LIB9P_ERRNO_L_ENONET
+ x = LIB9P_ERRNO_L_ENONET;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOPKG
+ x = LIB9P_ERRNO_L_ENOPKG;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOPROTOOPT
+ x = LIB9P_ERRNO_L_ENOPROTOOPT;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOSPC
+ x = LIB9P_ERRNO_L_ENOSPC;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOSR
+ x = LIB9P_ERRNO_L_ENOSR;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOSTR
+ x = LIB9P_ERRNO_L_ENOSTR;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOSYS
+ x = LIB9P_ERRNO_L_ENOSYS;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTBLK
+ x = LIB9P_ERRNO_L_ENOTBLK;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTCONN
+ x = LIB9P_ERRNO_L_ENOTCONN;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTDIR
+ x = LIB9P_ERRNO_L_ENOTDIR;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTEMPTY
+ x = LIB9P_ERRNO_L_ENOTEMPTY;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTNAM
+ x = LIB9P_ERRNO_L_ENOTNAM;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTRECOVERABLE
+ x = LIB9P_ERRNO_L_ENOTRECOVERABLE;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTSOCK
+ x = LIB9P_ERRNO_L_ENOTSOCK;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTTY
+ x = LIB9P_ERRNO_L_ENOTTY;
+#endif
+#ifdef LIB9P_ERRNO_L_ENOTUNIQ
+ x = LIB9P_ERRNO_L_ENOTUNIQ;
+#endif
+#ifdef LIB9P_ERRNO_L_ENXIO
+ x = LIB9P_ERRNO_L_ENXIO;
+#endif
+#ifdef LIB9P_ERRNO_L_EOPNOTSUPP
+ x = LIB9P_ERRNO_L_EOPNOTSUPP;
+#endif
+#ifdef LIB9P_ERRNO_L_EOVERFLOW
+ x = LIB9P_ERRNO_L_EOVERFLOW;
+#endif
+#ifdef LIB9P_ERRNO_L_EOWNERDEAD
+ x = LIB9P_ERRNO_L_EOWNERDEAD;
+#endif
+#ifdef LIB9P_ERRNO_L_EPERM
+ x = LIB9P_ERRNO_L_EPERM;
+#endif
+#ifdef LIB9P_ERRNO_L_EPFNOSUPPORT
+ x = LIB9P_ERRNO_L_EPFNOSUPPORT;
+#endif
+#ifdef LIB9P_ERRNO_L_EPIPE
+ x = LIB9P_ERRNO_L_EPIPE;
+#endif
+#ifdef LIB9P_ERRNO_L_EPROTO
+ x = LIB9P_ERRNO_L_EPROTO;
+#endif
+#ifdef LIB9P_ERRNO_L_EPROTONOSUPPORT
+ x = LIB9P_ERRNO_L_EPROTONOSUPPORT;
+#endif
+#ifdef LIB9P_ERRNO_L_EPROTOTYPE
+ x = LIB9P_ERRNO_L_EPROTOTYPE;
+#endif
+#ifdef LIB9P_ERRNO_L_ERANGE
+ x = LIB9P_ERRNO_L_ERANGE;
+#endif
+#ifdef LIB9P_ERRNO_L_EREMCHG
+ x = LIB9P_ERRNO_L_EREMCHG;
+#endif
+#ifdef LIB9P_ERRNO_L_EREMOTE
+ x = LIB9P_ERRNO_L_EREMOTE;
+#endif
+#ifdef LIB9P_ERRNO_L_EREMOTEIO
+ x = LIB9P_ERRNO_L_EREMOTEIO;
+#endif
+#ifdef LIB9P_ERRNO_L_ERESTART
+ x = LIB9P_ERRNO_L_ERESTART;
+#endif
+#ifdef LIB9P_ERRNO_L_ERFKILL
+ x = LIB9P_ERRNO_L_ERFKILL;
+#endif
+#ifdef LIB9P_ERRNO_L_EROFS
+ x = LIB9P_ERRNO_L_EROFS;
+#endif
+#ifdef LIB9P_ERRNO_L_ESHUTDOWN
+ x = LIB9P_ERRNO_L_ESHUTDOWN;
+#endif
+#ifdef LIB9P_ERRNO_L_ESOCKTNOSUPPORT
+ x = LIB9P_ERRNO_L_ESOCKTNOSUPPORT;
+#endif
+#ifdef LIB9P_ERRNO_L_ESPIPE
+ x = LIB9P_ERRNO_L_ESPIPE;
+#endif
+#ifdef LIB9P_ERRNO_L_ESRCH
+ x = LIB9P_ERRNO_L_ESRCH;
+#endif
+#ifdef LIB9P_ERRNO_L_ESRMNT
+ x = LIB9P_ERRNO_L_ESRMNT;
+#endif
+#ifdef LIB9P_ERRNO_L_ESTALE
+ x = LIB9P_ERRNO_L_ESTALE;
+#endif
+#ifdef LIB9P_ERRNO_L_ESTRPIPE
+ x = LIB9P_ERRNO_L_ESTRPIPE;
+#endif
+#ifdef LIB9P_ERRNO_L_ETIME
+ x = LIB9P_ERRNO_L_ETIME;
+#endif
+#ifdef LIB9P_ERRNO_L_ETIMEDOUT
+ x = LIB9P_ERRNO_L_ETIMEDOUT;
+#endif
+#ifdef LIB9P_ERRNO_L_ETOOMANYREFS
+ x = LIB9P_ERRNO_L_ETOOMANYREFS;
+#endif
+#ifdef LIB9P_ERRNO_L_ETXTBSY
+ x = LIB9P_ERRNO_L_ETXTBSY;
+#endif
+#ifdef LIB9P_ERRNO_L_EUCLEAN
+ x = LIB9P_ERRNO_L_EUCLEAN;
+#endif
+#ifdef LIB9P_ERRNO_L_EUNATCH
+ x = LIB9P_ERRNO_L_EUNATCH;
+#endif
+#ifdef LIB9P_ERRNO_L_EUSERS
+ x = LIB9P_ERRNO_L_EUSERS;
+#endif
+#ifdef LIB9P_ERRNO_L_EXDEV
+ x = LIB9P_ERRNO_L_EXDEV;
+#endif
+#ifdef LIB9P_ERRNO_L_EXFULL
+ x = LIB9P_ERRNO_L_EXFULL;
+#endif
+#ifdef LIB9P_ERRNO_NOERROR
+ x = LIB9P_ERRNO_NOERROR;
+#endif
+#ifdef LIB9P_FID_NOFID
+ x = LIB9P_FID_NOFID;
+#endif
+#ifdef LIB9P_GETATTR_ALL
+ x = LIB9P_GETATTR_ALL;
+#endif
+#ifdef LIB9P_GETATTR_ATIME
+ x = LIB9P_GETATTR_ATIME;
+#endif
+#ifdef LIB9P_GETATTR_BASIC
+ x = LIB9P_GETATTR_BASIC;
+#endif
+#ifdef LIB9P_GETATTR_BLOCKS
+ x = LIB9P_GETATTR_BLOCKS;
+#endif
+#ifdef LIB9P_GETATTR_BTIME
+ x = LIB9P_GETATTR_BTIME;
+#endif
+#ifdef LIB9P_GETATTR_CTIME
+ x = LIB9P_GETATTR_CTIME;
+#endif
+#ifdef LIB9P_GETATTR_DATA_VERSION
+ x = LIB9P_GETATTR_DATA_VERSION;
+#endif
+#ifdef LIB9P_GETATTR_GEN
+ x = LIB9P_GETATTR_GEN;
+#endif
+#ifdef LIB9P_GETATTR_GID
+ x = LIB9P_GETATTR_GID;
+#endif
+#ifdef LIB9P_GETATTR_INO
+ x = LIB9P_GETATTR_INO;
+#endif
+#ifdef LIB9P_GETATTR_MODE
+ x = LIB9P_GETATTR_MODE;
+#endif
+#ifdef LIB9P_GETATTR_MTIME
+ x = LIB9P_GETATTR_MTIME;
+#endif
+#ifdef LIB9P_GETATTR_NLINK
+ x = LIB9P_GETATTR_NLINK;
+#endif
+#ifdef LIB9P_GETATTR_RDEV
+ x = LIB9P_GETATTR_RDEV;
+#endif
+#ifdef LIB9P_GETATTR_SIZE
+ x = LIB9P_GETATTR_SIZE;
+#endif
+#ifdef LIB9P_GETATTR_UID
+ x = LIB9P_GETATTR_UID;
+#endif
+#ifdef LIB9P_LOCK_FLAGS_BLOCK
+ x = LIB9P_LOCK_FLAGS_BLOCK;
+#endif
+#ifdef LIB9P_LOCK_FLAGS_RECLAIM
+ x = LIB9P_LOCK_FLAGS_RECLAIM;
+#endif
+#ifdef LIB9P_LOCK_STATUS_BLOCKED
+ x = LIB9P_LOCK_STATUS_BLOCKED;
+#endif
+#ifdef LIB9P_LOCK_STATUS_ERROR
+ x = LIB9P_LOCK_STATUS_ERROR;
+#endif
+#ifdef LIB9P_LOCK_STATUS_GRACE
+ x = LIB9P_LOCK_STATUS_GRACE;
+#endif
+#ifdef LIB9P_LOCK_STATUS_SUCCESS
+ x = LIB9P_LOCK_STATUS_SUCCESS;
+#endif
+#ifdef LIB9P_LOCK_TYPE_RDLCK
+ x = LIB9P_LOCK_TYPE_RDLCK;
+#endif
+#ifdef LIB9P_LOCK_TYPE_UNLCK
+ x = LIB9P_LOCK_TYPE_UNLCK;
+#endif
+#ifdef LIB9P_LOCK_TYPE_WRLCK
+ x = LIB9P_LOCK_TYPE_WRLCK;
+#endif
+#ifdef LIB9P_LO_APPEND
+ x = LIB9P_LO_APPEND;
+#endif
+#ifdef LIB9P_LO_BSD_FASYNC
+ x = LIB9P_LO_BSD_FASYNC;
+#endif
+#ifdef LIB9P_LO_CLOEXEC
+ x = LIB9P_LO_CLOEXEC;
+#endif
+#ifdef LIB9P_LO_CREATE
+ x = LIB9P_LO_CREATE;
+#endif
+#ifdef LIB9P_LO_DIRECT
+ x = LIB9P_LO_DIRECT;
+#endif
+#ifdef LIB9P_LO_DIRECTORY
+ x = LIB9P_LO_DIRECTORY;
+#endif
+#ifdef LIB9P_LO_DSYNC
+ x = LIB9P_LO_DSYNC;
+#endif
+#ifdef LIB9P_LO_EXCL
+ x = LIB9P_LO_EXCL;
+#endif
+#ifdef LIB9P_LO_FLAG_MASK
+ x = LIB9P_LO_FLAG_MASK;
+#endif
+#ifdef LIB9P_LO_LARGEFILE
+ x = LIB9P_LO_LARGEFILE;
+#endif
+#ifdef LIB9P_LO_MODE_MASK
+ x = LIB9P_LO_MODE_MASK;
+#endif
+#ifdef LIB9P_LO_MODE_NOACCESS
+ x = LIB9P_LO_MODE_NOACCESS;
+#endif
+#ifdef LIB9P_LO_MODE_RDONLY
+ x = LIB9P_LO_MODE_RDONLY;
+#endif
+#ifdef LIB9P_LO_MODE_RDWR
+ x = LIB9P_LO_MODE_RDWR;
+#endif
+#ifdef LIB9P_LO_MODE_WRONLY
+ x = LIB9P_LO_MODE_WRONLY;
+#endif
+#ifdef LIB9P_LO_NOATIME
+ x = LIB9P_LO_NOATIME;
+#endif
+#ifdef LIB9P_LO_NOCTTY
+ x = LIB9P_LO_NOCTTY;
+#endif
+#ifdef LIB9P_LO_NOFOLLOW
+ x = LIB9P_LO_NOFOLLOW;
+#endif
+#ifdef LIB9P_LO_NONBLOCK
+ x = LIB9P_LO_NONBLOCK;
+#endif
+#ifdef LIB9P_LO_SYNC
+ x = LIB9P_LO_SYNC;
+#endif
+#ifdef LIB9P_LO_TRUNC
+ x = LIB9P_LO_TRUNC;
+#endif
+#ifdef LIB9P_MODE_FMT_BLOCK_DEV
+ x = LIB9P_MODE_FMT_BLOCK_DEV;
+#endif
+#ifdef LIB9P_MODE_FMT_CHAR_DEV
+ x = LIB9P_MODE_FMT_CHAR_DEV;
+#endif
+#ifdef LIB9P_MODE_FMT_DIRECTORY
+ x = LIB9P_MODE_FMT_DIRECTORY;
+#endif
+#ifdef LIB9P_MODE_FMT_MASK
+ x = LIB9P_MODE_FMT_MASK;
+#endif
+#ifdef LIB9P_MODE_FMT_PIPE
+ x = LIB9P_MODE_FMT_PIPE;
+#endif
+#ifdef LIB9P_MODE_FMT_REGULAR
+ x = LIB9P_MODE_FMT_REGULAR;
+#endif
+#ifdef LIB9P_MODE_FMT_SOCKET
+ x = LIB9P_MODE_FMT_SOCKET;
+#endif
+#ifdef LIB9P_MODE_FMT_SYMLINK
+ x = LIB9P_MODE_FMT_SYMLINK;
+#endif
+#ifdef LIB9P_MODE_PERM_GROUP_R
+ x = LIB9P_MODE_PERM_GROUP_R;
+#endif
+#ifdef LIB9P_MODE_PERM_GROUP_W
+ x = LIB9P_MODE_PERM_GROUP_W;
+#endif
+#ifdef LIB9P_MODE_PERM_GROUP_X
+ x = LIB9P_MODE_PERM_GROUP_X;
+#endif
+#ifdef LIB9P_MODE_PERM_MASK
+ x = LIB9P_MODE_PERM_MASK;
+#endif
+#ifdef LIB9P_MODE_PERM_OTHER_R
+ x = LIB9P_MODE_PERM_OTHER_R;
+#endif
+#ifdef LIB9P_MODE_PERM_OTHER_W
+ x = LIB9P_MODE_PERM_OTHER_W;
+#endif
+#ifdef LIB9P_MODE_PERM_OTHER_X
+ x = LIB9P_MODE_PERM_OTHER_X;
+#endif
+#ifdef LIB9P_MODE_PERM_OWNER_R
+ x = LIB9P_MODE_PERM_OWNER_R;
+#endif
+#ifdef LIB9P_MODE_PERM_OWNER_W
+ x = LIB9P_MODE_PERM_OWNER_W;
+#endif
+#ifdef LIB9P_MODE_PERM_OWNER_X
+ x = LIB9P_MODE_PERM_OWNER_X;
+#endif
+#ifdef LIB9P_MODE_PERM_SETGROUP
+ x = LIB9P_MODE_PERM_SETGROUP;
+#endif
+#ifdef LIB9P_MODE_PERM_SETUSER
+ x = LIB9P_MODE_PERM_SETUSER;
+#endif
+#ifdef LIB9P_MODE_PERM_STICKY
+ x = LIB9P_MODE_PERM_STICKY;
+#endif
+#ifdef LIB9P_NUID_NONUID
+ x = LIB9P_NUID_NONUID;
+#endif
+#ifdef LIB9P_O_FLAG_MASK
+ x = LIB9P_O_FLAG_MASK;
+#endif
+#ifdef LIB9P_O_MODE_EXEC
+ x = LIB9P_O_MODE_EXEC;
+#endif
+#ifdef LIB9P_O_MODE_MASK
+ x = LIB9P_O_MODE_MASK;
+#endif
+#ifdef LIB9P_O_MODE_RDWR
+ x = LIB9P_O_MODE_RDWR;
+#endif
+#ifdef LIB9P_O_MODE_READ
+ x = LIB9P_O_MODE_READ;
+#endif
+#ifdef LIB9P_O_MODE_WRITE
+ x = LIB9P_O_MODE_WRITE;
+#endif
+#ifdef LIB9P_O_RCLOSE
+ x = LIB9P_O_RCLOSE;
+#endif
+#ifdef LIB9P_O_TRUNC
+ x = LIB9P_O_TRUNC;
+#endif
+#ifdef LIB9P_QT_APPEND
+ x = LIB9P_QT_APPEND;
+#endif
+#ifdef LIB9P_QT_AUTH
+ x = LIB9P_QT_AUTH;
+#endif
+#ifdef LIB9P_QT_DIR
+ x = LIB9P_QT_DIR;
+#endif
+#ifdef LIB9P_QT_EXCL
+ x = LIB9P_QT_EXCL;
+#endif
+#ifdef LIB9P_QT_FILE
+ x = LIB9P_QT_FILE;
+#endif
+#ifdef LIB9P_QT_SYMLINK
+ x = LIB9P_QT_SYMLINK;
+#endif
+#ifdef LIB9P_QT_TMP
+ x = LIB9P_QT_TMP;
+#endif
+#ifdef LIB9P_RMSG_MAX_COPY
+ x = LIB9P_RMSG_MAX_COPY;
+#endif
+#ifdef LIB9P_RMSG_MAX_IOV
+ x = LIB9P_RMSG_MAX_IOV;
+#endif
+#ifdef LIB9P_SETATTR_ATIME
+ x = LIB9P_SETATTR_ATIME;
+#endif
+#ifdef LIB9P_SETATTR_ATIME_SET
+ x = LIB9P_SETATTR_ATIME_SET;
+#endif
+#ifdef LIB9P_SETATTR_CTIME
+ x = LIB9P_SETATTR_CTIME;
+#endif
+#ifdef LIB9P_SETATTR_GID
+ x = LIB9P_SETATTR_GID;
+#endif
+#ifdef LIB9P_SETATTR_MODE
+ x = LIB9P_SETATTR_MODE;
+#endif
+#ifdef LIB9P_SETATTR_MTIME
+ x = LIB9P_SETATTR_MTIME;
+#endif
+#ifdef LIB9P_SETATTR_MTIME_SET
+ x = LIB9P_SETATTR_MTIME_SET;
+#endif
+#ifdef LIB9P_SETATTR_SIZE
+ x = LIB9P_SETATTR_SIZE;
+#endif
+#ifdef LIB9P_SETATTR_UID
+ x = LIB9P_SETATTR_UID;
+#endif
+#ifdef LIB9P_SUPER_MAGIC_V9FS_MAGIC
+ x = LIB9P_SUPER_MAGIC_V9FS_MAGIC;
+#endif
+#ifdef LIB9P_TAG_NOTAG
+ x = LIB9P_TAG_NOTAG;
+#endif
+#ifdef LIB9P_TMSG_MAX_COPY
+ x = LIB9P_TMSG_MAX_COPY;
+#endif
+#ifdef LIB9P_TMSG_MAX_IOV
+ x = LIB9P_TMSG_MAX_IOV;
+#endif
+#ifdef _LIB9P_DM_PLAN9_MOUNT
+ x = _LIB9P_DM_PLAN9_MOUNT;
+#endif
+#ifdef _LIB9P_DM_UNUSED_10
+ x = _LIB9P_DM_UNUSED_10;
+#endif
+#ifdef _LIB9P_DM_UNUSED_11
+ x = _LIB9P_DM_UNUSED_11;
+#endif
+#ifdef _LIB9P_DM_UNUSED_12
+ x = _LIB9P_DM_UNUSED_12;
+#endif
+#ifdef _LIB9P_DM_UNUSED_13
+ x = _LIB9P_DM_UNUSED_13;
+#endif
+#ifdef _LIB9P_DM_UNUSED_14
+ x = _LIB9P_DM_UNUSED_14;
+#endif
+#ifdef _LIB9P_DM_UNUSED_15
+ x = _LIB9P_DM_UNUSED_15;
+#endif
+#ifdef _LIB9P_DM_UNUSED_16
+ x = _LIB9P_DM_UNUSED_16;
+#endif
+#ifdef _LIB9P_DM_UNUSED_17
+ x = _LIB9P_DM_UNUSED_17;
+#endif
+#ifdef _LIB9P_DM_UNUSED_22
+ x = _LIB9P_DM_UNUSED_22;
+#endif
+#ifdef _LIB9P_DM_UNUSED_24
+ x = _LIB9P_DM_UNUSED_24;
+#endif
+#ifdef _LIB9P_DM_UNUSED_25
+ x = _LIB9P_DM_UNUSED_25;
+#endif
+#ifdef _LIB9P_DM_UNUSED_9
+ x = _LIB9P_DM_UNUSED_9;
+#endif
+#ifdef _LIB9P_DT_WHITEOUT
+ x = _LIB9P_DT_WHITEOUT;
+#endif
+#ifdef _LIB9P_ENABLE_stat
+ x = _LIB9P_ENABLE_stat;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_14
+ x = _LIB9P_GETATTR_UNUSED_14;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_15
+ x = _LIB9P_GETATTR_UNUSED_15;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_16
+ x = _LIB9P_GETATTR_UNUSED_16;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_17
+ x = _LIB9P_GETATTR_UNUSED_17;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_18
+ x = _LIB9P_GETATTR_UNUSED_18;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_19
+ x = _LIB9P_GETATTR_UNUSED_19;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_20
+ x = _LIB9P_GETATTR_UNUSED_20;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_21
+ x = _LIB9P_GETATTR_UNUSED_21;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_22
+ x = _LIB9P_GETATTR_UNUSED_22;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_23
+ x = _LIB9P_GETATTR_UNUSED_23;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_24
+ x = _LIB9P_GETATTR_UNUSED_24;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_25
+ x = _LIB9P_GETATTR_UNUSED_25;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_26
+ x = _LIB9P_GETATTR_UNUSED_26;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_27
+ x = _LIB9P_GETATTR_UNUSED_27;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_28
+ x = _LIB9P_GETATTR_UNUSED_28;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_29
+ x = _LIB9P_GETATTR_UNUSED_29;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_30
+ x = _LIB9P_GETATTR_UNUSED_30;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_31
+ x = _LIB9P_GETATTR_UNUSED_31;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_32
+ x = _LIB9P_GETATTR_UNUSED_32;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_33
+ x = _LIB9P_GETATTR_UNUSED_33;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_34
+ x = _LIB9P_GETATTR_UNUSED_34;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_35
+ x = _LIB9P_GETATTR_UNUSED_35;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_36
+ x = _LIB9P_GETATTR_UNUSED_36;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_37
+ x = _LIB9P_GETATTR_UNUSED_37;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_38
+ x = _LIB9P_GETATTR_UNUSED_38;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_39
+ x = _LIB9P_GETATTR_UNUSED_39;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_40
+ x = _LIB9P_GETATTR_UNUSED_40;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_41
+ x = _LIB9P_GETATTR_UNUSED_41;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_42
+ x = _LIB9P_GETATTR_UNUSED_42;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_43
+ x = _LIB9P_GETATTR_UNUSED_43;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_44
+ x = _LIB9P_GETATTR_UNUSED_44;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_45
+ x = _LIB9P_GETATTR_UNUSED_45;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_46
+ x = _LIB9P_GETATTR_UNUSED_46;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_47
+ x = _LIB9P_GETATTR_UNUSED_47;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_48
+ x = _LIB9P_GETATTR_UNUSED_48;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_49
+ x = _LIB9P_GETATTR_UNUSED_49;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_50
+ x = _LIB9P_GETATTR_UNUSED_50;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_51
+ x = _LIB9P_GETATTR_UNUSED_51;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_52
+ x = _LIB9P_GETATTR_UNUSED_52;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_53
+ x = _LIB9P_GETATTR_UNUSED_53;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_54
+ x = _LIB9P_GETATTR_UNUSED_54;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_55
+ x = _LIB9P_GETATTR_UNUSED_55;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_56
+ x = _LIB9P_GETATTR_UNUSED_56;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_57
+ x = _LIB9P_GETATTR_UNUSED_57;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_58
+ x = _LIB9P_GETATTR_UNUSED_58;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_59
+ x = _LIB9P_GETATTR_UNUSED_59;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_60
+ x = _LIB9P_GETATTR_UNUSED_60;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_61
+ x = _LIB9P_GETATTR_UNUSED_61;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_62
+ x = _LIB9P_GETATTR_UNUSED_62;
+#endif
+#ifdef _LIB9P_GETATTR_UNUSED_63
+ x = _LIB9P_GETATTR_UNUSED_63;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_10
+ x = _LIB9P_LOCK_FLAGS_UNUSED_10;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_11
+ x = _LIB9P_LOCK_FLAGS_UNUSED_11;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_12
+ x = _LIB9P_LOCK_FLAGS_UNUSED_12;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_13
+ x = _LIB9P_LOCK_FLAGS_UNUSED_13;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_14
+ x = _LIB9P_LOCK_FLAGS_UNUSED_14;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_15
+ x = _LIB9P_LOCK_FLAGS_UNUSED_15;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_16
+ x = _LIB9P_LOCK_FLAGS_UNUSED_16;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_17
+ x = _LIB9P_LOCK_FLAGS_UNUSED_17;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_18
+ x = _LIB9P_LOCK_FLAGS_UNUSED_18;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_19
+ x = _LIB9P_LOCK_FLAGS_UNUSED_19;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_2
+ x = _LIB9P_LOCK_FLAGS_UNUSED_2;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_20
+ x = _LIB9P_LOCK_FLAGS_UNUSED_20;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_21
+ x = _LIB9P_LOCK_FLAGS_UNUSED_21;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_22
+ x = _LIB9P_LOCK_FLAGS_UNUSED_22;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_23
+ x = _LIB9P_LOCK_FLAGS_UNUSED_23;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_24
+ x = _LIB9P_LOCK_FLAGS_UNUSED_24;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_25
+ x = _LIB9P_LOCK_FLAGS_UNUSED_25;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_26
+ x = _LIB9P_LOCK_FLAGS_UNUSED_26;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_27
+ x = _LIB9P_LOCK_FLAGS_UNUSED_27;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_28
+ x = _LIB9P_LOCK_FLAGS_UNUSED_28;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_29
+ x = _LIB9P_LOCK_FLAGS_UNUSED_29;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_3
+ x = _LIB9P_LOCK_FLAGS_UNUSED_3;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_30
+ x = _LIB9P_LOCK_FLAGS_UNUSED_30;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_31
+ x = _LIB9P_LOCK_FLAGS_UNUSED_31;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_4
+ x = _LIB9P_LOCK_FLAGS_UNUSED_4;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_5
+ x = _LIB9P_LOCK_FLAGS_UNUSED_5;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_6
+ x = _LIB9P_LOCK_FLAGS_UNUSED_6;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_7
+ x = _LIB9P_LOCK_FLAGS_UNUSED_7;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_8
+ x = _LIB9P_LOCK_FLAGS_UNUSED_8;
+#endif
+#ifdef _LIB9P_LOCK_FLAGS_UNUSED_9
+ x = _LIB9P_LOCK_FLAGS_UNUSED_9;
+#endif
+#ifdef _LIB9P_LO_UNUSED_2
+ x = _LIB9P_LO_UNUSED_2;
+#endif
+#ifdef _LIB9P_LO_UNUSED_21
+ x = _LIB9P_LO_UNUSED_21;
+#endif
+#ifdef _LIB9P_LO_UNUSED_22
+ x = _LIB9P_LO_UNUSED_22;
+#endif
+#ifdef _LIB9P_LO_UNUSED_23
+ x = _LIB9P_LO_UNUSED_23;
+#endif
+#ifdef _LIB9P_LO_UNUSED_24
+ x = _LIB9P_LO_UNUSED_24;
+#endif
+#ifdef _LIB9P_LO_UNUSED_25
+ x = _LIB9P_LO_UNUSED_25;
+#endif
+#ifdef _LIB9P_LO_UNUSED_26
+ x = _LIB9P_LO_UNUSED_26;
+#endif
+#ifdef _LIB9P_LO_UNUSED_27
+ x = _LIB9P_LO_UNUSED_27;
+#endif
+#ifdef _LIB9P_LO_UNUSED_28
+ x = _LIB9P_LO_UNUSED_28;
+#endif
+#ifdef _LIB9P_LO_UNUSED_29
+ x = _LIB9P_LO_UNUSED_29;
+#endif
+#ifdef _LIB9P_LO_UNUSED_3
+ x = _LIB9P_LO_UNUSED_3;
+#endif
+#ifdef _LIB9P_LO_UNUSED_30
+ x = _LIB9P_LO_UNUSED_30;
+#endif
+#ifdef _LIB9P_LO_UNUSED_31
+ x = _LIB9P_LO_UNUSED_31;
+#endif
+#ifdef _LIB9P_LO_UNUSED_4
+ x = _LIB9P_LO_UNUSED_4;
+#endif
+#ifdef _LIB9P_LO_UNUSED_5
+ x = _LIB9P_LO_UNUSED_5;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_16
+ x = _LIB9P_MODE_UNUSED_16;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_17
+ x = _LIB9P_MODE_UNUSED_17;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_18
+ x = _LIB9P_MODE_UNUSED_18;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_19
+ x = _LIB9P_MODE_UNUSED_19;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_20
+ x = _LIB9P_MODE_UNUSED_20;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_21
+ x = _LIB9P_MODE_UNUSED_21;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_22
+ x = _LIB9P_MODE_UNUSED_22;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_23
+ x = _LIB9P_MODE_UNUSED_23;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_24
+ x = _LIB9P_MODE_UNUSED_24;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_25
+ x = _LIB9P_MODE_UNUSED_25;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_26
+ x = _LIB9P_MODE_UNUSED_26;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_27
+ x = _LIB9P_MODE_UNUSED_27;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_28
+ x = _LIB9P_MODE_UNUSED_28;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_29
+ x = _LIB9P_MODE_UNUSED_29;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_30
+ x = _LIB9P_MODE_UNUSED_30;
+#endif
+#ifdef _LIB9P_MODE_UNUSED_31
+ x = _LIB9P_MODE_UNUSED_31;
+#endif
+#ifdef _LIB9P_O_RESERVED_CEXEC
+ x = _LIB9P_O_RESERVED_CEXEC;
+#endif
+#ifdef _LIB9P_O_UNUSED_2
+ x = _LIB9P_O_UNUSED_2;
+#endif
+#ifdef _LIB9P_O_UNUSED_3
+ x = _LIB9P_O_UNUSED_3;
+#endif
+#ifdef _LIB9P_O_UNUSED_7
+ x = _LIB9P_O_UNUSED_7;
+#endif
+#ifdef _LIB9P_QT_PLAN9_MOUNT
+ x = _LIB9P_QT_PLAN9_MOUNT;
+#endif
+#ifdef _LIB9P_QT_UNUSED_0
+ x = _LIB9P_QT_UNUSED_0;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_10
+ x = _LIB9P_SETATTR_UNUSED_10;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_11
+ x = _LIB9P_SETATTR_UNUSED_11;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_12
+ x = _LIB9P_SETATTR_UNUSED_12;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_13
+ x = _LIB9P_SETATTR_UNUSED_13;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_14
+ x = _LIB9P_SETATTR_UNUSED_14;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_15
+ x = _LIB9P_SETATTR_UNUSED_15;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_16
+ x = _LIB9P_SETATTR_UNUSED_16;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_17
+ x = _LIB9P_SETATTR_UNUSED_17;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_18
+ x = _LIB9P_SETATTR_UNUSED_18;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_19
+ x = _LIB9P_SETATTR_UNUSED_19;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_20
+ x = _LIB9P_SETATTR_UNUSED_20;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_21
+ x = _LIB9P_SETATTR_UNUSED_21;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_22
+ x = _LIB9P_SETATTR_UNUSED_22;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_23
+ x = _LIB9P_SETATTR_UNUSED_23;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_24
+ x = _LIB9P_SETATTR_UNUSED_24;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_25
+ x = _LIB9P_SETATTR_UNUSED_25;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_26
+ x = _LIB9P_SETATTR_UNUSED_26;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_27
+ x = _LIB9P_SETATTR_UNUSED_27;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_28
+ x = _LIB9P_SETATTR_UNUSED_28;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_29
+ x = _LIB9P_SETATTR_UNUSED_29;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_30
+ x = _LIB9P_SETATTR_UNUSED_30;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_31
+ x = _LIB9P_SETATTR_UNUSED_31;
+#endif
+#ifdef _LIB9P_SETATTR_UNUSED_9
+ x = _LIB9P_SETATTR_UNUSED_9;
+#endif
+ return 0;
+}
diff --git a/lib9p/tests/test_compile.c.gen b/lib9p/tests/test_compile.c.gen
new file mode 100755
index 0000000..eb89c54
--- /dev/null
+++ b/lib9p/tests/test_compile.c.gen
@@ -0,0 +1,19 @@
+#!/bin/sh
+# lib9p/tests/test_compile.c.gen - Generate code to make sure all generated macros work
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+generated_h=$1
+outfile=$2
+
+{
+ echo "/* ${outfile} - Generated by $0. DO NOT EDIT! */"
+ echo
+ echo "#include <lib9p/core.h>"
+ echo 'int main(void) {'
+ echo ' [[gnu::unused]] uint64_t x;'
+ <"$generated_h" sed -nE 's/^\s*#\s*define\s*(\S[^ (]*)\s.*/\1/p' | LC_COLLATE=C sort -u | sed 's/.*/#ifdef &\n x = &;\n#endif/'
+ echo ' return 0;'
+ echo '}'
+} >"$outfile"
diff --git a/lib9p/tests/test_compile_config/config.h b/lib9p/tests/test_compile_config/config.h
new file mode 100644
index 0000000..02cb8e5
--- /dev/null
+++ b/lib9p/tests/test_compile_config/config.h
@@ -0,0 +1,31 @@
+/* config.h - Compile-time configuration for lib9p/test/test_compile
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _CONFIG_H_
+#define _CONFIG_H_
+
+/* 9P *************************************************************************/
+
+#define CONFIG_9P_MAX_ERR_SIZE 128
+#define CONFIG_9P_MAX_9P2000_e_WELEM 16
+
+/* 9P_SRV *********************************************************************/
+
+#define CONFIG_9P_SRV_MAX_MSG_SIZE ((4*1024)+24)
+#define CONFIG_9P_SRV_MAX_HOSTMSG_SIZE CONFIG_9P_SRV_MAX_MSG_SIZE+16
+
+/* COROUTINE ******************************************************************/
+
+#define CONFIG_COROUTINE_STACK_SIZE_DEFAULT (32*1024)
+#define CONFIG_COROUTINE_NAME_LEN 16
+#define CONFIG_COROUTINE_MEASURE_STACK 1 /* bool */
+#define CONFIG_COROUTINE_PROTECT_STACK 1 /* bool */
+#define CONFIG_COROUTINE_DEBUG 0 /* bool */
+#define CONFIG_COROUTINE_VALGRIND 1 /* bool */
+#define CONFIG_COROUTINE_GDB 1 /* bool */
+#define CONFIG_COROUTINE_NUM 8
+
+#endif /* _CONFIG_H_ */
diff --git a/lib9p/tests/test_server/CMakeLists.txt b/lib9p/tests/test_server/CMakeLists.txt
new file mode 100644
index 0000000..c61d344
--- /dev/null
+++ b/lib9p/tests/test_server/CMakeLists.txt
@@ -0,0 +1,45 @@
+# lib9p/tests/test_server/CMakeLists.txt - Build script for test_server executable
+#
+# Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+if (PICO_PLATFORM STREQUAL "host")
+
+# Compile ######################################################################
+
+add_library(test_server_objs OBJECT
+ main.c
+ fs_flush.c
+ fs_shutdown.c
+ fs_whoami.c
+)
+target_include_directories(test_server_objs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/config)
+target_include_directories(test_server_objs PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(test_server_objs
+ libcr
+ libcr_ipc
+ libmisc
+ lib9p_util
+ libhw_cr
+)
+
+# Analyze the stack ############################################################
+
+add_stack_analysis(test_server_stack.c test_server_objs)
+
+# Link #########################################################################
+
+add_executable(test_server)
+target_sources(test_server PRIVATE
+ test_server_stack.c
+ "$<TARGET_OBJECTS:test_server_objs>"
+)
+
+# Embed ########################################################################
+
+target_embed_sources(test_server_objs test_server static.h
+ static/README.md
+ static/Documentation/x.txt
+)
+
+endif()
diff --git a/lib9p/tests/test_server/config/config.h b/lib9p/tests/test_server/config/config.h
new file mode 100644
index 0000000..f49894b
--- /dev/null
+++ b/lib9p/tests/test_server/config/config.h
@@ -0,0 +1,67 @@
+/* config.h - Compile-time configuration for lib9p/test/test_server
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _CONFIG_H_
+#define _CONFIG_H_
+
+#define _CONFIG_9P_MAX_CONNS 8
+#define _CONFIG_9P_MAX_REQS (2*_CONFIG_9P_MAX_CONNS)
+
+/* 9P *************************************************************************/
+
+#define CONFIG_9P_MAX_ERR_SIZE 128 /* 128 is what Plan 9 4e uses */
+
+#define CONFIG_9P_ENABLE_9P2000 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_u 1 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_e 0 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_L 0 /* bool */
+#define CONFIG_9P_ENABLE_9P2000_p9p 0 /* bool */
+
+/* 9P_SRV *********************************************************************/
+
+#define CONFIG_9P_SRV_DEBUG 1 /* bool */
+
+/**
+ * This max-msg-size is sized so that a Twrite message can return
+ * 8KiB of data.
+ *
+ * This is the same as the default in Plan 9 4e's lib9p; it has the
+ * comment that "24" is "ample room for Twrite/Rread header
+ * (iounit)". In fact, the Twrite header is only 23 bytes
+ * ("size[4] Twrite[1] tag[2] fid[4] offset[8] count[4]") and the
+ * Rread header is even shorter at 11 bytes ("size[4] Rread[1]
+ * tag[2] count[4]"), so "24" appears to be the size of the Twrite
+ * header rounded up to a nice round number.
+ *
+ * In older versions of 9P ("9P1"), the max message size was
+ * defined as part of the protocol specification rather than
+ * negotiated. In Plan 9 1e it was (8*1024)+128, and was bumped to
+ * (8*1024)+160 in 2e and 3e.
+ */
+#define CONFIG_9P_SRV_MAX_MSG_SIZE ((4*1024)+24)
+/**
+ * Maximum host-data-structure size. A message may be larger in
+ * unmarshaled-host-structures than marshaled-net-bytes due to (1)
+ * struct padding, (2) array pointers.
+ */
+#define CONFIG_9P_SRV_MAX_HOSTMSG_SIZE CONFIG_9P_SRV_MAX_MSG_SIZE+16
+
+/* COROUTINE ******************************************************************/
+
+#define CONFIG_COROUTINE_STACK_SIZE_DEFAULT (32*1024)
+#define CONFIG_COROUTINE_NAME_LEN 16
+#define CONFIG_COROUTINE_MEASURE_STACK 1 /* bool */
+#define CONFIG_COROUTINE_PROTECT_STACK 1 /* bool */
+#define CONFIG_COROUTINE_DEBUG 0 /* bool */
+#define CONFIG_COROUTINE_VALGRIND 1 /* bool */
+#define CONFIG_COROUTINE_GDB 1 /* bool */
+#define CONFIG_COROUTINE_NUM ( \
+ 1 /* usb_common */ + \
+ 1 /* usb_keyboard */ + \
+ _CONFIG_9P_MAX_CONNS /* accept+read */ + \
+ _CONFIG_9P_MAX_REQS /* work+write */ )
+
+#endif /* _CONFIG_H_ */
diff --git a/lib9p/tests/test_server/fs_flush.c b/lib9p/tests/test_server/fs_flush.c
new file mode 100644
index 0000000..e6408d7
--- /dev/null
+++ b/lib9p/tests/test_server/fs_flush.c
@@ -0,0 +1,131 @@
+/* lib9p/tests/test_server/fs_flush.c - flush-* API endpoints
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/alloc.h>
+
+#define IMPLEMENTATION_FOR_LIB9P_SRV_H YES /* for ctx->flush_ch */
+#include "fs_flush.h"
+
+LO_IMPLEMENTATION_C(lib9p_srv_file, struct flush_file, flush_file, static);
+
+struct flush_fio {
+ struct flush_file *parent;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_fio, struct flush_fio, flush_fio);
+LO_IMPLEMENTATION_C(lib9p_srv_fio, struct flush_fio, flush_fio, static);
+
+/* srv_file *******************************************************************/
+
+static void flush_file_free(struct flush_file *self) {
+ assert(self);
+}
+static struct lib9p_qid flush_file_qid(struct flush_file *self) {
+ assert(self);
+ return (struct lib9p_qid){
+ .type = LIB9P_QT_FILE,
+ .vers = 1,
+ .path = self->pathnum,
+ };
+}
+
+static struct lib9p_srv_stat flush_file_stat(struct flush_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+ return (struct lib9p_srv_stat){
+ .qid = flush_file_qid(self),
+ .mode = 0444,
+ .atime_sec = UTIL9P_ATIME,
+ .mtime_sec = UTIL9P_MTIME,
+ .size = 6,
+ .name = lib9p_str(self->name),
+ .owner_uid = { .name = lib9p_str("root"), .num = 0 },
+ .owner_gid = { .name = lib9p_str("root"), .num = 0 },
+ .last_modifier_uid = { .name = lib9p_str("root"), .num = 0 },
+ .extension = lib9p_str(NULL),
+ };
+}
+static void flush_file_wstat(struct flush_file *self, struct lib9p_srv_ctx *ctx, struct lib9p_srv_stat) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot wstat API file");
+}
+static void flush_file_remove(struct flush_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot remove API file");
+}
+
+LIB9P_SRV_NOTDIR(struct flush_file, flush_file)
+
+static lo_interface lib9p_srv_fio flush_file_fopen(struct flush_file *self, struct lib9p_srv_ctx *ctx, bool, bool, bool) {
+ assert(self);
+ assert(ctx);
+
+ struct flush_fio *ret = heap_alloc(1, struct flush_fio);
+ ret->parent = self;
+
+ return lo_box_flush_fio_as_lib9p_srv_fio(ret);
+}
+
+/* srv_fio ********************************************************************/
+
+static void flush_fio_iofree(struct flush_fio *self) {
+ assert(self);
+ free(self);
+}
+
+static struct lib9p_qid flush_fio_qid(struct flush_fio *self) {
+ assert(self);
+ return flush_file_qid(self->parent);
+}
+
+static uint32_t flush_fio_iounit(struct flush_fio *self) {
+ assert(self);
+ return 0;
+}
+
+static uint32_t flush_fio_pwrite(struct flush_fio *LM_UNUSED(self),
+ struct lib9p_srv_ctx *LM_UNUSED(ctx),
+ void *LM_UNUSED(buf), uint32_t LM_UNUSED(byte_count),
+ uint64_t LM_UNUSED(offset)) {
+ assert_notreached("not writable");
+}
+
+static void flush_fio_pread(struct flush_fio *self, struct lib9p_srv_ctx *ctx,
+ uint32_t byte_count, uint64_t LM_UNUSED(byte_offset),
+ struct iovec *ret) {
+ assert(self);
+ assert(ctx);
+ assert(ret);
+
+ /* Wait for first Tflush */
+ while (!lib9p_srv_flush_requested(ctx))
+ cr_yield();
+
+ /* Wait for the specified number of Tflush (may be higher *or*
+ * lower than 1; lower would mean that the first Tflush needs
+ * to be flushed itself). */
+ while (cr_chan_num_waiters(&ctx->flush_ch) != self->parent->flush_cnt)
+ cr_yield();
+
+ /* Return */
+ switch (self->parent->flush_behavior) {
+ case FLUSH_READ:
+ *ret = (struct iovec){
+ .iov_base = "Sloth\n",
+ .iov_len = 6 < byte_count ? 6 : byte_count,
+ };
+ break;
+ case FLUSH_ERROR:
+ lib9p_srv_acknowledge_flush(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_ECANCELED, "request canceled by flush");
+ break;
+ case FLUSH_SILENT:
+ lib9p_srv_acknowledge_flush(ctx);
+ break;
+ }
+ cr_yield();
+}
diff --git a/lib9p/tests/test_server/fs_flush.h b/lib9p/tests/test_server/fs_flush.h
new file mode 100644
index 0000000..a509c4a
--- /dev/null
+++ b/lib9p/tests/test_server/fs_flush.h
@@ -0,0 +1,27 @@
+/* lib9p/tests/test_server/fs_flush.h - flush-* API endpoints
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIB9P_TESTS_TEST_SERVER_FS_FLUSH_H_
+#define _LIB9P_TESTS_TEST_SERVER_FS_FLUSH_H_
+
+#include <util9p/static.h>
+#include <libhw/host_net.h>
+
+struct flush_file {
+ char *name;
+ uint64_t pathnum;
+
+ unsigned int flush_cnt;
+ enum {
+ FLUSH_READ,
+ FLUSH_ERROR,
+ FLUSH_SILENT,
+ } flush_behavior;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct flush_file, flush_file);
+#define lo_box_flush_file_as_lib9p_srv_file(obj) util9p_box(flush_file, obj)
+
+#endif /* _LIB9P_TESTS_TEST_SERVER_FS_FLUSH_H_ */
diff --git a/lib9p/tests/test_server/fs_shutdown.c b/lib9p/tests/test_server/fs_shutdown.c
new file mode 100644
index 0000000..d4ae67e
--- /dev/null
+++ b/lib9p/tests/test_server/fs_shutdown.c
@@ -0,0 +1,104 @@
+/* lib9p/tests/test_server/fs_shutdown.c - /shutdown API endpoint
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libmisc/alloc.h>
+
+#include "fs_shutdown.h"
+
+LO_IMPLEMENTATION_C(lib9p_srv_file, struct shutdown_file, shutdown_file, static);
+
+struct shutdown_fio {
+ struct shutdown_file *parent;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_fio, struct shutdown_fio, shutdown_fio);
+LO_IMPLEMENTATION_C(lib9p_srv_fio, struct shutdown_fio, shutdown_fio, static);
+
+/* srv_file *******************************************************************/
+
+static void shutdown_file_free(struct shutdown_file *self) {
+ assert(self);
+}
+static struct lib9p_qid shutdown_file_qid(struct shutdown_file *self) {
+ assert(self);
+ return (struct lib9p_qid){
+ .type = LIB9P_QT_FILE | LIB9P_QT_APPEND,
+ .vers = 1,
+ .path = self->pathnum,
+ };
+}
+
+static struct lib9p_srv_stat shutdown_file_stat(struct shutdown_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+ return (struct lib9p_srv_stat){
+ .qid = shutdown_file_qid(self),
+ .mode = 0222 | LIB9P_DM_APPEND,
+ .atime_sec = UTIL9P_ATIME,
+ .mtime_sec = UTIL9P_MTIME,
+ .size = 0,
+ .name = lib9p_str(self->name),
+ .owner_uid = { .name=lib9p_str("root"), .num=0 },
+ .owner_gid = { .name=lib9p_str("root"), .num=0 },
+ .last_modifier_uid = { .name=lib9p_str("root"), .num=0 },
+ .extension = lib9p_str(NULL),
+ };
+}
+static void shutdown_file_wstat(struct shutdown_file *self, struct lib9p_srv_ctx *ctx, struct lib9p_srv_stat) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot wstat API file");
+}
+static void shutdown_file_remove(struct shutdown_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot remove API file");
+}
+
+LIB9P_SRV_NOTDIR(struct shutdown_file, shutdown_file)
+
+static lo_interface lib9p_srv_fio shutdown_file_fopen(struct shutdown_file *self, struct lib9p_srv_ctx *ctx, bool, bool, bool) {
+ assert(self);
+ assert(ctx);
+
+ struct shutdown_fio *ret = heap_alloc(1, struct shutdown_fio);
+ ret->parent = self;
+
+ return lo_box_shutdown_fio_as_lib9p_srv_fio(ret);
+}
+
+/* srv_fio ********************************************************************/
+
+static void shutdown_fio_iofree(struct shutdown_fio *self) {
+ assert(self);
+ free(self);
+}
+
+static struct lib9p_qid shutdown_fio_qid(struct shutdown_fio *self) {
+ assert(self);
+ return shutdown_file_qid(self->parent);
+}
+
+static uint32_t shutdown_fio_iounit(struct shutdown_fio *self) {
+ assert(self);
+ return 0;
+}
+
+static uint32_t shutdown_fio_pwrite(struct shutdown_fio *self, struct lib9p_srv_ctx *ctx, void *buf, uint32_t byte_count, uint64_t offset) {
+ assert(self);
+ assert(ctx);
+ assert(buf);
+ assert(offset == 0);
+ if (byte_count == 0)
+ return 0;
+ for (size_t i = 0; i < self->parent->nlisteners; i++)
+ LO_CALL(lo_box_hostnet_tcplist_as_net_stream_listener(&self->parent->listeners[i]), close);
+ return byte_count;
+}
+static void shutdown_fio_pread(struct shutdown_fio *LM_UNUSED(self), struct lib9p_srv_ctx *LM_UNUSED(ctx),
+ uint32_t LM_UNUSED(byte_count), uint64_t LM_UNUSED(byte_offset),
+ struct iovec *LM_UNUSED(ret)) {
+ assert_notreached("not readable");
+}
diff --git a/lib9p/tests/test_server/fs_shutdown.h b/lib9p/tests/test_server/fs_shutdown.h
new file mode 100644
index 0000000..65956db
--- /dev/null
+++ b/lib9p/tests/test_server/fs_shutdown.h
@@ -0,0 +1,23 @@
+/* lib9p/tests/test_server/fs_shutdown.h - /shutdown API endpoint
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIB9P_TESTS_TEST_SERVER_FS_SHUTDOWN_H_
+#define _LIB9P_TESTS_TEST_SERVER_FS_SHUTDOWN_H_
+
+#include <util9p/static.h>
+#include <libhw/host_net.h>
+
+struct shutdown_file {
+ char *name;
+ uint64_t pathnum;
+
+ struct hostnet_tcp_listener *listeners;
+ size_t nlisteners;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct shutdown_file, shutdown_file);
+#define lo_box_shutdown_file_as_lib9p_srv_file(obj) util9p_box(shutdown_file, obj)
+
+#endif /* _LIB9P_TESTS_TEST_SERVER_FS_SHUTDOWN_H_ */
diff --git a/lib9p/tests/test_server/fs_whoami.c b/lib9p/tests/test_server/fs_whoami.c
new file mode 100644
index 0000000..8d9752a
--- /dev/null
+++ b/lib9p/tests/test_server/fs_whoami.c
@@ -0,0 +1,153 @@
+/* lib9p/tests/test_server/fs_whoami.c - /whoami API endpoint
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <stdio.h> /* for snprintf() */
+#include <stdlib.h> /* for realloc(), free() */
+
+#include <libmisc/alloc.h>
+
+#include "fs_whoami.h"
+
+LO_IMPLEMENTATION_C(lib9p_srv_file, struct whoami_file, whoami_file, static);
+
+struct whoami_fio {
+ struct whoami_file *parent;
+ size_t buf_len;
+ char *buf;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_fio, struct whoami_fio, whoami_fio);
+LO_IMPLEMENTATION_C(lib9p_srv_fio, struct whoami_fio, whoami_fio, static);
+
+size_t whoami_len(struct lib9p_srv_ctx *ctx) {
+ assert(ctx);
+ assert(ctx->user);
+
+ size_t len = 0;
+ uint32_t uid = ctx->user->num;
+ while (uid) {
+ len++;
+ uid /= 10;
+ }
+ if (!len)
+ len++;
+ len += 2;
+ len += ctx->user->name.len;
+ return len;
+}
+
+/* srv_file *******************************************************************/
+
+static void whoami_file_free(struct whoami_file *self) {
+ assert(self);
+}
+static struct lib9p_qid whoami_file_qid(struct whoami_file *self) {
+ assert(self);
+ return (struct lib9p_qid){
+ .type = LIB9P_QT_FILE,
+ .vers = 1,
+ .path = self->pathnum,
+ };
+}
+
+static struct lib9p_srv_stat whoami_file_stat(struct whoami_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+
+ return (struct lib9p_srv_stat){
+ .qid = whoami_file_qid(self),
+ .mode = 0444,
+ .atime_sec = UTIL9P_ATIME,
+ .mtime_sec = UTIL9P_MTIME,
+ .size = whoami_len(ctx),
+ .name = lib9p_str(self->name),
+ .owner_uid = { .name=lib9p_str("root"), .num=0 },
+ .owner_gid = { .name=lib9p_str("root"), .num=0 },
+ .last_modifier_uid = { .name=lib9p_str("root"), .num=0 },
+ .extension = lib9p_str(NULL),
+ };
+}
+static void whoami_file_wstat(struct whoami_file *self, struct lib9p_srv_ctx *ctx, struct lib9p_srv_stat) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot wstat API file");
+}
+static void whoami_file_remove(struct whoami_file *self, struct lib9p_srv_ctx *ctx) {
+ assert(self);
+ assert(ctx);
+ lib9p_error(&ctx->basectx, LIB9P_ERRNO_L_EROFS, "cannot remove API file");
+}
+
+LIB9P_SRV_NOTDIR(struct whoami_file, whoami_file)
+
+static lo_interface lib9p_srv_fio whoami_file_fopen(struct whoami_file *self, struct lib9p_srv_ctx *ctx, bool, bool, bool) {
+ assert(self);
+ assert(ctx);
+
+ struct whoami_fio *ret = heap_alloc(1, struct whoami_fio);
+ ret->parent = self;
+ ret->buf_len = 0;
+ ret->buf = NULL;
+
+ return lo_box_whoami_fio_as_lib9p_srv_fio(ret);
+}
+
+/* srv_fio ********************************************************************/
+
+static void whoami_fio_iofree(struct whoami_fio *self) {
+ assert(self);
+ if (self->buf)
+ free(self->buf);
+ free(self);
+}
+
+static struct lib9p_qid whoami_fio_qid(struct whoami_fio *self) {
+ assert(self);
+ assert(self->parent);
+ return whoami_file_qid(self->parent);
+}
+
+static uint32_t whoami_fio_iounit(struct whoami_fio *self) {
+ assert(self);
+ return 0;
+}
+
+static uint32_t whoami_fio_pwrite(struct whoami_fio *LM_UNUSED(self),
+ struct lib9p_srv_ctx *LM_UNUSED(ctx),
+ void *LM_UNUSED(buf), uint32_t LM_UNUSED(byte_count),
+ uint64_t LM_UNUSED(offset)) {
+ assert_notreached("not writable");
+}
+static void whoami_fio_pread(struct whoami_fio *self, struct lib9p_srv_ctx *ctx,
+ uint32_t byte_count, uint64_t byte_offset,
+ struct iovec *ret) {
+ assert(self);
+ assert(ctx);
+ assert(ret);
+
+ size_t data_size = whoami_len(ctx);
+ if (self->buf_len < data_size+1) {
+ self->buf = realloc(self->buf, data_size+1);
+ self->buf_len = data_size+1;
+ }
+ snprintf(self->buf, self->buf_len, "%"PRIu32" %.*s\n",
+ ctx->user->num, ctx->user->name.len, ctx->user->name.utf8);
+
+ if (byte_offset > (uint64_t)data_size) {
+ lib9p_error(&ctx->basectx,
+ LIB9P_ERRNO_L_EINVAL, "offset is past end-of-file length");
+ return;
+ }
+
+ size_t beg_off = (size_t)byte_offset;
+ size_t end_off = beg_off + (size_t)byte_count;
+ if (end_off > data_size)
+ end_off = data_size;
+
+ *ret = (struct iovec){
+ .iov_base = &self->buf[beg_off],
+ .iov_len = end_off-beg_off,
+ };
+}
diff --git a/lib9p/tests/test_server/fs_whoami.h b/lib9p/tests/test_server/fs_whoami.h
new file mode 100644
index 0000000..0d3d311
--- /dev/null
+++ b/lib9p/tests/test_server/fs_whoami.h
@@ -0,0 +1,20 @@
+/* lib9p/tests/test_server/fs_whoami.h - /whoami API endpoint
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#ifndef _LIB9P_TESTS_TEST_SERVER_FS_WHOAMI_H_
+#define _LIB9P_TESTS_TEST_SERVER_FS_WHOAMI_H_
+
+#include <util9p/static.h>
+#include <libhw/host_net.h>
+
+struct whoami_file {
+ char *name;
+ uint64_t pathnum;
+};
+LO_IMPLEMENTATION_H(lib9p_srv_file, struct whoami_file, whoami_file);
+#define lo_box_whoami_file_as_lib9p_srv_file(obj) util9p_box(whoami_file, obj)
+
+#endif /* _LIB9P_TESTS_TEST_SERVER_FS_WHOAMI_H_ */
diff --git a/lib9p/tests/test_server/main.c b/lib9p/tests/test_server/main.c
new file mode 100644
index 0000000..d7819eb
--- /dev/null
+++ b/lib9p/tests/test_server/main.c
@@ -0,0 +1,168 @@
+/* lib9p/tests/test_server/main.c - Main entry point for test 9P server
+ *
+ * Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <error.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h> /* for atoi() */
+
+#include <lib9p/srv.h>
+#include <libcr/coroutine.h>
+#include <libhw/generic/net.h>
+#include <libhw/generic/alarmclock.h>
+#include <libhw/host_alarmclock.h>
+#include <libhw/host_net.h>
+#include <libmisc/macro.h>
+#include <util9p/static.h>
+
+#include "static.h"
+#include "fs_flush.h"
+#include "fs_shutdown.h"
+#include "fs_whoami.h"
+
+/* configuration **************************************************************/
+
+#include "config.h"
+
+#ifndef _CONFIG_9P_MAX_CONNS
+ #error config.h must define _CONFIG_9P_MAX_CONNS
+#endif
+#ifndef _CONFIG_9P_MAX_REQS
+ #error config.h must define _CONFIG_9P_MAX_REQS
+#endif
+
+/* globals ********************************************************************/
+
+static lo_interface lib9p_srv_file get_root(struct lib9p_srv_ctx *, struct lib9p_s);
+
+const char *hexdig = "0123456789abcdef";
+
+struct {
+ uint16_t port;
+ struct hostnet_tcp_listener listeners[_CONFIG_9P_MAX_CONNS];
+ struct lib9p_srv srv;
+ FILE *logstream;
+} globals = {
+ .srv = (struct lib9p_srv){
+ .rootdir = get_root,
+ },
+};
+
+/* file tree ******************************************************************/
+
+#define STATIC_FILE(N, STRNAME, SYMNAME) \
+ UTIL9P_STATIC_FILE(N, STRNAME, \
+ .data_start = _binary_static_##SYMNAME##_start, \
+ .data_end = _binary_static_##SYMNAME##_end)
+#define STATIC_DIR(N, STRNAME, ...) \
+ UTIL9P_STATIC_DIR(N, STRNAME, __VA_ARGS__)
+
+#define API_FILE(N, STRNAME, SYMNAME, ...) \
+ lo_box_##SYMNAME##_file_as_lib9p_srv_file(&((struct SYMNAME##_file){ \
+ .name = STRNAME, \
+ .pathnum = N \
+ __VA_OPT__(,) __VA_ARGS__ \
+ }))
+
+struct lib9p_srv_file root =
+ STATIC_DIR(1, "",
+ STATIC_DIR(2, "Documentation",
+ STATIC_FILE(3, "x", Documentation_x_txt),
+ ),
+ STATIC_FILE(4, "README.md", README_md),
+ API_FILE(5, "shutdown", shutdown,
+ .listeners = globals.listeners,
+ .nlisteners = LM_ARRAY_LEN(globals.listeners)),
+ API_FILE(8, "whoami", whoami),
+ API_FILE(9, "flush-read", flush, .flush_cnt=1, .flush_behavior=FLUSH_READ),
+ API_FILE(10, "flush-error", flush, .flush_cnt=1, .flush_behavior=FLUSH_ERROR),
+ API_FILE(11, "flush-silent", flush, .flush_cnt=1, .flush_behavior=FLUSH_SILENT),
+ API_FILE(12, "flush-slowsilent", flush, .flush_cnt=2, .flush_behavior=FLUSH_SILENT),
+ API_FILE(13, "flush-slowread", flush, .flush_cnt=0, .flush_behavior=FLUSH_READ),
+ );
+
+static lo_interface lib9p_srv_file get_root(struct lib9p_srv_ctx *LM_UNUSED(ctx), struct lib9p_s LM_UNUSED(treename)) {
+ return root;
+}
+
+/* main ***********************************************************************/
+
+static COROUTINE read_cr(void *_i) {
+ int i = *((int *)_i);
+ cr_begin();
+
+ hostnet_tcp_listener_init(&globals.listeners[i], globals.port);
+
+ lib9p_srv_accept_and_read_loop(&globals.srv, lo_box_hostnet_tcplist_as_net_stream_listener(&globals.listeners[i]));
+
+ cr_end();
+}
+
+static COROUTINE write_cr(void *) {
+ cr_begin();
+
+ lib9p_srv_worker_loop(&globals.srv);
+
+ cr_end();
+}
+
+static COROUTINE init_cr(void *) {
+ cr_begin();
+
+ sleep_for_ms(1); /* test that sleep works */
+
+ for (int i = 0; i < _CONFIG_9P_MAX_CONNS; i++) {
+ char name[] = {'r', 'e', 'a', 'd', '-', hexdig[i], '\0'};
+ if (!coroutine_add(name, read_cr, &i))
+ error(1, 0, "coroutine_add(read_cr, &i)");
+ }
+ for (int i = 0; i < _CONFIG_9P_MAX_REQS; i++) {
+ char name[] = {'w', 'r', 'i', 't', 'e', '-', hexdig[i], '\0'};
+ if (!coroutine_add(name, write_cr, NULL))
+ error(1, 0, "coroutine_add(write_cr, NULL)");
+ }
+
+ cr_exit();
+}
+
+static void log_fct(char character, void *_stream) {
+ FILE *stream = _stream;
+ putc(character, stream);
+ putchar(character);
+}
+
+static void log_msg(struct lib9p_srv_ctx *ctx, enum lib9p_msg_type typ, void *hostmsg) {
+ /* It sucks that %v trips -Wformat and -Wformat-extra-args
+ * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=47781 */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat"
+#pragma GCC diagnostic ignored "-Wformat-extra-args"
+ fmt_fctprintf(log_fct, globals.logstream,
+ "%c %v\n", typ % 2 ? '<' : '>',
+ lo_box_lib9p_msg_as_fmt_formatter(&ctx->basectx, typ, hostmsg));
+#pragma GCC diagnostic pop
+ fflush(globals.logstream);
+}
+
+int main(int argc, char *argv[]) {
+ if (argc != 3)
+ error(2, 0, "usage: %s PORT_NUMBER LOGFILE", argv[0]);
+
+ globals.port = atoi(argv[1]);
+ globals.logstream = fopen(argv[2], "w");
+ if (!globals.logstream)
+ error(2, errno, "fopen");
+ globals.srv.msglog = log_msg;
+
+ struct hostclock clock_monotonic = {
+ .clock_id = CLOCK_MONOTONIC,
+ };
+ bootclock = lo_box_hostclock_as_alarmclock(&clock_monotonic);
+ coroutine_add("init", init_cr, NULL);
+ coroutine_main();
+ fclose(globals.logstream);
+ return 0;
+}
diff --git a/lib9p/tests/test_server/static/Documentation/x.txt b/lib9p/tests/test_server/static/Documentation/x.txt
new file mode 100644
index 0000000..e85ee4e
--- /dev/null
+++ b/lib9p/tests/test_server/static/Documentation/x.txt
@@ -0,0 +1,7 @@
+<!--
+ Documentation/x.txt - test static file
+
+ Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+foo
diff --git a/lib9p/tests/test_server/static/README.md b/lib9p/tests/test_server/static/README.md
new file mode 100644
index 0000000..c2d88ed
--- /dev/null
+++ b/lib9p/tests/test_server/static/README.md
@@ -0,0 +1,7 @@
+<!--
+ README.md - test static file
+
+ Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+Hello, world!
diff --git a/lib9p/tests/testclient-p9p b/lib9p/tests/testclient-p9p
new file mode 100755
index 0000000..09ce746
--- /dev/null
+++ b/lib9p/tests/testclient-p9p
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# lib9p/tests/testclient-p9p - Test the 9P `test_server` against Plan 9 Port's `9p` utility
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+set -euE -o pipefail
+
+if [[ $# != 1 ]]; then
+ echo >&2 "Usage: $0 SERVER_PORT"
+ echo >&2 "Usage: ./runtest $0 EXPLOG"
+ exit 2
+fi
+
+expect_lines() (
+ { set +x; } &>/dev/null
+ printf >&2 '+ diff -u expected.txt actual.txt\n'
+ diff -u <(printf '%s\n' "$@") <(printf '%s\n' "$out")
+)
+
+set -x
+client=(unshare --user 9p -a "localhost:${1}")
+
+out=$("${client[@]}" ls -l '')
+expect_lines \
+ 'd-r-xr-xr-x M 0 root root 0 Oct 7 2024 Documentation' \
+ '--r--r--r-- M 0 root root 166 Oct 7 2024 README.md' \
+ '--r--r--r-- M 0 root root 6 Oct 7 2024 flush-error' \
+ '--r--r--r-- M 0 root root 6 Oct 7 2024 flush-read' \
+ '--r--r--r-- M 0 root root 6 Oct 7 2024 flush-silent' \
+ '--r--r--r-- M 0 root root 6 Oct 7 2024 flush-slowread' \
+ '--r--r--r-- M 0 root root 6 Oct 7 2024 flush-slowsilent' \
+ 'a--w--w--w- M 0 root root 0 Oct 7 2024 shutdown' \
+ '--r--r--r-- M 0 root root 9 Oct 7 2024 whoami'
+
+out=$("${client[@]}" ls -l 'Documentation/')
+expect_lines \
+ '--r--r--r-- M 0 root root 166 Oct 7 2024 x'
+
+out=$("${client[@]}" read 'README.md')
+expect_lines \
+ '<!--' \
+ ' README.md - test static file' \
+ '' \
+ ' Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>' \
+ ' SPDX-License-Identifier: AGPL-3.0-or-later' \
+ '-->' \
+ 'Hello, world!'
+
+out=$("${client[@]}" read 'Documentation/x')
+expect_lines \
+ '<!--' \
+ ' Documentation/x.txt - test static file' \
+ '' \
+ ' Copyright (C) 2024-2025 Luke T. Shumaker <lukeshu@lukeshu.com>' \
+ ' SPDX-License-Identifier: AGPL-3.0-or-later' \
+ '-->' \
+ 'foo'
+
+out=$("${client[@]}" stat 'Documentation/x')
+expect_lines \
+ "'x' 'root' 'root' 'root' q (0000000000000003 1 ) m 0444 at 1728337905 mt 1728337904 l 166 t 0 d 0"
+
+out=$("${client[@]}" write 'shutdown' <<<1)
+expect_lines ''
diff --git a/lib9p/tests/testclient-p9p.explog b/lib9p/tests/testclient-p9p.explog
new file mode 100644
index 0000000..54f1e4b
--- /dev/null
+++ b/lib9p/tests/testclient-p9p.explog
@@ -0,0 +1,106 @@
+# lib9p/tests/testclient-p9p.explog - Expected 9P logfile of testclient-p9p
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=0 wname=[ ] }
+< Rwalk { tag=0 nwqid=0 wqid=[ ] }
+> Tstat { tag=0 fid=1 }
+< Rstat { tag=0 stat={ fstype=0 fsdev=0 qid={ type=(DIR) vers=1 path=1 } mode=(DIR|0555) atime=1728337905 mtime=1728337904 length=0 name="" owner_uname="root" owner_gname="root" last_modifier_uname="root" extension="" owner_unum=0 owner_gnum=0 last_modifier_unum=0 } }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Twalk { tag=0 fid=0 newfid=1 nwname=0 wname=[ ] }
+< Rwalk { tag=0 nwqid=0 wqid=[ ] }
+> Topen { tag=0 fid=1 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(DIR) vers=1 path=1 } iounit=0 }
+> Tread { tag=0 fid=1 offset=0 count=4096 }
+< Rread { tag=0 count=648 data=<bytedata> }
+> Tread { tag=0 fid=1 offset=648 count=4096 }
+< Rread { tag=0 count=0 data="" }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=1 wname=[ "Documentation" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(DIR) vers=1 path=2 } ] }
+> Tstat { tag=0 fid=1 }
+< Rstat { tag=0 stat={ fstype=0 fsdev=0 qid={ type=(DIR) vers=1 path=2 } mode=(DIR|0555) atime=1728337905 mtime=1728337904 length=0 name="Documentation" owner_uname="root" owner_gname="root" last_modifier_uname="root" extension="" owner_unum=0 owner_gnum=0 last_modifier_unum=0 } }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Twalk { tag=0 fid=0 newfid=1 nwname=1 wname=[ "Documentation" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(DIR) vers=1 path=2 } ] }
+> Topen { tag=0 fid=1 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(DIR) vers=1 path=2 } iounit=0 }
+> Tread { tag=0 fid=1 offset=0 count=4096 }
+< Rread { tag=0 count=62 data=<bytedata> }
+> Tread { tag=0 fid=1 offset=62 count=4096 }
+< Rread { tag=0 count=0 data="" }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=1 wname=[ "README.md" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=4 } ] }
+> Topen { tag=0 fid=1 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=4 } iounit=0 }
+> Tread { tag=0 fid=1 offset=0 count=4096 }
+< Rread { tag=0 count=166 data="<!--\n README.md - test static file\n\n Copyright ("... }
+> Tread { tag=0 fid=1 offset=166 count=4096 }
+< Rread { tag=0 count=0 data="" }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=2 wname=[ "Documentation", "x" ] }
+< Rwalk { tag=0 nwqid=2 wqid=[ { type=(DIR) vers=1 path=2 }, { type=(0) vers=1 path=3 } ] }
+> Topen { tag=0 fid=1 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=3 } iounit=0 }
+> Tread { tag=0 fid=1 offset=0 count=4096 }
+< Rread { tag=0 count=166 data="<!--\n Documentation/x.txt - test static file\n\n C"... }
+> Tread { tag=0 fid=1 offset=166 count=4096 }
+< Rread { tag=0 count=0 data="" }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=2 wname=[ "Documentation", "x" ] }
+< Rwalk { tag=0 nwqid=2 wqid=[ { type=(DIR) vers=1 path=2 }, { type=(0) vers=1 path=3 } ] }
+> Tstat { tag=0 fid=1 }
+< Rstat { tag=0 stat={ fstype=0 fsdev=0 qid={ type=(0) vers=1 path=3 } mode=(0444) atime=1728337905 mtime=1728337904 length=166 name="x" owner_uname="root" owner_gname="root" last_modifier_uname="root" extension="" owner_unum=0 owner_gnum=0 last_modifier_unum=0 } }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
+> Tversion { tag=NOTAG max_msg_size=8192 version="9P2000" }
+< Rversion { tag=NOTAG max_msg_size=4120 version="9P2000" }
+> Tauth { tag=0 afid=0 uname="nobody" aname="" unum=0 }
+< Rerror { tag=0 errstr="authentication not required" errnum=L_EOPNOTSUPP }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=1 nwname=1 wname=[ "shutdown" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(APPEND) vers=1 path=5 } ] }
+> Topen { tag=0 fid=1 mode=(TRUNC|MODE_WRITE) }
+< Ropen { tag=0 qid={ type=(APPEND) vers=1 path=5 } iounit=0 }
+> Twrite { tag=0 fid=1 offset=0 count=2 data="1\n" }
+< Rwrite { tag=0 count=2 }
+> Tclunk { tag=0 fid=1 }
+< Rclunk { tag=0 }
diff --git a/lib9p/tests/testclient-sess.c b/lib9p/tests/testclient-sess.c
new file mode 100644
index 0000000..7cb7f97
--- /dev/null
+++ b/lib9p/tests/testclient-sess.c
@@ -0,0 +1,243 @@
+/* lib9p/tests/testclient-sess.c - Test the 9P `test_server`'s sessions
+ *
+ * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <arpa/inet.h> /* for htons(), inet_addr() */
+#include <errno.h>
+#include <error.h>
+#include <netinet/in.h> /* for struct sockaddr{,_in} */
+#include <stdlib.h> /* for atoi() */
+#include <sys/socket.h> /* for socket(), connect() */
+#include <sys/uio.h> /* for writev() */
+#include <unistd.h> /* for read() */
+
+#include <libmisc/assert.h>
+#include <libmisc/endian.h>
+#include <lib9p/core.h>
+
+#define MAX_MSG_SIZE (8*1024)
+
+static void _send9p(int fd, struct lib9p_ctx *ctx, enum lib9p_msg_type typ, void *body) {
+ struct lib9p_Tmsg_send_buf buf;
+ bool err = lib9p_Tmsg_marshal(ctx, typ, body, &buf);
+ assert(!err);
+ size_t exp = 0;
+ for (size_t i = 0; i < buf.iov_cnt; i++)
+ exp += buf.iov[i].iov_len;
+ ssize_t act = writev(fd, buf.iov, buf.iov_cnt);
+ if (act < 0)
+ error(1, errno, "writev");
+ assert((size_t)act == exp);
+}
+
+#define send9p(typ, ...) _send9p(fd, &ctx, LIB9P_TYP_##typ, &((struct lib9p_msg_##typ){ __VA_ARGS__ }))
+
+static void _recv9p(int fd) {
+ uint8_t buf[MAX_MSG_SIZE];
+ size_t goal = 4;
+ size_t done = 0;
+ while (done < goal) {
+ ssize_t n = read(fd, &buf[done], goal-done);
+ if (n < 0)
+ error(1, errno, "read");
+ done += n;
+ }
+ goal = uint32le_decode(buf);
+ assert(goal <= MAX_MSG_SIZE);
+ while (done < goal) {
+ ssize_t n = read(fd, &buf[done], goal-done);
+ if (n < 0)
+ error(1, errno, "read");
+ done += n;
+ }
+}
+
+#define recv9p() _recv9p(fd)
+
+int main(int argc, char *argv[]) {
+ if (argc != 2)
+ error(2, 0, "Usage: %s SERVER_PORT", argv[0]);
+ uint16_t server_port = atoi(argv[1]);
+
+ union {
+ struct sockaddr gen;
+ struct sockaddr_in in;
+ } server_addr = {};
+ server_addr.in.sin_family = AF_INET;
+ server_addr.in.sin_addr.s_addr = inet_addr("127.0.0.1");
+ server_addr.in.sin_port = htons(server_port);
+
+ int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (fd < 0)
+ error(1, errno, "socket");
+ if (connect(fd, &server_addr.gen, sizeof(server_addr)) < 0)
+ error(1, errno, "connect");
+
+ struct lib9p_ctx ctx = {
+ .max_msg_size = 16*1024,
+ };
+
+ struct lib9p_s wname[1];
+
+ /* numeric downgrade, unknown ext *************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=57, .version=lib9p_str("9P2025.x"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000;
+
+ /* numeric downgrade, known ext ***************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=57, .version=lib9p_str("9P2025.u"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000_u;
+
+ /* ext version, users *************************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=(8*1024), .version=lib9p_str("9P2000.u"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000_u;
+ send9p(Tattach, .tag=0, .fid=0, .afid=LIB9P_FID_NOFID, .uname=lib9p_str("alice"), .unum=1000, .aname=lib9p_str(""));
+ recv9p(); /* Rattach */
+ send9p(Tattach, .tag=0, .fid=1, .afid=LIB9P_FID_NOFID, .uname=lib9p_str("bob"), .unum=1001, .aname=lib9p_str(""));
+ recv9p(); /* Rattach */
+ wname[0] = lib9p_str("whoami"); send9p(Twalk, .tag=0, .fid=0, .newfid=2, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ wname[0] = lib9p_str("whoami"); send9p(Twalk, .tag=0, .fid=1, .newfid=3, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=2, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Topen, .tag=0, .fid=3, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=2, .offset=0, .count=100);
+ recv9p(); /* Rread */
+ send9p(Tread, .tag=0, .fid=3, .offset=0, .count=100);
+ recv9p(); /* Rread */
+
+ /* walk ***************************************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=(8*1024), .version=lib9p_str("9P2000"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000;
+ send9p(Tattach, .tag=0, .fid=0, .afid=LIB9P_FID_NOFID, .uname=lib9p_str("nobody"), .aname=lib9p_str(""));
+ recv9p(); /* Rattach */
+
+ /* dup */
+ send9p(Twalk, .tag=0, .fid=0, .newfid=1, .nwname=0);
+ recv9p(); /* Rwalk */
+
+ /* "The walk request carries as arguments an existing fid"... */
+ send9p(Twalk, .tag=0, .fid=2, .newfid=3, .nwname=0);
+ recv9p(); /* Rerror */
+
+ /* ..."and a proposed newfid"... */
+ send9p(Twalk, .tag=0, .fid=1, .newfid=0xffffffff, .nwname=0);
+ recv9p(); /* Rerror */
+
+ /* ..."(which must not be in use"... */
+ send9p(Twalk, .tag=0, .fid=1, .newfid=0, .nwname=0);
+ recv9p(); /* Rerror */
+
+ /* ..."unless it is the same as fid)"... */
+ send9p(Twalk, .tag=0, .fid=1, .newfid=1, .nwname=0);
+ recv9p(); /* Rwalk */
+
+ /* ... "that the client wishes to associate with the result of
+ * traversing the directory hierarchy by `walking' the heierarchy using
+ * the successive path name elements wname."... */
+
+ /* ..."The fid must represent a directory"... */
+ wname[0] = lib9p_str("README.md"); send9p(Twalk, .tag=0, .fid=1, .newfid=2, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ wname[0] = lib9p_str(".."); send9p(Twalk, .tag=0, .fid=2, .newfid=3, .nwname=1, .wname=wname);
+ recv9p(); /* Rerror */
+
+ /* ..."unless zero path name elements are specified." */
+ send9p(Twalk, .tag=0, .fid=2, .newfid=3, .nwname=0);
+ recv9p(); /* Rwalk */
+
+ /* "The fid must be valid in the current session" (tested above)... */
+
+ /* ..."and must not have been opened for I/O by an open or create
+ * message."... */
+ send9p(Topen, .tag=0, .fid=3, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Twalk, .tag=0, .fid=3, .newfid=4, .nwname=0);
+ recv9p(); /* Rerror */
+
+ /* flush **************************************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=(8*1024), .version=lib9p_str("9P2000"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000;
+ send9p(Tattach, .tag=0, .fid=0, .afid=LIB9P_FID_NOFID, .uname=lib9p_str("nobody"), .aname=lib9p_str(""));
+ recv9p(); /* Rattach */
+
+ /* flush, but original response comes back first */
+ wname[0] = lib9p_str("flush-read"); send9p(Twalk, .tag=0, .fid=0, .newfid=1, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=1, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=1, .offset=0, .count=10);
+ send9p(Tflush, .tag=1, .oldtag=0);
+ recv9p(); /* Rread */
+ recv9p(); /* Rflush */
+
+ /* flush, original request is aborted with error */
+ wname[0] = lib9p_str("flush-error"); send9p(Twalk, .tag=0, .fid=0, .newfid=2, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=2, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=2, .offset=0, .count=10);
+ send9p(Tflush, .tag=1, .oldtag=0);
+ recv9p(); /* Rerror */
+ recv9p(); /* Rflush */
+
+ /* flush, original request is aborted without error */
+ wname[0] = lib9p_str("flush-silent"); send9p(Twalk, .tag=0, .fid=0, .newfid=3, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=3, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=3, .offset=0, .count=10);
+ send9p(Tflush, .tag=1, .oldtag=0);
+ recv9p(); /* Rflush */
+
+ /* multiflush, original request is aborted without error */
+ wname[0] = lib9p_str("flush-slowsilent"); send9p(Twalk, .tag=0, .fid=0, .newfid=4, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=4, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=4, .offset=0, .count=10);
+ send9p(Tflush, .tag=1, .oldtag=0);
+ send9p(Tflush, .tag=2, .oldtag=0);
+ recv9p(); /* Rflush */
+
+ /* flush, but flush is flushed */
+ wname[0] = lib9p_str("flush-slowread"); send9p(Twalk, .tag=0, .fid=0, .newfid=5, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=5, .mode=LIB9P_O_MODE_READ);
+ recv9p(); /* Ropen */
+ send9p(Tread, .tag=0, .fid=5, .offset=0, .count=10);
+ send9p(Tflush, .tag=1, .oldtag=0);
+ send9p(Tflush, .tag=2, .oldtag=1);
+ recv9p(); /* Rflush */
+ recv9p(); /* Rread */
+
+ /* flush, unknown tag */
+ send9p(Tflush, .tag=0, .oldtag=99);
+ recv9p(); /* Rflush */
+
+ /* flushed by Tversion */
+ send9p(Tread, .tag=0, .fid=3, .offset=0, .count=10);
+
+ /* shutdown ***********************************************************/
+ send9p(Tversion, .tag=0, .max_msg_size=(8*1024), .version=lib9p_str("9P2000"));
+ recv9p(); /* Rversion */
+ ctx.version = LIB9P_VER_9P2000;
+ send9p(Tattach, .tag=0, .fid=0, .afid=LIB9P_FID_NOFID, .uname=lib9p_str("nobody"), .aname=lib9p_str(""));
+ recv9p(); /* Rattach */
+ /* check the newfid==fid case */
+ wname[0] = lib9p_str("shutdown"); send9p(Twalk, .tag=0, .fid=0, .newfid=0, .nwname=1, .wname=wname);
+ recv9p(); /* Rwalk */
+ send9p(Topen, .tag=0, .fid=0, .mode=LIB9P_O_MODE_WRITE);
+ recv9p(); /* Ropen */
+ send9p(Twrite, .tag=0, .fid=0, .offset=0, .count=2, .data="1\n");
+ recv9p(); /* Rwrite */
+ return 0;
+}
diff --git a/lib9p/tests/testclient-sess.explog b/lib9p/tests/testclient-sess.explog
new file mode 100644
index 0000000..a3838ac
--- /dev/null
+++ b/lib9p/tests/testclient-sess.explog
@@ -0,0 +1,156 @@
+# lib9p/tests/testclient-sess.explog - Expected 9P logfile of testclient-sess.c
+#
+# Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# numeric downgrade, unknown ext ###############################################
+> Tversion { tag=0 max_msg_size=57 version="9P2025.x" }
+< Rversion { tag=0 max_msg_size=57 version="9P2000" }
+
+# numeric downgrade, known ext #################################################
+> Tversion { tag=0 max_msg_size=57 version="9P2025.u" }
+< Rversion { tag=0 max_msg_size=57 version="9P2000.u" }
+
+# ext version, users ###########################################################
+> Tversion { tag=0 max_msg_size=8192 version="9P2000.u" }
+< Rversion { tag=0 max_msg_size=4120 version="9P2000.u" }
+> Tattach { tag=0 fid=0 afid=NOFID uname="alice" aname="" unum=1000 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Tattach { tag=0 fid=1 afid=NOFID uname="bob" aname="" unum=1001 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=2 nwname=1 wname=[ "whoami" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=8 } ] }
+> Twalk { tag=0 fid=1 newfid=3 nwname=1 wname=[ "whoami" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=8 } ] }
+> Topen { tag=0 fid=2 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=8 } iounit=0 }
+> Topen { tag=0 fid=3 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=8 } iounit=0 }
+> Tread { tag=0 fid=2 offset=0 count=100 }
+< Rread { tag=0 count=11 data="1000 alice\n" }
+> Tread { tag=0 fid=3 offset=0 count=100 }
+< Rread { tag=0 count=9 data="1001 bob\n" }
+
+# walk #########################################################################
+> Tversion { tag=0 max_msg_size=8192 version="9P2000" }
+< Rversion { tag=0 max_msg_size=4120 version="9P2000" }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+
+# dup
+> Twalk { tag=0 fid=0 newfid=1 nwname=0 wname=[ ] }
+< Rwalk { tag=0 nwqid=0 wqid=[ ] }
+
+# "The walk request carries as arguments an existing fid"...
+> Twalk { tag=0 fid=2 newfid=3 nwname=0 wname=[ ] }
+< Rerror { tag=0 errstr="bad file number 2" errnum=L_EBADF }
+
+# ..."and a proposed newfid"...
+> Twalk { tag=0 fid=1 newfid=NOFID nwname=0 wname=[ ] }
+< Rerror { tag=0 errstr="cannot assign to NOFID" errnum=L_EBADF }
+
+# ..."(which must not be in use"...
+> Twalk { tag=0 fid=1 newfid=0 nwname=0 wname=[ ] }
+< Rerror { tag=0 errstr="FID already in use" errnum=L_EBADF }
+
+# ..."unless it is the same as fid)"...
+> Twalk { tag=0 fid=1 newfid=1 nwname=0 wname=[ ] }
+< Rwalk { tag=0 nwqid=0 wqid=[ ] }
+
+# ... "that the client wishes to associate with the result of
+# traversing the directory hierarchy by `walking' the heierarchy using
+# the successive path name elements wname."...
+
+# ..."The fid must represent a directory"...
+> Twalk { tag=0 fid=1 newfid=2 nwname=1 wname=[ "README.md" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=4 } ] }
+> Twalk { tag=0 fid=2 newfid=3 nwname=1 wname=[ ".." ] }
+< Rerror { tag=0 errstr="not a directory" errnum=L_ENOTDIR }
+
+# ..."unless zero path name elements are specified."
+> Twalk { tag=0 fid=2 newfid=3 nwname=0 wname=[ ] }
+< Rwalk { tag=0 nwqid=0 wqid=[ ] }
+
+# "The fid must be valid in the current session" (tested above)...
+
+# ..."and must not have been opened for I/O by an open or create
+# message."...
+> Topen { tag=0 fid=3 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=4 } iounit=0 }
+> Twalk { tag=0 fid=3 newfid=4 nwname=0 wname=[ ] }
+< Rerror { tag=0 errstr="cannot walk on FID open for I/O" errnum=L_EALREADY }
+
+# flush ########################################################################
+> Tversion { tag=0 max_msg_size=8192 version="9P2000" }
+< Rversion { tag=0 max_msg_size=4120 version="9P2000" }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+
+# flush, but original response comes back first
+> Twalk { tag=0 fid=0 newfid=1 nwname=1 wname=[ "flush-read" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=9 } ] }
+> Topen { tag=0 fid=1 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=9 } iounit=0 }
+> Tread { tag=0 fid=1 offset=0 count=10 }
+> Tflush { tag=1 oldtag=0 }
+< Rread { tag=0 count=6 data="Sloth\n" }
+< Rflush { tag=1 }
+
+# flush, original request is aborted with error
+> Twalk { tag=0 fid=0 newfid=2 nwname=1 wname=[ "flush-error" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=10 } ] }
+> Topen { tag=0 fid=2 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=10 } iounit=0 }
+> Tread { tag=0 fid=2 offset=0 count=10 }
+> Tflush { tag=1 oldtag=0 }
+< Rerror { tag=0 errstr="request canceled by flush" errnum=L_ECANCELED }
+< Rflush { tag=1 }
+
+# flush, original request is aborted without error
+> Twalk { tag=0 fid=0 newfid=3 nwname=1 wname=[ "flush-silent" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=11 } ] }
+> Topen { tag=0 fid=3 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=11 } iounit=0 }
+> Tread { tag=0 fid=3 offset=0 count=10 }
+> Tflush { tag=1 oldtag=0 }
+< Rflush { tag=1 }
+
+# multiflush, original request is aborted without error
+> Twalk { tag=0 fid=0 newfid=4 nwname=1 wname=[ "flush-slowsilent" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=12 } ] }
+> Topen { tag=0 fid=4 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=12 } iounit=0 }
+> Tread { tag=0 fid=4 offset=0 count=10 }
+> Tflush { tag=1 oldtag=0 }
+> Tflush { tag=2 oldtag=0 }
+< Rflush { tag=2 }
+
+# flush, but flush is flushed
+> Twalk { tag=0 fid=0 newfid=5 nwname=1 wname=[ "flush-slowread" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(0) vers=1 path=13 } ] }
+> Topen { tag=0 fid=5 mode=(MODE_READ) }
+< Ropen { tag=0 qid={ type=(0) vers=1 path=13 } iounit=0 }
+> Tread { tag=0 fid=5 offset=0 count=10 }
+> Tflush { tag=1 oldtag=0 }
+> Tflush { tag=2 oldtag=1 }
+< Rflush { tag=2 }
+< Rread { tag=0 count=6 data="Sloth\n" }
+
+# flush, unknown tag
+> Tflush { tag=0 oldtag=99 }
+< Rflush { tag=0 }
+
+# flushed by Tversion
+> Tread { tag=0 fid=3 offset=0 count=10 }
+
+# shutdown #####################################################################
+> Tversion { tag=0 max_msg_size=8192 version="9P2000" }
+< Rversion { tag=0 max_msg_size=4120 version="9P2000" }
+> Tattach { tag=0 fid=0 afid=NOFID uname="nobody" aname="" unum=0 }
+< Rattach { tag=0 qid={ type=(DIR) vers=1 path=1 } }
+> Twalk { tag=0 fid=0 newfid=0 nwname=1 wname=[ "shutdown" ] }
+< Rwalk { tag=0 nwqid=1 wqid=[ { type=(APPEND) vers=1 path=5 } ] }
+> Topen { tag=0 fid=0 mode=(MODE_WRITE) }
+< Ropen { tag=0 qid={ type=(APPEND) vers=1 path=5 } iounit=0 }
+> Twrite { tag=0 fid=0 offset=0 count=2 data="1\n" }
+< Rwrite { tag=0 count=2 }