[PATCH pynfs 02/13] server41tests: test COPY with non-zero offsets
Jeff Layton <[email protected]>
| Newsgroups | gmane.linux.nfs |
|---|---|
| Message-ID | <[email protected]> |
Add testCopyWithOffset (COPY2) which copies 4KB from source offset 1024 to destination offset 512, then reads back the destination to verify the data landed at the correct position. Signed-off-by: Jeff Layton <[email protected]> --- CLAUDE.md | 100 +++++++++++++++++++++ ...ts-add-a-test-for-rename-within-a-dir-wit.patch | 55 ++++++++++++ nfs4.1/server41tests/st_copy.py | 25 ++++++ 3 files changed, 180 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000000..91a499884990 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What is pynfs? + +pynfs is an NFS protocol conformance testing framework written in Python. It tests NFSv4.0 and NFSv4.1/4.2 servers by sending crafted NFS COMPOUND operations over RPC and verifying responses. Tests require a live NFS server with an exported filesystem. + +## Build + +```bash +# Install dependencies (Fedora) +yum install krb5-devel python3-devel swig python3-gssapi python3-ply +pip install xdrlib3 + +# Build (generates XDR code from .x files -- must be done before running tests) +./setup.py build +``` + +Build order matters: xdr → rpc → nfs4.1 → nfs4.0. The top-level setup.py handles this. + +## Running Tests + +Tests use a **custom test framework** (not pytest). They require a live NFS server. + +```bash +cd nfs4.1 + +# First run: create test directory tree on server +./testserver.py SERVER:/export --maketree -v all + +# Run all tests +./testserver.py SERVER:/export -v all + +# Run a single test by code +./testserver.py SERVER:/export OPEN1 + +# Run a flag group +./testserver.py SERVER:/export dirdeleg + +# Exclude tests: prefix with "no" +./testserver.py SERVER:/export all nodirdeleg +./testserver.py SERVER:/export dirdeleg noDIRDELEG3 + +# Run with dependencies auto-resolved +./testserver.py SERVER:/export --rundeps DIRDELEG5 + +# Skip initial tree cleanup (faster for tests that don't need it) +./testserver.py SERVER:/export --noinit EXID1 +``` + +NFSv4.0 tests work the same way from the `nfs4.0/` directory. + +## Architecture + +- **`xdr/`** — XDR parser/code generator (`xdrgen.py` uses PLY). Generates `*_const.py`, `*_type.py`, `*_pack.py` from `.x` definition files. These generated files are gitignored. +- **`rpc/`** — Shared RPC client library with AUTH_SYS and RPCSEC_GSS support. +- **`nfs4.1/`** — NFSv4.1/4.2 test suite and utilities. Active development happens here. +- **`nfs4.0/`** — NFSv4.0 test suite. Older codebase with its own copy of rpc/testmod in `lib/`. +- **`nfs4.1/testmod.py`** — The custom test framework engine (discovery, dependencies, execution). +- **`nfs4.1/nfs4client.py`** — NFS4 client implementation built on RPC. +- **`nfs4.1/nfs_ops.py`** — Operation factory (`op.open()`, `op.putfh()`, `op.rename()`, etc.). + +## Writing Tests (NFSv4.1) + +Tests live in `nfs4.1/server41tests/st_*.py`. Each test is a function: + +```python +def testMyFeature(t, env): + """Describe what this test verifies + + FLAGS: myfeature all + CODE: MYFEAT1 + DEPEND: MYFEAT0 + """ + sess = env.c1.new_client_session(env.testname(t)) + res = sess.compound([op.putrootfh()]) + check(res) +``` + +Key conventions: +- Function name starts with `test`, takes `(t, env)`. +- **CODE** (required): unique identifier like `OPEN1`, `DIRDELEG5`. +- **FLAGS**: space-separated group tags. Include `all` for general tests. +- **DEPEND**: test codes that must pass first. +- **VERS**: minor version range (e.g., `1-2`). +- `check(res, NFS4_OK)` is the primary assertion — compares compound result status. +- `t.fail("msg")` signals failure; `t.pass_warn("msg")` for warnings; `t.fail_support("msg")` for unsupported features. +- New test modules must be added to `server41tests/__init__.py`'s `__all__` list. +- Environment helpers in `server41tests/environment.py`: `create_file()`, `open_file()`, `close_file()`, `clean_dir()`, `do_readdir()`, etc. + +## Contributing + +Patches go to [email protected] via `git format-patch` / `git send-email`. Commits should be signed off (`git commit -s`). Follow Linux kernel patch submission conventions. + +## Notes + +- The NFS server under test must allow high-port connections (`insecure` export option on Linux). +- `use_local.py` in each package manipulates `sys.path` so tests can run directly from the source tree without installation. +- Test results are not authoritative protocol statements — consult the RFCs if a server fails a test. diff --git a/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch b/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch new file mode 100644 index 000000000000..7e855592da85 --- /dev/null +++ b/nfs4.1/0001-server41tests-add-a-test-for-rename-within-a-dir-wit.patch @@ -0,0 +1,55 @@ +From 0ce942390b36e00547ea9ea56afdc449a9a50699 Mon Sep 17 00:00:00 2001 +From: Jeff Layton <[email protected]> +Date: Wed, 28 May 2025 12:10:22 -0400 +Subject: [PATCH] server41tests: add a test for rename within a dir with a + delegation + +Signed-off-by: Jeff Layton <[email protected]> +--- + nfs4.1/server41tests/st_dir_deleg.py | 33 ++++++++++++++++++++++++++++ + 1 file changed, 33 insertions(+) + +diff --git a/nfs4.1/server41tests/st_dir_deleg.py b/nfs4.1/server41tests/st_dir_deleg.py +index 1fbeb6e0efb9..505e46680197 100644 +--- a/nfs4.1/server41tests/st_dir_deleg.py ++++ b/nfs4.1/server41tests/st_dir_deleg.py +@@ -102,3 +102,36 @@ def testDirDelegRemove(t, env): + + if (not completed): + fail("I didn't receive a CB_NOTIFY from the server!") ++ ++def testDirDelegRename(t, env): ++ """Create a dir_deleg that accepts notification of RENAME events ++ ++ FLAGS: dirdeleg all ++ CODE: DIRDELEG2 ++ """ ++ c = env.c1 ++ recall = threading.Event() ++ notify = threading.Event() ++ sess1, fh, deleg = _getDirDeleg(t, env, [NOTIFY4_RENAME_ENTRY], recall, notify) ++ ++ claim = open_claim4(CLAIM_NULL, env.testname(t)) ++ owner = open_owner4(0, b"owner") ++ how = openflag4(OPEN4_CREATE, createhow4(GUARDED4, {FATTR4_SIZE:0})) ++ open_op = [ op.putfh(fh), op.open(0, OPEN4_SHARE_ACCESS_WRITE, ++ OPEN4_SHARE_DENY_NONE, owner, how, claim) ] ++ res = sess1.compound(open_op) ++ check(res) ++ ++ sess2 = c.new_client_session(b"%s_2" % env.testname(t)) ++ topdir = c.homedir + [t.code.encode('utf8')] ++ oldpath = b"%s/%s" % (topdir, env.testname(t)) ++ newpath = b"%s_2" % oldpath) ++ res = rename_obj(sess2, oldpath, newpath) ++ check(res) ++ ++ completed = notify.wait(5) ++ ops = [ op.putfh(fh), op.delegreturn(deleg) ] ++ res = sess1.compound(ops) ++ ++ if (not completed): ++ fail("I didn't receive a CB_NOTIFY from the server!") +-- +2.49.0 + diff --git a/nfs4.1/server41tests/st_copy.py b/nfs4.1/server41tests/st_copy.py index c7e48adf6fbe..bfc64bbe1584 100644 --- a/nfs4.1/server41tests/st_copy.py +++ b/nfs4.1/server41tests/st_copy.py @@ -97,6 +97,31 @@ def testSyncCopy(t, env): _verify_data(sess, dst_fh, dst_stateid, data) +def testCopyWithOffset(t, env): + """copy with non-zero source and destination offsets + + FLAGS: copy + CODE: COPY2 + """ + sess = env.c1.new_client_session(env.testname(t)) + src_fh, src_stateid = _create_and_open(sess, env.testname(t)) + data = b"\x00" * 1024 + b"B" * 4096 + b"\x00" * 1024 + _write_data(sess, src_fh, src_stateid, data) + + dst_fh, dst_stateid = _create_and_open(sess, env.testname(t) + b"_dst") + + res = _do_copy(sess, src_fh, src_stateid, dst_fh, dst_stateid, + src_offset=1024, dst_offset=512, count=4096, synchronous=1) + check(res) + cr = res.resarray[-1] + if cr.cr_response.wr_count != 4096: + fail("Expected to copy 4096 bytes, got %d" % cr.cr_response.wr_count) + + res = read_file(sess, dst_fh, 512, 4096, dst_stateid) + check(res) + if res.data != b"B" * 4096: + fail("Destination data at offset 512 does not match expected content") + def testZeroLengthCopy(t, env): """test that zero-length copy copies to EOF -- 2.55.0