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