aboutsummaryrefslogtreecommitdiff
path: root/internal/lock/lock_test.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-12 20:14:47 +0200
commit3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch)
treeec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /internal/lock/lock_test.go
parent24a84671ace373ae331fa83a1ff484990f4dff0e (diff)
downloadkrino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.tar.gz
krino-3f8679be9373ee7508d512dfdfc1dda0839c7f90.zip
krino: acting — trash, journal, apply, lock, review, undo
Diffstat (limited to 'internal/lock/lock_test.go')
-rw-r--r--internal/lock/lock_test.go124
1 files changed, 124 insertions, 0 deletions
diff --git a/internal/lock/lock_test.go b/internal/lock/lock_test.go
new file mode 100644
index 0000000..fca4d76
--- /dev/null
+++ b/internal/lock/lock_test.go
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package lock
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+)
+
+func TestAcquireAndRelease(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "state", "dl.lock")
+ l, err := Acquire(context.Background(), path, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(path); err != nil {
+ t.Errorf("lock file missing: %v", err)
+ }
+ if b, _ := os.ReadFile(path); !strings.Contains(string(b), fmt.Sprint(os.Getpid())) {
+ t.Errorf("lock file does not name the holder's pid: %q", b)
+ }
+ if err := l.Release(); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Error("Release left the lock file behind")
+ }
+ if err := l.Release(); err != nil {
+ t.Errorf("a second Release must be harmless: %v", err)
+ }
+}
+
+func TestAcquireFailsWhenHeldAndNotWaiting(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "dl.lock")
+ first, err := Acquire(context.Background(), path, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer first.Release()
+ if _, err := Acquire(context.Background(), path, false); !errors.Is(err, ErrHeld) {
+ t.Fatalf("second Acquire err = %v, want ErrHeld", err)
+ }
+}
+
+func TestAcquireWaitsUntilReleased(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "dl.lock")
+ first, err := Acquire(context.Background(), path, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ go func() {
+ time.Sleep(150 * time.Millisecond)
+ first.Release()
+ }()
+ start := time.Now()
+ second, err := Acquire(context.Background(), path, true)
+ if err != nil {
+ t.Fatalf("waiting Acquire failed: %v", err)
+ }
+ defer second.Release()
+ if time.Since(start) < 100*time.Millisecond {
+ t.Error("Acquire returned before the first holder released")
+ }
+}
+
+// TestAcquireRespectsContextCancellation is fix round 2026-09-12/item 3: a
+// waiting Acquire must not ignore an interrupt - a cancelled ctx must return
+// promptly with ctx.Err(), not poll forever. The unfixed code HANGS rather
+// than fails here, so the wait for Acquire's result is itself bounded with
+// its own hard timeout: a regression must fail this test, not hang the
+// whole suite.
+func TestAcquireRespectsContextCancellation(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "dl.lock")
+ held, err := Acquire(context.Background(), path, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer held.Release()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+ defer cancel()
+
+ result := make(chan error, 1)
+ start := time.Now()
+ go func() {
+ _, err := Acquire(ctx, path, true)
+ result <- err
+ }()
+
+ select {
+ case err := <-result:
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("Acquire err = %v, want context.DeadlineExceeded", err)
+ }
+ if elapsed := time.Since(start); elapsed > 500*time.Millisecond {
+ t.Errorf("Acquire took %v to notice cancellation, want well under a second", elapsed)
+ }
+ case <-time.After(2 * time.Second):
+ t.Fatal("Acquire ignored context cancellation and is still blocked")
+ }
+}
+
+// TestStaleLockIsTakenOver: a lock naming a pid that is not running must not
+// wedge krino - a machine that lost power mid-run would need manual cleanup.
+func TestStaleLockIsTakenOver(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "dl.lock")
+ if err := os.WriteFile(path, []byte("pid 4294967000\nstarted 2020-01-01T00:00:00Z\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ l, err := Acquire(context.Background(), path, false)
+ if err != nil {
+ t.Fatalf("a stale lock blocked Acquire: %v", err)
+ }
+ defer l.Release()
+ if !l.TookOverStale {
+ t.Error("the takeover was not reported to the caller")
+ }
+}