diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-12 20:14:47 +0200 |
| commit | 3f8679be9373ee7508d512dfdfc1dda0839c7f90 (patch) | |
| tree | ec02eb075f6c4e90f21baa2fe674e86a2f7f6a62 /internal/lock/lock_test.go | |
| parent | 24a84671ace373ae331fa83a1ff484990f4dff0e (diff) | |
| download | krino-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.go | 124 |
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") + } +} |
