返回 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 TestWorkspaceIdentityHelpersPreserveCanonicalRoot(t *testing.T) {
108 owner, err := New(t.TempDir(), t.TempDir(), nil)
109 if err != nil {
110 t.Fatal(err)
111 }
112 ancestors := ancestorDirectories(owner.compatibility)
113 if len(ancestors) == 0 || ancestors[len(ancestors)-1] != owner.compatibility {
114 t.Fatalf("ancestor chain = %q, want compatibility root %q last", ancestors, owner.compatibility)
115 }
116 if got := workspaceLockPath(owner.lockDir, owner.compatibility); got != owner.lockPath {
117 t.Fatalf("compatibility root lock = %q, want owner lock %q", got, owner.lockPath)
118 }
119 chain := pathChain(owner.canonical, filepath.Join(owner.canonical, "nested", "file.go"))
120 if len(chain) == 0 || chain[0] != owner.canonical {
121 t.Fatalf("path chain = %q, want canonical root %q first", chain, owner.canonical)
122 }
123 }
124
125 func TestRepositoryRootAndSubdirectoryOwnersSerialize(t *testing.T) {
126 repo, locks := t.TempDir(), t.TempDir()
127 if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
128 t.Fatal(err)
129 }
130 subdir := filepath.Join(repo, "nested", "project")
131 if err := os.MkdirAll(subdir, 0o755); err != nil {
132 t.Fatal(err)
133 }
134 rootOwner, err := New(repo, locks, nil)
135 if err != nil {
136 t.Fatal(err)
137 }
138 subdirOwner, err := New(subdir, locks, nil)
139 if err != nil {
140 t.Fatal(err)
141 }
142 rootOwner.BeginRun()
143 subdirOwner.BeginRun()
144 if err := rootOwner.AcquireWrite(context.Background()); err != nil {
145 t.Fatal(err)
146 }
147 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
148 if err := subdirOwner.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
149 cancel()
150 t.Fatalf("repository subdirectory owner acquired independently: %v", err)
151 }
152 cancel()
153 rootOwner.EndRun()
154 if err := subdirOwner.AcquireWrite(context.Background()); err != nil {
155 t.Fatal(err)
156 }
157 subdirOwner.EndRun()
158 }
159
160 func TestOwnersSerializeSameWorkspaceAndNotifyOnce(t *testing.T) {
161 root, locks := t.TempDir(), t.TempDir()
162 first, err := New(root, locks, nil)
163 if err != nil {
164 t.Fatal(err)
165 }
166 var notices atomic.Int32
167 waiting := make(chan struct{}, 1)
168 second, err := New(root, locks, func() {
169 notices.Add(1)
170 select {
171 case waiting <- struct{}{}:
172 default:
173 }
174 })
175 if err != nil {
176 t.Fatal(err)
177 }
178 first.BeginRun()
179 second.BeginRun()
180 t.Cleanup(first.EndRun)
181 t.Cleanup(second.EndRun)
182 if err := first.AcquireWrite(context.Background()); err != nil {
183 t.Fatal(err)
184 }
185
186 acquired := make(chan error, 1)
187 go func() { acquired <- second.AcquireWrite(context.Background()) }()
188 select {
189 case err := <-acquired:
190 t.Fatalf("second owner acquired early: %v", err)
191 case <-waiting:
192 case <-time.After(2 * time.Second):
193 t.Fatal("second owner did not report waiting")
194 }
195 first.EndRun()
196 select {
197 case err := <-acquired:
198 if err != nil {
199 t.Fatal(err)
200 }
201 case <-time.After(2 * time.Second):
202 t.Fatal("second owner did not acquire after release")
203 }
204 if got := notices.Load(); got != 1 {
205 t.Fatalf("wait notices = %d, want 1", got)
206 }
207 second.EndRun()
208 }
209
210 func TestStateReportsWaitingAndAcquiredWithoutIdentity(t *testing.T) {
211 root, locks := t.TempDir(), t.TempDir()
212 first, err := New(root, locks, nil)
213 if err != nil {
214 t.Fatal(err)
215 }
216 waiting := make(chan struct{}, 1)
217 second, err := New(root, locks, func() { waiting <- struct{}{} })
218 if err != nil {
219 t.Fatal(err)
220 }
221 first.BeginRun()
222 second.BeginRun()
223 if err := first.AcquireWrite(context.Background()); err != nil {
224 t.Fatal(err)
225 }
226 if got := first.State(); !got.Acquired || got.Waiting {
227 t.Fatalf("first owner state = %+v, want acquired and not waiting", got)
228 }
229 acquired := make(chan error, 1)
230 go func() { acquired <- second.AcquireWrite(context.Background()) }()
231 select {
232 case <-waiting:
233 case <-time.After(2 * time.Second):
234 t.Fatal("second owner did not report waiting")
235 }
236 if got := second.State(); got.Acquired || !got.Waiting {
237 t.Fatalf("second owner state = %+v, want waiting and not acquired", got)
238 }
239 first.EndRun()
240 if err := <-acquired; err != nil {
241 t.Fatal(err)
242 }
243 if got := second.State(); !got.Acquired || got.Waiting {
244 t.Fatalf("second owner state after acquire = %+v, want acquired and not waiting", got)
245 }
246 second.EndRun()
247 }
248
249 func TestIndependentWorkspacesDoNotBlockEachOther(t *testing.T) {
250 locks := t.TempDir()
251 first, err := New(t.TempDir(), locks, nil)
252 if err != nil {
253 t.Fatal(err)
254 }
255 secondRoot, _ := unrelatedTreePath(t, first, t.TempDir())
256 second, err := New(secondRoot, locks, nil)
257 if err != nil {
258 t.Fatal(err)
259 }
260 first.BeginRun()
261 second.BeginRun()
262 t.Cleanup(first.EndRun)
263 t.Cleanup(second.EndRun)
264 if err := first.AcquireWrite(context.Background()); err != nil {
265 t.Fatal(err)
266 }
267 ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
268 defer cancel()
269 if err := second.AcquireWrite(ctx); err != nil {
270 t.Fatalf("independent workspace was blocked: %v", err)
271 }
272 first.EndRun()
273 second.EndRun()
274 }
275
276 func TestLeaseMetadataNeverDirtiesWorkspace(t *testing.T) {
277 root, locks := t.TempDir(), t.TempDir()
278 before, err := os.ReadDir(root)
279 if err != nil {
280 t.Fatal(err)
281 }
282 o, err := New(root, locks, nil)
283 if err != nil {
284 t.Fatal(err)
285 }
286 o.BeginRun()
287 if err := o.AcquireWrite(context.Background()); err != nil {
288 t.Fatal(err)
289 }
290 o.EndRun()
291 after, err := os.ReadDir(root)
292 if err != nil {
293 t.Fatal(err)
294 }
295 if len(after) != len(before) {
296 t.Fatalf("workspace entries changed after lease: before=%d after=%d", len(before), len(after))
297 }
298 }
299
300 func TestAcquireIsReentrantWithinOwner(t *testing.T) {
301 o, err := New(t.TempDir(), t.TempDir(), nil)
302 if err != nil {
303 t.Fatal(err)
304 }
305 o.BeginRun()
306 if err := o.AcquireWrite(context.Background()); err != nil {
307 t.Fatal(err)
308 }
309 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
310 defer cancel()
311 if err := o.AcquireWrite(ctx); err != nil {
312 t.Fatalf("re-entrant acquire failed: %v", err)
313 }
314 o.EndRun()
315 }
316
317 func TestCancelledWaitDoesNotLeakLocalLease(t *testing.T) {
318 root, locks := t.TempDir(), t.TempDir()
319 first, _ := New(root, locks, nil)
320 second, _ := New(root, locks, nil)
321 third, _ := New(root, locks, nil)
322 first.BeginRun()
323 second.BeginRun()
324 third.BeginRun()
325 if err := first.AcquireWrite(context.Background()); err != nil {
326 t.Fatal(err)
327 }
328 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
329 defer cancel()
330 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
331 t.Fatalf("cancelled acquire = %v, want deadline", err)
332 }
333 second.EndRun()
334 first.EndRun()
335 if err := third.AcquireWrite(context.Background()); err != nil {
336 t.Fatalf("lease leaked after cancellation: %v", err)
337 }
338 third.EndRun()
339 }
340
341 func TestLeaseWaitsForLastRun(t *testing.T) {
342 root, locks := t.TempDir(), t.TempDir()
343 first, _ := New(root, locks, nil)
344 second, _ := New(root, locks, nil)
345 first.BeginRun()
346 first.BeginRun()
347 second.BeginRun()
348 if err := first.AcquireWrite(context.Background()); err != nil {
349 t.Fatal(err)
350 }
351 first.EndRun()
352 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
353 defer cancel()
354 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
355 t.Fatalf("second acquired before final run ended: %v", err)
356 }
357 first.EndRun()
358 if err := second.AcquireWrite(context.Background()); err != nil {
359 t.Fatal(err)
360 }
361 second.EndRun()
362 }
363
364 func TestBackgroundRetentionOutlivesRun(t *testing.T) {
365 root, locks := t.TempDir(), t.TempDir()
366 first, _ := New(root, locks, nil)
367 second, _ := New(root, locks, nil)
368 first.BeginRun()
369 second.BeginRun()
370 if err := first.AcquireWrite(context.Background()); err != nil {
371 t.Fatal(err)
372 }
373 done := make(chan struct{})
374 first.RetainUntil(done)
375 first.EndRun()
376 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
377 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
378 cancel()
379 t.Fatalf("second acquired while background job was running: %v", err)
380 }
381 cancel()
382 close(done)
383 ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
384 defer cancel()
385 if err := second.AcquireWrite(ctx); err != nil {
386 t.Fatal(err)
387 }
388 second.EndRun()
389 }
390
391 func TestLeaseWaitsForEveryRetainedBackgroundJob(t *testing.T) {
392 root, locks := t.TempDir(), t.TempDir()
393 first, _ := New(root, locks, nil)
394 second, _ := New(root, locks, nil)
395 first.BeginRun()
396 second.BeginRun()
397 if err := first.AcquireWrite(context.Background()); err != nil {
398 t.Fatal(err)
399 }
400 one, two := make(chan struct{}), make(chan struct{})
401 first.RetainUntil(one)
402 first.RetainUntil(two)
403 first.EndRun()
404 close(one)
405 ctx, cancel := context.WithTimeout(context.Background(), 60*time.Millisecond)
406 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
407 cancel()
408 t.Fatalf("lease released before final background job: %v", err)
409 }
410 cancel()
411 close(two)
412 ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
413 defer cancel()
414 if err := second.AcquireWrite(ctx); err != nil {
415 t.Fatal(err)
416 }
417 second.EndRun()
418 }
419
420 func TestNestedRepoPathWritesRunInParallel(t *testing.T) {
421 parent, locks := t.TempDir(), t.TempDir()
422 repoA := filepath.Join(parent, "A")
423 repoB := filepath.Join(parent, "B")
424 for _, repo := range []string{repoA, repoB} {
425 if err := os.MkdirAll(filepath.Join(repo, ".git"), 0o755); err != nil {
426 t.Fatal(err)
427 }
428 }
429 first, err := New(parent, locks, nil)
430 if err != nil {
431 t.Fatal(err)
432 }
433 second, err := New(parent, locks, nil)
434 if err != nil {
435 t.Fatal(err)
436 }
437 first.BeginRun()
438 second.BeginRun()
439 t.Cleanup(first.EndRun)
440 t.Cleanup(second.EndRun)
441 firstPath := filepath.Join(repoA, "a.go")
442 secondPath := distinctPathSlotInDirectory(t, second, repoB, canonicalPathSlot(t, first, firstPath))
443 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
444 defer cancel()
445 if err := first.AcquireWriteForPath(ctx, firstPath); err != nil {
446 t.Fatal(err)
447 }
448 if err := second.AcquireWriteForPath(ctx, secondPath); err != nil {
449 t.Fatal(err)
450 }
451 first.EndRun()
452 second.EndRun()
453 }
454
455 func TestExclusiveWorkspaceWriteBlocksNestedRepoPath(t *testing.T) {
456 parent, locks := t.TempDir(), t.TempDir()
457 repoB := filepath.Join(parent, "B")
458 if err := os.MkdirAll(filepath.Join(repoB, ".git"), 0o755); err != nil {
459 t.Fatal(err)
460 }
461 first, err := New(parent, locks, nil)
462 if err != nil {
463 t.Fatal(err)
464 }
465 second, err := New(parent, locks, nil)
466 if err != nil {
467 t.Fatal(err)
468 }
469 first.BeginRun()
470 second.BeginRun()
471 if err := first.AcquireWrite(context.Background()); err != nil {
472 t.Fatal(err)
473 }
474 ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
475 if err := second.AcquireWriteForPath(ctx, filepath.Join(repoB, "b.go")); !errors.Is(err, context.DeadlineExceeded) {
476 cancel()
477 t.Fatalf("nested path write acquired under exclusive workspace: %v", err)
478 }
479 cancel()
480 first.EndRun()
481 if err := second.AcquireWriteForPath(context.Background(), filepath.Join(repoB, "b.go")); err != nil {
482 t.Fatal(err)
483 }
484 second.EndRun()
485 }
486
487 func TestSameRepoDifferentFilesRunInParallel(t *testing.T) {
488 repo, locks := t.TempDir(), t.TempDir()
489 if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
490 t.Fatal(err)
491 }
492 first, err := New(repo, locks, nil)
493 if err != nil {
494 t.Fatal(err)
495 }
496 second, err := New(repo, locks, nil)
497 if err != nil {
498 t.Fatal(err)
499 }
500 first.BeginRun()
501 second.BeginRun()
502 t.Cleanup(first.EndRun)
503 t.Cleanup(second.EndRun)
504 firstPath, secondPath := increasingPathSlots(t, first)
505 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
506 defer cancel()
507 if err := first.AcquireWriteForPath(ctx, firstPath); err != nil {
508 t.Fatal(err)
509 }
510 if err := second.AcquireWriteForPath(ctx, secondPath); err != nil {
511 t.Fatal(err)
512 }
513 }
514
515 func TestSameFilePathWritesStillSerialize(t *testing.T) {
516 repo, locks := t.TempDir(), t.TempDir()
517 if err := os.Mkdir(filepath.Join(repo, ".git"), 0o755); err != nil {
518 t.Fatal(err)
519 }
520 first, err := New(repo, locks, nil)
521 if err != nil {
522 t.Fatal(err)
523 }
524 second, err := New(repo, locks, nil)
525 if err != nil {
526 t.Fatal(err)
527 }
528 first.BeginRun()
529 second.BeginRun()
530 path := filepath.Join(repo, "a.go")
531 if err := first.AcquireWriteForPath(context.Background(), path); err != nil {
532 t.Fatal(err)
533 }
534 ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
535 if err := second.AcquireWriteForPath(ctx, path); !errors.Is(err, context.DeadlineExceeded) {
536 cancel()
537 t.Fatalf("same file must still serialize: %v", err)
538 }
539 cancel()
540 first.EndRun()
541 if err := second.AcquireWriteForPath(context.Background(), path); err != nil {
542 t.Fatal(err)
543 }
544 second.EndRun()
545 }
546
547 func TestHoldWriteReleasesBeforeEndRun(t *testing.T) {
548 root, locks := t.TempDir(), t.TempDir()
549 first, err := New(root, locks, nil)
550 if err != nil {
551 t.Fatal(err)
552 }
553 second, err := New(root, locks, nil)
554 if err != nil {
555 t.Fatal(err)
556 }
557 first.BeginRun()
558 second.BeginRun()
559 release, err := first.HoldWrite(context.Background())
560 if err != nil {
561 t.Fatal(err)
562 }
563 ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
564 if err := second.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
565 cancel()
566 t.Fatalf("held write should block: %v", err)
567 }
568 cancel()
569 release()
570 if err := second.AcquireWrite(context.Background()); err != nil {
571 t.Fatal(err)
572 }
573 first.EndRun()
574 second.EndRun()
575 }
576
577 func TestPathWriteUpgradeToExclusiveBlocksOtherRepo(t *testing.T) {
578 parent, locks := t.TempDir(), t.TempDir()
579 repoA := filepath.Join(parent, "A")
580 repoB := filepath.Join(parent, "B")
581 for _, repo := range []string{repoA, repoB} {
582 if err := os.MkdirAll(filepath.Join(repo, ".git"), 0o755); err != nil {
583 t.Fatal(err)
584 }
585 }
586 first, err := New(parent, locks, nil)
587 if err != nil {
588 t.Fatal(err)
589 }
590 second, err := New(parent, locks, nil)
591 if err != nil {
592 t.Fatal(err)
593 }
594 first.BeginRun()
595 second.BeginRun()
596 releasePath, err := first.HoldWriteForPath(context.Background(), filepath.Join(repoA, "a.go"))
597 if err != nil {
598 t.Fatal(err)
599 }
600 upgraded := make(chan error, 1)
601 go func() {
602 release, upgradeErr := first.HoldWrite(context.Background())
603 if upgradeErr == nil {
604 release()
605 }
606 upgraded <- upgradeErr
607 }()
608 waitForOwnerAcquisition(t, first)
609 releasePath()
610 if err := <-upgraded; err != nil {
611 t.Fatal(err)
612 }
613 release, err := first.HoldWrite(context.Background())
614 if err != nil {
615 t.Fatal(err)
616 }
617 ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
618 if err := second.AcquireWriteForPath(ctx, filepath.Join(repoB, "b.go")); !errors.Is(err, context.DeadlineExceeded) {
619 cancel()
620 t.Fatalf("upgrade to exclusive should block other repo: %v", err)
621 }
622 cancel()
623 release()
624 first.EndRun()
625 if err := second.AcquireWriteForPath(context.Background(), filepath.Join(repoB, "b.go")); err != nil {
626 t.Fatal(err)
627 }
628 second.EndRun()
629 }
630
631 func TestRetainWithoutWriteDoesNotBlockReaders(t *testing.T) {
632 root, locks := t.TempDir(), t.TempDir()
633 reader, _ := New(root, locks, nil)
634 writer, _ := New(root, locks, nil)
635 reader.BeginRun()
636 done := make(chan struct{})
637 reader.RetainUntil(done)
638 reader.EndRun()
639 writer.BeginRun()
640 if err := writer.AcquireWrite(context.Background()); err != nil {
641 t.Fatal(err)
642 }
643 writer.EndRun()
644 close(done)
645 }
646
647 func TestCrossProcessLeaseBlocksAndCrashReleases(t *testing.T) {
648 root, locks := t.TempDir(), t.TempDir()
649 ready := filepath.Join(t.TempDir(), "ready")
650 cmd := exec.Command(os.Args[0], "-test.run=^TestWorkspaceLeaseHelperProcess$")
651 cmd.Env = append(os.Environ(),
652 "REASONIX_WORKSPACE_LEASE_HELPER=1",
653 "REASONIX_WORKSPACE_LEASE_ROOT="+root,
654 "REASONIX_WORKSPACE_LEASE_DIR="+locks,
655 "REASONIX_WORKSPACE_LEASE_READY="+ready,
656 )
657 if err := cmd.Start(); err != nil {
658 t.Fatal(err)
659 }
660 t.Cleanup(func() {
661 _ = cmd.Process.Kill()
662 _, _ = cmd.Process.Wait()
663 })
664 deadline := time.Now().Add(5 * time.Second)
665 for {
666 if _, err := os.Stat(ready); err == nil {
667 break
668 }
669 if time.Now().After(deadline) {
670 t.Fatal("helper process did not acquire lease")
671 }
672 time.Sleep(20 * time.Millisecond)
673 }
674
675 o, err := New(root, locks, nil)
676 if err != nil {
677 t.Fatal(err)
678 }
679 o.BeginRun()
680 ctx, cancel := context.WithTimeout(context.Background(), 120*time.Millisecond)
681 if err := o.AcquireWrite(ctx); !errors.Is(err, context.DeadlineExceeded) {
682 cancel()
683 t.Fatalf("cross-process acquire while helper lived = %v, want deadline", err)
684 }
685 cancel()
686
687 if err := cmd.Process.Kill(); err != nil {
688 t.Fatal(err)
689 }
690 _, _ = cmd.Process.Wait()
691 ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
692 defer cancel()
693 if err := o.AcquireWrite(ctx); err != nil {
694 t.Fatalf("OS lease did not release after helper crash: %v", err)
695 }
696 o.EndRun()
697 }
698
698 lines GO