返回 DeepSeek-Reasonix
roots_test.go
根目录 / internal / workspacelease / roots_test.go
1 package workspacelease
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "reflect"
10 "strings"
11 "testing"
12 "time"
13
14 "reasonix/internal/filelock"
15 )
16
17 func collidingRootOwners(t *testing.T) (string, *Owner, *Owner) {
18 t.Helper()
19 base, err := CanonicalWorkspace(t.TempDir())
20 if err != nil {
21 t.Fatal(err)
22 }
23 locks := t.TempDir()
24 seen := map[string]*Owner{}
25 for i := range treeLockStripes + 1 {
26 owner, err := New(filepath.Join(base, fmt.Sprintf("root-%d", i)), locks, nil)
27 if err != nil {
28 t.Fatal(err)
29 }
30 slot := owner.treeLockPath(owner.canonical)
31 if first := seen[slot]; first != nil {
32 for _, root := range []string{first.canonical, owner.canonical} {
33 if err := os.MkdirAll(root, 0o700); err != nil {
34 t.Fatal(err)
35 }
36 }
37 return locks, first, owner
38 }
39 seen[slot] = owner
40 }
41 t.Fatal("no tree stripe collision found")
42 return "", nil, nil
43 }
44
45 func TestRootGroupCoalescesCollidingTreeStripes(t *testing.T) {
46 locks, first, second := collidingRootOwners(t)
47 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
48 defer cancel()
49 release, err := HoldWriteRoots(ctx, locks, first.canonical, second.canonical)
50 if err != nil {
51 t.Fatalf("group waited for its own tree stripe: %v", err)
52 }
53 defer release()
54 for _, owner := range []*Owner{first, second} {
55 if unlock, err := filelock.TryAcquire(owner.lockPath); !errors.Is(err, filelock.ErrHeld) {
56 if unlock != nil {
57 unlock()
58 }
59 t.Fatalf("group omitted legacy workspace protection: %v", err)
60 }
61 }
62 blocked, stop := context.WithTimeout(context.Background(), 30*time.Millisecond)
63 defer stop()
64 if unlock, err := second.HoldWrite(blocked); !errors.Is(err, context.DeadlineExceeded) {
65 if unlock != nil {
66 unlock()
67 }
68 t.Fatalf("independent owner bypassed group: %v", err)
69 }
70 release()
71 release() // Group release is idempotent.
72 for _, owner := range []*Owner{first, second} {
73 unlock, err := owner.HoldWrite(ctx)
74 if err != nil {
75 t.Fatalf("group leaked lease: %v", err)
76 }
77 unlock()
78 }
79 }
80
81 func TestRootGroupCancellationReleasesPartialCompatibilityHolds(t *testing.T) {
82 locks, first, second := collidingRootOwners(t)
83 holdTree, err := filelock.TryAcquire(first.treeLockPath(first.canonical))
84 if err != nil {
85 t.Fatal(err)
86 }
87 defer holdTree()
88 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
89 defer cancel()
90 if release, err := HoldWriteRoots(ctx, locks, first.canonical, second.canonical); !errors.Is(err, context.DeadlineExceeded) {
91 if release != nil {
92 release()
93 }
94 t.Fatalf("group did not honor cancellation: %v", err)
95 }
96 for _, owner := range []*Owner{first, second} {
97 unlock, err := filelock.TryAcquire(owner.lockPath)
98 if err != nil {
99 t.Fatalf("cancelled group leaked compatibility lock: %v", err)
100 }
101 unlock()
102 }
103 }
104
105 func TestRootGroupRejectsLinkRedirectAfterLockAcquisition(t *testing.T) {
106 root := t.TempDir()
107 first, second := filepath.Join(root, "first"), filepath.Join(root, "second")
108 if err := os.Mkdir(first, 0o700); err != nil {
109 t.Fatal(err)
110 }
111 if err := os.Mkdir(second, 0o700); err != nil {
112 t.Fatal(err)
113 }
114 alias := filepath.Join(root, "alias")
115 if err := os.Symlink(first, alias); err != nil {
116 t.Skipf("symlink unavailable: %v", err)
117 }
118 original := workspaceRootsAfterAcquire
119 workspaceRootsAfterAcquire = func() {
120 if err := os.Remove(alias); err != nil {
121 t.Fatal(err)
122 }
123 if err := os.Symlink(second, alias); err != nil {
124 t.Fatal(err)
125 }
126 }
127 t.Cleanup(func() { workspaceRootsAfterAcquire = original })
128 release, err := HoldWriteRoots(t.Context(), t.TempDir(), alias)
129 if release != nil {
130 release()
131 }
132 if err == nil || !strings.Contains(err.Error(), "identity changed") {
133 t.Fatalf("error = %v", err)
134 }
135 }
136
137 func TestRootGroupPromotesRequestedAncestorAndOrdersBothDirections(t *testing.T) {
138 parent := t.TempDir()
139 child := filepath.Join(parent, "child")
140 if err := os.Mkdir(child, 0o700); err != nil {
141 t.Fatal(err)
142 }
143 locks := t.TempDir()
144 _, forward, err := rootLockDomains(locks, []string{parent, child})
145 if err != nil {
146 t.Fatal(err)
147 }
148 _, reverse, err := rootLockDomains(locks, []string{child, parent, parent})
149 if err != nil || !reflect.DeepEqual(forward, reverse) {
150 t.Fatalf("group ordering depends on caller order: %v", err)
151 }
152 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
153 defer cancel()
154 release, err := HoldWriteRoots(ctx, locks, child, parent)
155 if err != nil {
156 t.Fatalf("group reacquired its ancestor: %v", err)
157 }
158 defer release()
159 owner, err := New(parent, locks, nil)
160 if err != nil {
161 t.Fatal(err)
162 }
163 if unlock, err := filelock.TryAcquire(owner.lockPath); !errors.Is(err, filelock.ErrHeld) {
164 if unlock != nil {
165 unlock()
166 }
167 t.Fatalf("ancestor was not exclusive: %v", err)
168 }
169 }
170
171 func TestRootGroupRejectsCancelledContextWithFreeLocks(t *testing.T) {
172 root, locks := t.TempDir(), t.TempDir()
173 ctx, cancel := context.WithCancel(context.Background())
174 cancel()
175 if release, err := HoldWriteRoots(ctx, locks, root); !errors.Is(err, context.Canceled) {
176 if release != nil {
177 release()
178 }
179 t.Fatalf("cancelled group acquired free locks: %v", err)
180 }
181 }
182
182 lines GO