package staros import ( "errors" "os" "path/filepath" "runtime" "testing" "time" ) func Test_FileLock(t *testing.T) { filename := filepath.Join(t.TempDir(), "test.file") lock := NewFileLock(filename) lock2 := NewFileLock(filename) if err := lock.LockNoBlocking(false); err != nil { t.Fatal(err) } if err := lock2.LockNoBlocking(false); err != nil { t.Fatal(err) } if err := lock.Unlock(); err != nil { t.Fatal(err) } if err := lock2.Unlock(); err != nil { t.Fatal(err) } if err := lock2.LockNoBlocking(true); err != nil { t.Fatal(err) } if err := lock2.Unlock(); err != nil { t.Fatal(err) } _ = os.Remove(filename) } func TestFileLockExclusiveConflictTimeout(t *testing.T) { filename := filepath.Join(t.TempDir(), "timeout.file") lock := NewFileLock(filename) contender := NewFileLock(filename) if err := lock.Lock(true); err != nil { t.Fatal(err) } defer lock.Unlock() if err := contender.LockNoBlocking(true); !errors.Is(err, ERR_ALREADY_LOCKED) { if err == nil { _ = contender.Unlock() } t.Fatalf("expected non-blocking exclusive lock conflict, got %v", err) } start := time.Now() if err := contender.LockWithTimeout(50*time.Millisecond, true); !errors.Is(err, ERR_TIMEOUT) { if err == nil { _ = contender.Unlock() } t.Fatalf("expected exclusive lock timeout, got %v", err) } if elapsed := time.Since(start); elapsed > time.Second { t.Fatalf("lock timeout took too long: %s", elapsed) } if err := lock.Unlock(); err != nil { t.Fatal(err) } if err := contender.LockWithTimeout(time.Second, true); err != nil { t.Fatalf("expected lock after owner unlock, got %v", err) } if err := contender.Unlock(); err != nil { t.Fatal(err) } } func TestFileLockUnlockWithoutSuccessfulLock(t *testing.T) { filename := filepath.Join(t.TempDir(), "unlock-state.file") lock := NewFileLock(filename) if err := lock.Unlock(); !errors.Is(err, errFileLockNotLocked) { t.Fatalf("expected unlock without lock error, got %v", err) } owner := NewFileLock(filename) contender := NewFileLock(filename) if err := owner.Lock(true); err != nil { t.Fatal(err) } defer owner.Unlock() if err := contender.LockNoBlocking(true); !errors.Is(err, ERR_ALREADY_LOCKED) { if err == nil { _ = contender.Unlock() } t.Fatalf("expected lock conflict, got %v", err) } if err := contender.Unlock(); !errors.Is(err, errFileLockNotLocked) { t.Fatalf("failed lock attempt should not be unlockable, got %v", err) } if err := owner.Unlock(); err != nil { t.Fatal(err) } } func TestFileLockRejectsSecondLockOnSameObject(t *testing.T) { filename := filepath.Join(t.TempDir(), "same-object.file") lock := NewFileLock(filename) if err := lock.Lock(true); err != nil { t.Fatal(err) } defer lock.Unlock() for name, fn := range map[string]func() error{ "Lock": func() error { return lock.Lock(true) }, "LockNoBlocking": func() error { return lock.LockNoBlocking(true) }, "LockWithTimeout": func() error { return lock.LockWithTimeout(time.Second, true) }, } { if err := fn(); !errors.Is(err, ERR_ALREADY_LOCKED) { if err == nil { _ = lock.Unlock() } t.Fatalf("expected %s on same lock object to reject second lock, got %v", name, err) } } } func TestGetFileCreationTimeLinuxUnavailable(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("linux-only creation time fallback") } filename := filepath.Join(t.TempDir(), "creation-time.file") if err := os.WriteFile(filename, []byte("demo"), 0o644); err != nil { t.Fatal(err) } info, err := os.Stat(filename) if err != nil { t.Fatal(err) } if got := GetFileCreationTime(info); !got.IsZero() { t.Fatalf("linux FileInfo should not report synthetic creation time, got %s", got) } }