Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
126 lines
4.5 KiB
Go
126 lines
4.5 KiB
Go
// 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)
|
|
}
|
|
}
|