lock_test.go

  1package lock
  2
  3import (
  4	"context"
  5	"errors"
  6	"path/filepath"
  7	"sync"
  8	"testing"
  9	"time"
 10
 11	"github.com/stretchr/testify/require"
 12)
 13
 14func TestTryFile_AcquiresWhenFree(t *testing.T) {
 15	t.Parallel()
 16	path := filepath.Join(t.TempDir(), "test.lock")
 17
 18	release, err := TryFile(path)
 19	require.NoError(t, err)
 20	require.NotNil(t, release)
 21	release()
 22}
 23
 24func TestTryFile_ReturnsErrContendedWhenHeld(t *testing.T) {
 25	t.Parallel()
 26	path := filepath.Join(t.TempDir(), "test.lock")
 27
 28	release, err := TryFile(path)
 29	require.NoError(t, err)
 30	t.Cleanup(release)
 31
 32	_, err = TryFile(path)
 33	require.ErrorIs(t, err, ErrContended)
 34}
 35
 36func TestTryFile_ReacquireAfterRelease(t *testing.T) {
 37	t.Parallel()
 38	path := filepath.Join(t.TempDir(), "test.lock")
 39
 40	release, err := TryFile(path)
 41	require.NoError(t, err)
 42	release()
 43
 44	release2, err := TryFile(path)
 45	require.NoError(t, err)
 46	t.Cleanup(release2)
 47}
 48
 49func TestFile_AcquiresWhenFree(t *testing.T) {
 50	t.Parallel()
 51	path := filepath.Join(t.TempDir(), "test.lock")
 52
 53	release, err := File(context.Background(), path)
 54	require.NoError(t, err)
 55	t.Cleanup(release)
 56}
 57
 58func TestFile_BlocksThenSucceeds(t *testing.T) {
 59	t.Parallel()
 60	path := filepath.Join(t.TempDir(), "test.lock")
 61
 62	release, err := TryFile(path)
 63	require.NoError(t, err)
 64
 65	// Release the lock after a short delay so the blocking acquirer
 66	// can complete within the test timeout.
 67	go func() {
 68		time.Sleep(150 * time.Millisecond)
 69		release()
 70	}()
 71
 72	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
 73	defer cancel()
 74	release2, err := File(ctx, path)
 75	require.NoError(t, err, "should acquire after first releases")
 76	release2()
 77}
 78
 79func TestFile_RespectsContextDeadline(t *testing.T) {
 80	t.Parallel()
 81	path := filepath.Join(t.TempDir(), "test.lock")
 82
 83	release, err := TryFile(path)
 84	require.NoError(t, err)
 85	t.Cleanup(release)
 86
 87	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
 88	defer cancel()
 89	start := time.Now()
 90	_, err = File(ctx, path)
 91	elapsed := time.Since(start)
 92	require.Error(t, err)
 93	require.True(t, errors.Is(err, context.DeadlineExceeded), "expected deadline exceeded, got %v", err)
 94	require.Less(t, elapsed, 1*time.Second, "should return promptly after deadline")
 95}
 96
 97func TestFile_RespectsContextCancellation(t *testing.T) {
 98	t.Parallel()
 99	path := filepath.Join(t.TempDir(), "test.lock")
100
101	release, err := TryFile(path)
102	require.NoError(t, err)
103	t.Cleanup(release)
104
105	ctx, cancel := context.WithCancel(context.Background())
106	done := make(chan error, 1)
107	go func() {
108		_, err := File(ctx, path)
109		done <- err
110	}()
111
112	time.Sleep(50 * time.Millisecond)
113	cancel()
114
115	select {
116	case err := <-done:
117		require.ErrorIs(t, err, context.Canceled)
118	case <-time.After(2 * time.Second):
119		t.Fatal("File did not return after context cancellation")
120	}
121}
122
123// TestFile_ConcurrentAcquirers verifies that multiple blocking acquirers
124// queue up correctly: each gets the lock in turn, exactly one at a time.
125func TestFile_ConcurrentAcquirers(t *testing.T) {
126	t.Parallel()
127	path := filepath.Join(t.TempDir(), "test.lock")
128
129	const n = 5
130	var (
131		mu       sync.Mutex
132		inside   int
133		maxSeen  int
134		finished int
135	)
136	var wg sync.WaitGroup
137	wg.Add(n)
138	for range n {
139		go func() {
140			defer wg.Done()
141			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
142			defer cancel()
143			release, err := File(ctx, path)
144			if err != nil {
145				t.Errorf("acquire failed: %v", err)
146				return
147			}
148			mu.Lock()
149			inside++
150			if inside > maxSeen {
151				maxSeen = inside
152			}
153			mu.Unlock()
154
155			time.Sleep(20 * time.Millisecond)
156
157			mu.Lock()
158			inside--
159			finished++
160			mu.Unlock()
161			release()
162		}()
163	}
164	wg.Wait()
165
166	require.Equal(t, n, finished)
167	require.Equal(t, 1, maxSeen, "lock must be mutually exclusive")
168}