Files

126 lines
4.5 KiB
Go
Raw Permalink Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
}
}