返回 DeepSeek-Reasonix
update_lock_test.go
根目录 / internal / repair / update_lock_test.go
1 package repair
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8 )
9
10 // TestPendingUpdateRollbackExcludesConcurrentCommit pins the pending-update
11 // lock: while one process is mid-rollback (staging backups), a concurrent
12 // MarkUpdateHealthy must wait instead of deleting the transaction and backups
13 // under the restorer, and the release unit must end on the rolled-back bytes.
14 // The two operations run on separate file descriptors, so this exercises the
15 // same exclusion two processes would see.
16 func TestPendingUpdateRollbackExcludesConcurrentCommit(t *testing.T) {
17 home := t.TempDir()
18 t.Setenv("REASONIX_HOME", home)
19 dir := t.TempDir()
20 target := filepath.Join(dir, "reasonix-desktop")
21 originalExecutable := repairExecutable
22 repairExecutable = func() (string, error) { return filepath.Join(dir, "reasonix-guard"), nil }
23 t.Cleanup(func() { repairExecutable = originalExecutable })
24 if err := os.WriteFile(target, []byte("old"), 0o700); err != nil {
25 t.Fatal(err)
26 }
27 if _, err := PrepareFileUpdate("v1", "v2", target); err != nil {
28 t.Fatal(err)
29 }
30 if err := os.WriteFile(target, []byte("new"), 0o700); err != nil {
31 t.Fatal(err)
32 }
33
34 entered := make(chan struct{})
35 release := make(chan struct{})
36 originalStage := rollbackStageCopy
37 // The transaction holds exactly one file, so the hook fires exactly once.
38 rollbackStageCopy = func(src, dst string, mode os.FileMode) (string, error) {
39 close(entered)
40 <-release
41 return originalStage(src, dst, mode)
42 }
43 t.Cleanup(func() { rollbackStageCopy = originalStage })
44
45 rollbackDone := make(chan error, 1)
46 go func() {
47 _, err := RollbackPendingUpdate()
48 rollbackDone <- err
49 }()
50 <-entered
51
52 commitDone := make(chan error, 1)
53 go func() { commitDone <- MarkUpdateHealthy("v2") }()
54 select {
55 case err := <-commitDone:
56 t.Fatalf("MarkUpdateHealthy completed during an in-flight rollback (err=%v); pending-update operations must serialize", err)
57 case <-time.After(300 * time.Millisecond):
58 }
59
60 close(release)
61 if err := <-rollbackDone; err != nil {
62 t.Fatalf("rollback: %v", err)
63 }
64 if err := <-commitDone; err != nil {
65 t.Fatalf("commit after rollback: %v", err)
66 }
67 got, err := os.ReadFile(target)
68 if err != nil {
69 t.Fatal(err)
70 }
71 if string(got) != "old" {
72 t.Fatalf("target after serialized rollback = %q, want %q", got, "old")
73 }
74 if _, err := ReadPendingUpdate(); !os.IsNotExist(err) {
75 t.Fatalf("pending update should be consumed by the rollback, got err=%v", err)
76 }
77 }
78
79 func TestAppBundleRollbackLocksBackupPath(t *testing.T) {
80 home := t.TempDir()
81 t.Setenv("REASONIX_HOME", home)
82 dir, err := filepath.EvalSymlinks(t.TempDir())
83 if err != nil {
84 t.Fatal(err)
85 }
86 app := filepath.Join(dir, "Reasonix.app")
87 exe := filepath.Join(app, "Contents", "MacOS", "Reasonix")
88 if err := os.MkdirAll(filepath.Dir(exe), 0o700); err != nil {
89 t.Fatal(err)
90 }
91 if err := os.WriteFile(exe, []byte("old"), 0o700); err != nil {
92 t.Fatal(err)
93 }
94 originalExecutable := repairExecutable
95 repairExecutable = func() (string, error) { return exe, nil }
96 t.Cleanup(func() { repairExecutable = originalExecutable })
97
98 backup := app + ".reasonix-update-backup"
99 if _, err := PrepareAppBundleUpdate("v1", "v2", app, backup); err != nil {
100 t.Fatal(err)
101 }
102 if err := os.Rename(app, backup); err != nil {
103 t.Fatal(err)
104 }
105 holder, err := LockRepairMutations(backup)
106 if err != nil {
107 t.Fatal(err)
108 }
109
110 done := make(chan error, 1)
111 go func() {
112 _, err := RollbackPendingUpdate()
113 done <- err
114 }()
115 select {
116 case err := <-done:
117 holder()
118 t.Fatalf("rollback completed without the backup lock: %v", err)
119 case <-time.After(300 * time.Millisecond):
120 }
121 holder()
122 if err := <-done; err != nil {
123 t.Fatalf("rollback after backup unlock: %v", err)
124 }
125 }
126
126 lines GO