返回 DeepSeek-Reasonix
lease_test.go
根目录 / internal / workspacelease / lease_test.go
1 package workspacelease
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "runtime"
10 "sync/atomic"
11 "testing"
12 "time"
13 )
14
15 func TestWorkspaceLeaseHelperProcess(t *testing.T) {
16 if os.Getenv("REASONIX_WORKSPACE_LEASE_HELPER") != "1" {
17 return
18 }
19 root := os.Getenv("REASONIX_WORKSPACE_LEASE_ROOT")
20 locks := os.Getenv("REASONIX_WORKSPACE_LEASE_DIR")
21 ready := os.Getenv("REASONIX_WORKSPACE_LEASE_READY")
22 o, err := New(root, locks, nil)
23 if err != nil {
24 t.Fatal(err)
25 }
26 o.BeginRun()
27 if err := o.AcquireWrite(context.Background()); err != nil {
28 t.Fatal(err)
29 }
30 if err := os.WriteFile(ready, []byte("ready"), 0o600); err != nil {
31 t.Fatal(err)
32 }
33 for {
34 time.Sleep(time.Hour)
35 }
36 }
37
38 func TestCanonicalWorkspaceResolvesSymlink(t *testing.T) {
39 if runtime.GOOS == "windows" {
40 t.Skip("symlink creation requires privileges on some Windows builders")
41 }
42 real := t.TempDir()
43 link := filepath.Join(t.TempDir(), "workspace-link")
44 if err := os.Symlink(real, link); err != nil {
45 t.Fatal(err)
46 }
47 got, err := CanonicalWorkspace(filepath.Join(link, "."))
48 if err != nil {
49 t.Fatal(err)
50 }
51 want, err := CanonicalWorkspace(real)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if got != want {
56 t.Fatalf("canonical identities differ: got %q want %q", got, want)
57 }
58 }
59
60 func TestCanonicalWorkspaceFoldsRepositorySubdirectoriesWithoutGitBinary(t *testing.T) {
61 repo := t.TempDir()
62 if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
63 t.Fatal(err)
64 }
65 subdir := filepath.Join(repo, "packages", "app")
66 if err := os.MkdirAll(subdir, 0o755); err != nil {
67 t.Fatal(err)
68 }
69 rootIdentity, err := CanonicalWorkspace(repo)
70 if err != nil {
71 t.Fatal(err)
72 }
73 subdirIdentity, err := CanonicalWorkspace(subdir)
74 if err != nil {
75 t.Fatal(err)
76 }
77 if subdirIdentity != rootIdentity {
78 t.Fatalf("repository subdirectory identity = %q, want root identity %q", subdirIdentity, rootIdentity)
79 }
80 }
81
82 func TestCanonicalWorkspaceKeepsLinkedWorktreesIndependent(t *testing.T) {
83 parent := t.TempDir()
84 first := filepath.Join(parent, "worktree-one")
85 second := filepath.Join(parent, "worktree-two")
86 for _, root := range []string{first, second} {
87 if err := os.MkdirAll(root, 0o755); err != nil {
88 t.Fatal(err)
89 }
90 if err := os.WriteFile(filepath.Join(root, ".git"), []byte("gitdir: ../common\n"), 0o600); err != nil {
91 t.Fatal(err)
92 }
93 }
94 firstIdentity, err := CanonicalWorkspace(first)
95 if err != nil {
96 t.Fatal(err)
97 }
98 secondIdentity, err := CanonicalWorkspace(second)
99 if err != nil {
100 t.Fatal(err)
101 }
102 if firstIdentity == secondIdentity {
103 t.Fatalf("linked worktrees shared identity %q", firstIdentity)
104 }
105 }
106
107 func TestRepositoryRootAndSubdirectoryOwnersSerialize(t *testing.T) {
108 repo, locks := t.TempDir(), t.TempDir()
109 if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
110 t.Fatal(err)
111 }
112 subdir := filepath.Join(repo, "nested", "project")
113 if err := os.MkdirAll(subdir, 0o755); err != nil {
114 t.Fatal(err)
115 }
116 rootOwner, err := New(repo, locks, nil)
117 if err != nil {
118 t.Fatal(err)
119 }
120 subdirOwner, err := New(subdir, locks, nil)
121 if err != nil {
122 t.Fatal(err)
123 }
124 rootOwner.BeginRun()
125 subdirOwner.BeginRun()
126 if err := rootOwner.AcquireWrite(context.Background()); err != nil {
127 t.Fatal(err)
128 }
129 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
130 if err := subdirOwner.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
131 cancel()
132 t.Fatalf("repository subdirectory owner acquired independently: %v", err)
133 }
134 cancel()
135 rootOwner.EndRun()
136 if err := subdirOwner.AcquireWrite(context.Background()); err != nil {
137 t.Fatal(err)
138 }
139 subdirOwner.EndRun()
140 }
141
142 func TestOwnersSerializeSameWorkspaceAndNotifyOnce(t *testing.T) {
143 root, locks := t.TempDir(), t.TempDir()
144 first, err := New(root, locks, nil)
145 if err != nil {
146 t.Fatal(err)
147 }
148 var notices atomic.Int32
149 second, err := New(root, locks, func() { notices.Add(1) })
150 if err != nil {
151 t.Fatal(err)
152 }
153 first.BeginRun()
154 second.BeginRun()
155 if err := first.AcquireWrite(context.Background()); err != nil {
156 t.Fatal(err)
157 }
158
159 acquired := make(chan error, 1)
160 go func() { acquired <- second.AcquireWrite(context.Background()) }()
161 select {
162 case err := <-acquired:
163 t.Fatalf("second owner acquired early: %v", err)
164 case <-time.After(100 * time.Millisecond):
165 }
166 first.EndRun()
167 select {
168 case err := <-acquired:
169 if err != nil {
170 t.Fatal(err)
171 }
172 case <-time.After(2 * time.Second):
173 t.Fatal("second owner did not acquire after release")
174 }
175 if got := notices.Load(); got != 1 {
176 t.Fatalf("wait notices = %d, want 1", got)
177 }
178 second.EndRun()
179 }
180
181 func TestStateReportsWaitingAndAcquiredWithoutIdentity(t *testing.T) {
182 root, locks := t.TempDir(), t.TempDir()
183 first, err := New(root, locks, nil)
184 if err != nil {
185 t.Fatal(err)
186 }
187 waiting := make(chan struct{}, 1)
188 second, err := New(root, locks, func() { waiting <- struct{}{} })
189 if err != nil {
190 t.Fatal(err)
191 }
192 first.BeginRun()
193 second.BeginRun()
194 if err := first.AcquireWrite(context.Background()); err != nil {
195 t.Fatal(err)
196 }
197 if got := first.State(); !got.Acquired || got.Waiting {
198 t.Fatalf("first owner state = %+v, want acquired and not waiting", got)
199 }
200 acquired := make(chan error, 1)
201 go func() { acquired <- second.AcquireWrite(context.Background()) }()
202 select {
203 case <-waiting:
204 case <-time.After(2 * time.Second):
205 t.Fatal("second owner did not report waiting")
206 }
207 if got := second.State(); got.Acquired || !got.Waiting {
208 t.Fatalf("second owner state = %+v, want waiting and not acquired", got)
209 }
210 first.EndRun()
211 if err := <-acquired; err != nil {
212 t.Fatal(err)
213 }
214 if got := second.State(); !got.Acquired || got.Waiting {
215 t.Fatalf("second owner state after acquire = %+v, want acquired and not waiting", got)
216 }
217 second.EndRun()
218 }
219
220 func TestIndependentWorkspacesDoNotBlockEachOther(t *testing.T) {
221 locks := t.TempDir()
222 first, err := New(t.TempDir(), locks, nil)
223 if err != nil {
224 t.Fatal(err)
225 }
226 second, err := New(t.TempDir(), locks, nil)
227 if err != nil {
228 t.Fatal(err)
229 }
230 first.BeginRun()
231 second.BeginRun()
232 if err := first.AcquireWrite(context.Background()); err != nil {
233 t.Fatal(err)
234 }
235 ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
236 defer cancel()
237 if err := second.AcquireWrite(ctx); err != nil {
238 t.Fatalf("independent workspace was blocked: %v", err)
239 }
240 first.EndRun()
241 second.EndRun()
242 }
243
244 func TestLeaseMetadataNeverDirtiesWorkspace(t *testing.T) {
245 root, locks := t.TempDir(), t.TempDir()
246 before, err := os.ReadDir(root)
247 if err != nil {
248 t.Fatal(err)
249 }
250 o, err := New(root, locks, nil)
251 if err != nil {
252 t.Fatal(err)
253 }
254 o.BeginRun()
255 if err := o.AcquireWrite(context.Background()); err != nil {
256 t.Fatal(err)
257 }
258 o.EndRun()
259 after, err := os.ReadDir(root)
260 if err != nil {
261 t.Fatal(err)
262 }
263 if len(after) != len(before) {
264 t.Fatalf("workspace entries changed after lease: before=%d after=%d", len(before), len(after))
265 }
266 }
267
268 func TestAcquireIsReentrantWithinOwner(t *testing.T) {
269 o, err := New(t.TempDir(), t.TempDir(), nil)
270 if err != nil {
271 t.Fatal(err)
272 }
273 o.BeginRun()
274 if err := o.AcquireWrite(context.Background()); err != nil {
275 t.Fatal(err)
276 }
277 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
278 defer cancel()
279 if err := o.AcquireWrite(ctx); err != nil {
280 t.Fatalf("re-entrant acquire failed: %v", err)
281 }
282 o.EndRun()
283 }
284
285 func TestCancelledWaitDoesNotLeakLocalLease(t *testing.T) {
286 root, locks := t.TempDir(), t.TempDir()
287 first, _ := New(root, locks, nil)
288 second, _ := New(root, locks, nil)
289 third, _ := New(root, locks, nil)
290 first.BeginRun()
291 second.BeginRun()
292 third.BeginRun()
293 if err := first.AcquireWrite(context.Background()); err != nil {
294 t.Fatal(err)
295 }
296 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
297 defer cancel()
298 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
299 t.Fatalf("cancelled acquire = %v, want deadline", err)
300 }
301 second.EndRun()
302 first.EndRun()
303 if err := third.AcquireWrite(context.Background()); err != nil {
304 t.Fatalf("lease leaked after cancellation: %v", err)
305 }
306 third.EndRun()
307 }
308
309 func TestLeaseWaitsForLastRun(t *testing.T) {
310 root, locks := t.TempDir(), t.TempDir()
311 first, _ := New(root, locks, nil)
312 second, _ := New(root, locks, nil)
313 first.BeginRun()
314 first.BeginRun()
315 second.BeginRun()
316 if err := first.AcquireWrite(context.Background()); err != nil {
317 t.Fatal(err)
318 }
319 first.EndRun()
320 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
321 defer cancel()
322 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
323 t.Fatalf("second acquired before final run ended: %v", err)
324 }
325 first.EndRun()
326 if err := second.AcquireWrite(context.Background()); err != nil {
327 t.Fatal(err)
328 }
329 second.EndRun()
330 }
331
332 func TestBackgroundRetentionOutlivesRun(t *testing.T) {
333 root, locks := t.TempDir(), t.TempDir()
334 first, _ := New(root, locks, nil)
335 second, _ := New(root, locks, nil)
336 first.BeginRun()
337 second.BeginRun()
338 if err := first.AcquireWrite(context.Background()); err != nil {
339 t.Fatal(err)
340 }
341 done := make(chan struct{})
342 first.RetainUntil(done)
343 first.EndRun()
344 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
345 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
346 cancel()
347 t.Fatalf("second acquired while background job was running: %v", err)
348 }
349 cancel()
350 close(done)
351 ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
352 defer cancel()
353 if err := second.AcquireWrite(ctx); err != nil {
354 t.Fatal(err)
355 }
356 second.EndRun()
357 }
358
359 func TestLeaseWaitsForEveryRetainedBackgroundJob(t *testing.T) {
360 root, locks := t.TempDir(), t.TempDir()
361 first, _ := New(root, locks, nil)
362 second, _ := New(root, locks, nil)
363 first.BeginRun()
364 second.BeginRun()
365 if err := first.AcquireWrite(context.Background()); err != nil {
366 t.Fatal(err)
367 }
368 one, two := make(chan struct{}), make(chan struct{})
369 first.RetainUntil(one)
370 first.RetainUntil(two)
371 first.EndRun()
372 close(one)
373 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
374 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
375 cancel()
376 t.Fatalf("lease released before final background job: %v", err)
377 }
378 cancel()
379 close(two)
380 ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
381 defer cancel()
382 if err := second.AcquireWrite(ctx); err != nil {
383 t.Fatal(err)
384 }
385 second.EndRun()
386 }
387
388 func TestRetainWithoutWriteDoesNotBlockReaders(t *testing.T) {
389 root, locks := t.TempDir(), t.TempDir()
390 reader, _ := New(root, locks, nil)
391 writer, _ := New(root, locks, nil)
392 reader.BeginRun()
393 done := make(chan struct{})
394 reader.RetainUntil(done)
395 reader.EndRun()
396 writer.BeginRun()
397 if err := writer.AcquireWrite(context.Background()); err != nil {
398 t.Fatal(err)
399 }
400 writer.EndRun()
401 close(done)
402 }
403
404 func TestCrossProcessLeaseBlocksAndCrashReleases(t *testing.T) {
405 root, locks := t.TempDir(), t.TempDir()
406 ready := filepath.Join(t.TempDir(), "ready")
407 cmd := exec.Command(os.Args[0], "-test.run=^TestWorkspaceLeaseHelperProcess$")
408 cmd.Env = append(os.Environ(),
409 "REASONIX_WORKSPACE_LEASE_HELPER=1",
410 "REASONIX_WORKSPACE_LEASE_ROOT="+root,
411 "REASONIX_WORKSPACE_LEASE_DIR="+locks,
412 "REASONIX_WORKSPACE_LEASE_READY="+ready,
413 )
414 if err := cmd.Start(); err != nil {
415 t.Fatal(err)
416 }
417 t.Cleanup(func() {
418 _ = cmd.Process.Kill()
419 _, _ = cmd.Process.Wait()
420 })
421 deadline := time.Now().Add(5 * time.Second)
422 for {
423 if _, err := os.Stat(ready); err == nil {
424 break
425 }
426 if time.Now().After(deadline) {
427 t.Fatal("helper process did not acquire lease")
428 }
429 time.Sleep(20 * time.Millisecond)
430 }
431
432 o, err := New(root, locks, nil)
433 if err != nil {
434 t.Fatal(err)
435 }
436 o.BeginRun()
437 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Millisecond)
438 if err := o.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
439 cancel()
440 t.Fatalf("cross-process acquire while helper lived = %v, want deadline", err)
441 }
442 cancel()
443
444 if err := cmd.Process.Kill(); err != nil {
445 t.Fatal(err)
446 }
447 _, _ = cmd.Process.Wait()
448 ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
449 defer cancel()
450 if err := o.AcquireWrite(ctx); err != nil {
451 t.Fatalf("OS lease did not release after helper crash: %v", err)
452 }
453 o.EndRun()
454 }
455
455 lines GO