// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server import ( "testing" "time" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" ) func TestStateStoreDelegOfAndGrace(t *testing.T) { s := newStateStore("") fh := nfsfs.Handle([]byte("delegated-file")) st, status := s.open(fh, 7, []byte("owner"), nfs4.ShareAccessRead, 0) if status != nfs4.ErrOK { t.Fatalf("open: %d", status) } if _, ok := s.delegOf(st); ok { t.Fatal("no delegation bound yet") } deleg := nfs4.Stateid{1, 'D', 'E', 'L'} s.bindDelegation(st, deleg) got, ok := s.delegOf(st) if !ok || got != deleg { t.Fatalf("delegOf %v %v", got, ok) } // Only the owning client may close or downgrade the open; a foreign // stateid use is BAD_STATEID even when the stateid itself is live. if _, status := s.close(st, 8); status != nfs4.ErrBadStateid { t.Fatalf("a foreign close: %d", status) } if _, status := s.downgrade(st, 8, nfs4.ShareAccessRead, 0); status != nfs4.ErrBadStateid { t.Fatalf("a foreign downgrade: %d", status) } if _, status := s.checkStateid(st, fh, 8); status != nfs4.ErrBadStateid { t.Fatalf("a foreign stateid check: %d", status) } if _, status := s.close(st, 7); status != nfs4.ErrOK { t.Fatalf("the owner's close: %d", status) } // The grace window: a reclaim inside the window passes once, the // second announcement and every reclaim after it fail with the // error that names the reason. g := newGrace(time.Minute) if !g.active(time.Now()) { t.Fatal("grace inactive at birth") } if ok, status := g.reclaimOKFor(9, time.Now()); !ok || status != 0 { t.Fatalf("reclaim refused inside the window: %d", status) } if !g.complete(9, time.Now()) { t.Fatal("first complete refused") } if g.complete(9, time.Now()) { t.Fatal("second complete accepted") } if ok, status := g.reclaimOKFor(9, time.Now()); ok || status != nfs4.ErrCompleteAlready { t.Fatalf("done client still reclaimable: %d", status) } if ok, status := g.reclaimOKFor(9, time.Now().Add(time.Hour)); ok || status != nfs4.ErrNoGrace { t.Fatalf("reclaim accepted after the window: %d", status) } // FREE_STATEID removes the owner state the byOwn index points at, // and only for the owner. locks := newLockStore() lst, status := locks.lock(fh, 5, []byte("lock-owner"), true, 0, 10) if status != nfs4.ErrOK { t.Fatalf("lock: %d", status) } if status := locks.freeStateid(lst, 6); status != nfs4.ErrBadStateid { t.Fatalf("a foreign free: %d", status) } if status := locks.freeStateid(lst, 5); status != nfs4.ErrLocksHeld { t.Fatalf("free with ranges: %d", status) } if _, status := locks.unlock(lst, 6, 0, 0); status != nfs4.ErrBadStateid { t.Fatalf("a foreign unlock: %d", status) } if _, status := locks.unlock(lst, 5, 0, 0); status != nfs4.ErrOK { t.Fatalf("unlock: %d", status) } if status := locks.freeStateid(lst, 5); status != nfs4.ErrOK { t.Fatalf("free: %d", status) } locks.releaseOwner(5, []byte("lock-owner")) } // Re-locking a range the owner already holds merges into one range // instead of piling entries up, RFC 8881 section 18.10. func TestLockRangesMerge(t *testing.T) { locks := newLockStore() fh := nfsfs.Handle([]byte("merge-file")) if _, status := locks.lock(fh, 1, []byte("o"), true, 0, 10); status != nfs4.ErrOK { t.Fatalf("first lock: %d", status) } if _, status := locks.lock(fh, 1, []byte("o"), true, 10, 10); status != nfs4.ErrOK { t.Fatalf("second lock: %d", status) } if _, status := locks.lock(fh, 1, []byte("o"), true, 20, 10); status != nfs4.ErrOK { t.Fatalf("third lock: %d", status) } // The merged span answers conflicts as one range, 0 to 30. denied, dstatus := locks.test(fh, 2, []byte("other"), true, 5, 1) if dstatus != nfs4.ErrDenied || denied.Offset != 0 || denied.Length != 30 { t.Fatalf("the merged range answers conflicts as one: %+v %d", denied, dstatus) } // A read lock inside the write span carves its bytes out of the write // form: a read probe over the carved bytes passes, one over the // neighbouring write bytes still conflicts. if _, status := locks.lock(fh, 1, []byte("o"), false, 5, 1); status != nfs4.ErrOK { t.Fatalf("read inside write: %d", status) } if _, dstatus := locks.test(fh, 2, []byte("other"), false, 5, 1); dstatus != nfs4.ErrOK { t.Fatalf("a read probe over the carved read range conflicted: %d", dstatus) } if _, dstatus := locks.test(fh, 2, []byte("other"), false, 6, 1); dstatus != nfs4.ErrDenied { t.Fatalf("a read probe over the neighbouring write range passed: %d", dstatus) } }