// 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") } } // TestRunningRejectsPidsBeyondTheKernelsRange: a pid wider than 32 bits in a // damaged lock file is cut to its low bits by kill(2), so without a range // check it names some other process - here this very one - and a stale lock // would never be taken over. func TestRunningRejectsPidsBeyondTheKernelsRange(t *testing.T) { if running(os.Getpid() + 1<<32) { t.Error("running(pid + 2^32) is true: the pid was truncated to this process") } if !running(os.Getpid()) { t.Error("running(own pid) is false") } }