返回 DeepSeek-Reasonix
backup_test.go
根目录 / internal / topicstate / backup_test.go
1 package topicstate
2
3 import (
4 "bytes"
5 "context"
6 "database/sql"
7 "errors"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12 )
13
14 func TestBackupExistingIncludesWALAndUnknownTables(t *testing.T) {
15 path := filepath.Join(t.TempDir(), "source %20 # 主题.sqlite")
16 db, err := sql.Open("sqlite", path)
17 if err != nil {
18 t.Fatal(err)
19 }
20 defer db.Close()
21 if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0; CREATE TABLE future_data(value TEXT); INSERT INTO future_data VALUES ('preserved');`); err != nil {
22 t.Fatal(err)
23 }
24 dest := filepath.Join(t.TempDir(), "backup.sqlite")
25 if err := BackupExisting(t.Context(), path, dest); err != nil {
26 t.Fatal(err)
27 }
28 backup, err := sql.Open("sqlite", dest)
29 if err != nil {
30 t.Fatal(err)
31 }
32 defer backup.Close()
33 var value string
34 if err := backup.QueryRow("SELECT value FROM future_data").Scan(&value); err != nil || value != "preserved" {
35 t.Fatalf("backup value=%q err=%v", value, err)
36 }
37 if err := BackupExisting(t.Context(), filepath.Join(t.TempDir(), "missing"), dest); !os.IsNotExist(err) {
38 t.Fatalf("missing source: %v", err)
39 }
40 }
41
42 func TestBackupExistingRejectsEmptyDestination(t *testing.T) {
43 source := filepath.Join(t.TempDir(), "source.sqlite")
44 db, err := sql.Open("sqlite", source)
45 if err != nil {
46 t.Fatal(err)
47 }
48 if _, err := db.Exec(`CREATE TABLE preserved(value TEXT); INSERT INTO preserved VALUES('source')`); err != nil {
49 t.Fatal(err)
50 }
51 if err := db.Close(); err != nil {
52 t.Fatal(err)
53 }
54 destination := filepath.Join(t.TempDir(), "existing.sqlite")
55 if err := os.WriteFile(destination, nil, 0600); err != nil {
56 t.Fatal(err)
57 }
58 if err := BackupExisting(t.Context(), source, destination); !errors.Is(err, os.ErrExist) {
59 t.Fatalf("existing destination error = %v, want ErrExist", err)
60 }
61 if got, err := os.ReadFile(destination); err != nil || len(got) != 0 {
62 t.Fatalf("existing empty file changed: %d bytes, %v", len(got), err)
63 }
64 }
65
66 func TestBackupExistingDoesNotOverwriteDestinationOrSource(t *testing.T) {
67 path := filepath.Join(t.TempDir(), "source.sqlite")
68 db, err := sql.Open("sqlite", path)
69 if err != nil {
70 t.Fatal(err)
71 }
72 if _, err := db.Exec(`CREATE TABLE preserved(value TEXT); INSERT INTO preserved VALUES('source')`); err != nil {
73 _ = db.Close()
74 t.Fatal(err)
75 }
76 if err := db.Close(); err != nil {
77 t.Fatal(err)
78 }
79 sourceBefore, err := os.ReadFile(path)
80 if err != nil {
81 t.Fatal(err)
82 }
83 destination := filepath.Join(t.TempDir(), "existing.sqlite")
84 destinationBefore := []byte("do not replace")
85 if err := os.WriteFile(destination, destinationBefore, 0o600); err != nil {
86 t.Fatal(err)
87 }
88 if err := BackupExisting(t.Context(), path, destination); err == nil {
89 t.Fatal("backup unexpectedly replaced an existing destination")
90 }
91 if got, err := os.ReadFile(path); err != nil || !bytes.Equal(got, sourceBefore) {
92 t.Fatalf("source changed: err=%v", err)
93 }
94 if got, err := os.ReadFile(destination); err != nil || !bytes.Equal(got, destinationBefore) {
95 t.Fatalf("destination changed: err=%v got=%q", err, got)
96 }
97 }
98
99 func TestBackupFailureDoesNotPublishPartialDestination(t *testing.T) {
100 root := t.TempDir()
101 source := filepath.Join(root, "corrupt.sqlite")
102 original := []byte("not a SQLite database")
103 if err := os.WriteFile(source, original, 0o600); err != nil {
104 t.Fatal(err)
105 }
106 destination := filepath.Join(root, "backup.sqlite")
107 if err := BackupExisting(t.Context(), source, destination); err == nil {
108 t.Fatal("corrupt source must fail backup")
109 }
110 if _, err := os.Lstat(destination); !os.IsNotExist(err) {
111 t.Fatalf("failed backup published a destination: %v", err)
112 }
113 if got, err := os.ReadFile(source); err != nil || !bytes.Equal(got, original) {
114 t.Fatalf("failed backup modified source: %q, %v", got, err)
115 }
116 entries, err := os.ReadDir(root)
117 if err != nil || len(entries) != 1 {
118 t.Fatalf("backup left temporary artifacts: %+v, %v", entries, err)
119 }
120 }
121
122 func TestBackupDoesNotReplaceDestinationCreatedDuringSnapshot(t *testing.T) {
123 root := t.TempDir()
124 source := filepath.Join(root, "source.sqlite")
125 db, err := sql.Open("sqlite", source)
126 if err != nil {
127 t.Fatal(err)
128 }
129 defer db.Close()
130 db.SetMaxOpenConns(1)
131 if _, err := db.Exec(`CREATE TABLE preserved(value TEXT); INSERT INTO preserved VALUES('source'); BEGIN EXCLUSIVE`); err != nil {
132 t.Fatal(err)
133 }
134 ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
135 destination := filepath.Join(root, "backup.sqlite")
136 done := make(chan struct{})
137 var backupErr error
138 go func() {
139 backupErr = BackupExisting(ctx, source, destination)
140 close(done)
141 }()
142 defer func() {
143 cancel()
144 _, _ = db.Exec(`ROLLBACK`)
145 <-done
146 }()
147 // The exclusive source lock prevents VACUUM from finishing. Observing its
148 // private directory proves the initial destination check has already passed.
149 deadline := time.Now().Add(3 * time.Second)
150 for {
151 paths, err := filepath.Glob(filepath.Join(root, ".topic-backup-*"))
152 if err != nil {
153 t.Fatal(err)
154 }
155 if len(paths) > 0 {
156 break
157 }
158 if time.Now().After(deadline) {
159 t.Fatal("backup did not reach the locked snapshot stage")
160 }
161 time.Sleep(10 * time.Millisecond)
162 }
163 original := []byte("created by another process after preflight")
164 if err := os.WriteFile(destination, original, 0o600); err != nil {
165 t.Fatal(err)
166 }
167 if _, err := db.Exec(`COMMIT`); err != nil {
168 t.Fatal(err)
169 }
170 <-done
171 if !errors.Is(backupErr, os.ErrExist) {
172 t.Fatalf("concurrent destination error=%v, want ErrExist", backupErr)
173 }
174 if got, err := os.ReadFile(destination); err != nil || !bytes.Equal(got, original) {
175 t.Fatalf("concurrent destination changed: %q, %v", got, err)
176 }
177 }
178
178 lines GO