192 lines
5.1 KiB
Go
192 lines
5.1 KiB
Go
package dream
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeLock writes a lock file with the given pid and started_at directly, for
|
|
// tests that need to simulate a pre-existing (possibly stale) lock.
|
|
func writeLock(t *testing.T, root string, pid int, startedAt time.Time) string {
|
|
t.Helper()
|
|
dir := filepath.Join(root, "global", "dream")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(dir, "dream.lock")
|
|
data, err := json.Marshal(lockInfo{PID: pid, StartedAt: startedAt})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestAcquireLockMutualExclusion(t *testing.T) {
|
|
root := t.TempDir()
|
|
l1, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("first AcquireLock: %v", err)
|
|
}
|
|
defer l1.Release()
|
|
|
|
// A second acquire while the first is live must fail with ErrLocked.
|
|
l2, err := AcquireLock(root)
|
|
if !errors.Is(err, ErrLocked) {
|
|
t.Fatalf("second AcquireLock err = %v, want ErrLocked", err)
|
|
}
|
|
if l2 != nil {
|
|
t.Errorf("second AcquireLock returned non-nil Lock alongside error")
|
|
}
|
|
}
|
|
|
|
func TestAcquireLockCreatesFileWithBody(t *testing.T) {
|
|
root := t.TempDir()
|
|
l, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("AcquireLock: %v", err)
|
|
}
|
|
defer l.Release()
|
|
|
|
path := filepath.Join(root, "global", "dream", "dream.lock")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read lock file: %v", err)
|
|
}
|
|
var info lockInfo
|
|
if err := json.Unmarshal(data, &info); err != nil {
|
|
t.Fatalf("lock body not valid JSON: %v", err)
|
|
}
|
|
if info.PID != os.Getpid() {
|
|
t.Errorf("lock pid = %d, want %d", info.PID, os.Getpid())
|
|
}
|
|
if info.StartedAt.IsZero() {
|
|
t.Errorf("lock started_at is zero, want current time")
|
|
}
|
|
}
|
|
|
|
func TestAcquireLockDoesNotTouchState(t *testing.T) {
|
|
root := t.TempDir()
|
|
l, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("AcquireLock: %v", err)
|
|
}
|
|
defer l.Release()
|
|
if _, err := os.Stat(filepath.Join(root, "global", "dream", "state.json")); !os.IsNotExist(err) {
|
|
t.Errorf("AcquireLock created/left state.json (err=%v); lock must be a separate file", err)
|
|
}
|
|
}
|
|
|
|
func TestAcquireLockStaleTakeover(t *testing.T) {
|
|
root := t.TempDir()
|
|
// Existing lock older than staleAfter → takeable.
|
|
writeLock(t, root, 99999, time.Now().Add(-DefaultStaleAfter-time.Minute))
|
|
|
|
l, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("AcquireLock over stale lock err = %v, want takeover success", err)
|
|
}
|
|
defer l.Release()
|
|
|
|
// The lock body should now reflect our pid.
|
|
data, err := os.ReadFile(filepath.Join(root, "global", "dream", "dream.lock"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var info lockInfo
|
|
if err := json.Unmarshal(data, &info); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.PID != os.Getpid() {
|
|
t.Errorf("after takeover pid = %d, want %d (our pid)", info.PID, os.Getpid())
|
|
}
|
|
}
|
|
|
|
func TestAcquireLockFreshLockNotTakeable(t *testing.T) {
|
|
root := t.TempDir()
|
|
// A recently-started lock is live, not stale.
|
|
writeLock(t, root, 99999, time.Now().Add(-time.Minute))
|
|
|
|
if _, err := AcquireLock(root); !errors.Is(err, ErrLocked) {
|
|
t.Fatalf("AcquireLock over fresh lock err = %v, want ErrLocked", err)
|
|
}
|
|
}
|
|
|
|
func TestAcquireLockMalformedTakeover(t *testing.T) {
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "global", "dream")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "dream.lock"), []byte("{garbage not json"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
l, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("AcquireLock over malformed lock err = %v, want takeover success", err)
|
|
}
|
|
defer l.Release()
|
|
}
|
|
|
|
func TestReleaseAndReacquire(t *testing.T) {
|
|
root := t.TempDir()
|
|
l1, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("first AcquireLock: %v", err)
|
|
}
|
|
if err := l1.Release(); err != nil {
|
|
t.Fatalf("Release: %v", err)
|
|
}
|
|
// File must be gone after release.
|
|
if _, err := os.Stat(filepath.Join(root, "global", "dream", "dream.lock")); !os.IsNotExist(err) {
|
|
t.Errorf("lock file still present after Release (err=%v)", err)
|
|
}
|
|
// Re-acquire must succeed now that the lock is free.
|
|
l2, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("re-acquire after release: %v", err)
|
|
}
|
|
defer l2.Release()
|
|
}
|
|
|
|
func TestReleaseDoubleCallSafe(t *testing.T) {
|
|
root := t.TempDir()
|
|
l, err := AcquireLock(root)
|
|
if err != nil {
|
|
t.Fatalf("AcquireLock: %v", err)
|
|
}
|
|
if err := l.Release(); err != nil {
|
|
t.Fatalf("first Release: %v", err)
|
|
}
|
|
// Second Release (and even after the file is gone) must not panic or error.
|
|
if err := l.Release(); err != nil {
|
|
t.Errorf("second Release err = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestReleaseNilSafe(t *testing.T) {
|
|
var l *Lock
|
|
if err := l.Release(); err != nil {
|
|
t.Errorf("nil Lock Release err = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestStaleLockBoundary(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := writeLock(t, root, 1, time.Unix(0, 0).UTC())
|
|
now := time.Unix(0, 0).UTC()
|
|
if staleLock(path, now.Add(DefaultStaleAfter-time.Second)) {
|
|
t.Errorf("lock within staleAfter reported stale")
|
|
}
|
|
if !staleLock(path, now.Add(DefaultStaleAfter+time.Second)) {
|
|
t.Errorf("lock past staleAfter reported not stale")
|
|
}
|
|
}
|