返回 DeepSeek-Reasonix
windows_entries_test.go
根目录 / internal / installlayout / windows_entries_test.go
1 package installlayout
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "testing"
9
10 "reasonix/internal/filelock"
11 )
12
13 func windowsEntryRequest(t *testing.T, root, version, body string) ActivationRequest {
14 t.Helper()
15 src := t.TempDir()
16 req := ActivationRequest{InstallRoot: root, Version: version}
17 for _, name := range AllowedVersionMembers() {
18 req.Members = append(req.Members, Member{Name: name, Path: writeTempMember(t, src, name, body)})
19 }
20 req.WindowsRootEntries = &WindowsRootEntrySources{
21 LauncherPath: writeTempMember(t, src, "payload-launcher", body+"-gui"),
22 CLIEntryPath: writeTempMember(t, src, "payload-cli", body+"-cli"),
23 }
24 return req
25 }
26
27 func assertEntryBytes(t *testing.T, root, name, want string) {
28 t.Helper()
29 got, err := os.ReadFile(filepath.Join(root, name))
30 if err != nil || string(got) != want {
31 t.Fatalf("%s = %q (%v), want %q", name, got, err, want)
32 }
33 }
34
35 func TestWindowsEntriesFreshRepairAndUpgrade(t *testing.T) {
36 for _, legacy := range []bool{false, true} {
37 for _, canonical := range []bool{false, true} {
38 t.Run(map[bool]string{false: "fresh", true: "legacy"}[legacy]+map[bool]string{false: "", true: "-canonical"}[canonical], func(t *testing.T) {
39 root := filepath.Join(t.TempDir(), "Reasonix 测试")
40 if err := os.Mkdir(root, 0o755); err != nil {
41 t.Fatal(err)
42 }
43 if legacy {
44 writeTempMember(t, root, "reasonix-launcher.exe", "old")
45 }
46 if canonical {
47 writeTempMember(t, root, "Reasonix.exe", "old")
48 }
49 for i, version := range []string{"v1.38.9", "v1.38.9", "v1.39.0"} {
50 body := []string{"install", "repair", "upgrade"}[i]
51 if err := ActivateVersion(windowsEntryRequest(t, root, version, body)); err != nil {
52 t.Fatal(err)
53 }
54 assertEntryBytes(t, root, "Reasonix.exe", body+"-gui")
55 assertEntryBytes(t, root, "reasonix-cli.exe", body+"-cli")
56 if legacy {
57 assertEntryBytes(t, root, "reasonix-launcher.exe", body+"-gui")
58 } else if _, err := os.Lstat(filepath.Join(root, "reasonix-launcher.exe")); !os.IsNotExist(err) {
59 t.Fatalf("created legacy entry: %v", err)
60 }
61 ptr, err := ReadCurrent(root)
62 if err != nil || ptr.ActiveVersion != version || ptr.SchemaVersion != 1 {
63 t.Fatalf("pointer=%+v err=%v", ptr, err)
64 }
65 }
66 })
67 }
68 }
69 }
70
71 func TestWindowsEntriesInspectLegacyAfterLockAcquisition(t *testing.T) {
72 root := t.TempDir()
73 req := windowsEntryRequest(t, root, "v1.39.0", "new")
74 // Simulate the previous writer publishing the legacy entry while this
75 // activation waits to acquire its lock. No sleeps or global test hooks.
76 err := activateVersion(req, func(ctx context.Context, path string) (func(), error) {
77 writeTempMember(t, root, "reasonix-launcher.exe", "previous-writer")
78 return filelock.Acquire(ctx, path)
79 })
80 if err != nil {
81 t.Fatal(err)
82 }
83 assertEntryBytes(t, root, "reasonix-launcher.exe", "new-gui")
84 }
85
86 func TestConcurrentOldAndNewActivationPreservesCommittedLegacyEntry(t *testing.T) {
87 root := t.TempDir()
88 first := windowsEntryRequest(t, root, "v1.38.9", "old")
89 first.RootMembers = []Member{
90 {Name: "Reasonix.exe", Path: first.WindowsRootEntries.LauncherPath},
91 {Name: "reasonix-launcher.exe", Path: first.WindowsRootEntries.LauncherPath},
92 {Name: "reasonix-cli.exe", Path: first.WindowsRootEntries.CLIEntryPath},
93 }
94 first.RequiredRootNames = []string{"Reasonix.exe", "reasonix-launcher.exe", "reasonix-cli.exe"}
95 first.WindowsRootEntries = nil // Model the old writer's explicit entry list.
96 second := windowsEntryRequest(t, root, "v1.39.0", "new")
97 firstLocked, releaseFirst, secondAcquiring := make(chan struct{}), make(chan struct{}), make(chan struct{})
98 checks := 0
99 first.CheckProcesses = func() error {
100 checks++
101 if checks == 1 {
102 close(firstLocked)
103 <-releaseFirst
104 }
105 return nil
106 }
107 firstDone, secondDone := make(chan error, 1), make(chan error, 1)
108 go func() { firstDone <- ActivateVersion(first) }()
109 select {
110 case <-firstLocked:
111 case err := <-firstDone:
112 t.Fatalf("first activation never reached publication: %v", err)
113 }
114 go func() {
115 secondDone <- activateVersion(second, func(ctx context.Context, path string) (func(), error) {
116 close(secondAcquiring)
117 return filelock.Acquire(ctx, path)
118 })
119 }()
120 select {
121 case <-secondAcquiring:
122 case err := <-secondDone:
123 close(releaseFirst)
124 <-firstDone
125 t.Fatalf("second activation never acquired its lock: %v", err)
126 }
127 close(releaseFirst)
128 if err := <-firstDone; err != nil {
129 t.Fatal(err)
130 }
131 if err := <-secondDone; err != nil {
132 t.Fatal(err)
133 }
134 assertEntryBytes(t, root, "Reasonix.exe", "new-gui")
135 assertEntryBytes(t, root, "reasonix-launcher.exe", "new-gui")
136 assertEntryBytes(t, root, "reasonix-cli.exe", "new-cli")
137 ptr, err := ReadCurrent(root)
138 if err != nil || ptr.ActiveVersion != "v1.39.0" {
139 t.Fatalf("pointer=%+v err=%v", ptr, err)
140 }
141 }
142
143 func TestWindowsEntriesRollbackAllSelectedEntries(t *testing.T) {
144 for _, failure := range []string{"process", "pointer", "root"} {
145 t.Run(failure, func(t *testing.T) {
146 root := t.TempDir()
147 writeTempMember(t, root, "reasonix-launcher.exe", "original")
148 if err := ActivateVersion(windowsEntryRequest(t, root, "v1.38.9", "old")); err != nil {
149 t.Fatal(err)
150 }
151 req := windowsEntryRequest(t, root, "v1.39.0", "new")
152 checks := 0
153 switch failure {
154 case "process":
155 req.CheckProcesses = func() error {
156 checks++
157 if checks == 2 {
158 return errors.New("process appeared")
159 }
160 return nil
161 }
162 case "pointer":
163 if err := os.Remove(filepath.Join(root, CurrentFileName)); err != nil {
164 t.Fatal(err)
165 }
166 if err := os.Mkdir(filepath.Join(root, CurrentFileName), 0o755); err != nil {
167 t.Fatal(err)
168 }
169 case "root":
170 if err := os.Remove(filepath.Join(root, "reasonix-cli.exe")); err != nil {
171 t.Fatal(err)
172 }
173 if err := os.Mkdir(filepath.Join(root, "reasonix-cli.exe"), 0o755); err != nil {
174 t.Fatal(err)
175 }
176 }
177 if err := ActivateVersion(req); err == nil {
178 t.Fatal("activation should fail")
179 }
180 assertEntryBytes(t, root, "Reasonix.exe", "old-gui")
181 assertEntryBytes(t, root, "reasonix-launcher.exe", "old-gui")
182 if failure != "root" {
183 assertEntryBytes(t, root, "reasonix-cli.exe", "old-cli")
184 }
185 if failure != "pointer" {
186 ptr, err := ReadCurrent(root)
187 if err != nil || ptr.ActiveVersion != "v1.38.9" {
188 t.Fatalf("pointer=%+v err=%v", ptr, err)
189 }
190 }
191 if _, err := os.Stat(filepath.Join(root, "versions", "v1.39.0")); !os.IsNotExist(err) {
192 t.Fatalf("uncommitted version survived: %v", err)
193 }
194 })
195 }
196 }
197
198 func TestWindowsEntriesRejectInvalidLegacyAndMixedInputs(t *testing.T) {
199 for _, kind := range []string{"directory", "symlink", "mixed"} {
200 t.Run(kind, func(t *testing.T) {
201 root := t.TempDir()
202 req := windowsEntryRequest(t, root, "v1.39.0", "new")
203 legacy := filepath.Join(root, "reasonix-launcher.exe")
204 switch kind {
205 case "directory":
206 if err := os.Mkdir(legacy, 0o755); err != nil {
207 t.Fatal(err)
208 }
209 case "symlink":
210 if err := os.Symlink(req.WindowsRootEntries.LauncherPath, legacy); err != nil {
211 t.Skipf("symlink privilege unavailable: %v", err)
212 }
213 case "mixed":
214 req.RootMembers = []Member{{Name: "other", Path: req.WindowsRootEntries.LauncherPath}}
215 }
216 if err := ActivateVersion(req); err == nil {
217 t.Fatal("invalid request accepted")
218 }
219 if HasCurrent(root) {
220 t.Fatal("invalid activation committed")
221 }
222 if _, err := os.Lstat(filepath.Join(root, "Reasonix.exe")); !os.IsNotExist(err) {
223 t.Fatalf("published root entry: %v", err)
224 }
225 })
226 }
227 }
228
228 lines GO