-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbackup_test.go
More file actions
105 lines (91 loc) · 3.2 KB
/
backup_test.go
File metadata and controls
105 lines (91 loc) · 3.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Copyright 2025 The Sqlite Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sqlite // import "modernc.org/sqlite"
import (
"context"
"os"
"path/filepath"
"testing"
)
// TestBackupCommitClosesConnOnError verifies that Commit() closes the
// destination connection when sqlite3_backup_finish returns an error.
// Before the fix, the error branch returned (nil, err) without closing
// dstConn, leaking the file descriptor, TLS, and memory.
//
// Strategy: create a backup whose destination is a file-based database,
// then make the destination file read-only so that backup_step fails
// with an I/O or read-only error. Commit() must still close the
// destination connection.
func TestBackupCommitClosesConnOnError(t *testing.T) {
// Create source in-memory database with some data.
srcConn, err := newConn(":memory:")
if err != nil {
t.Fatal(err)
}
defer srcConn.Close()
// Populate source so there is at least one page to copy.
if err := execConn(srcConn, "CREATE TABLE t(x)"); err != nil {
t.Fatal(err)
}
if err := execConn(srcConn, "INSERT INTO t VALUES('hello')"); err != nil {
t.Fatal(err)
}
// Create a temp directory for the destination database.
tmpDir, err := os.MkdirTemp("", "backup_commit_test_")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
dstPath := filepath.Join(tmpDir, "dst.db")
// Create the backup object (this opens the destination connection).
bck, err := srcConn.NewBackup(dstPath)
if err != nil {
t.Fatal(err)
}
// Make the destination directory read-only so that writes to the
// destination database fail. This causes backup_step to return an
// error (SQLITE_READONLY or SQLITE_IOERR), which backup_finish
// propagates.
if err := os.Chmod(dstPath, 0o444); err != nil {
t.Skipf("chmod not supported: %v", err)
}
if err := os.Chmod(tmpDir, 0o555); err != nil {
t.Skipf("chmod not supported: %v", err)
}
// Restore write permission on cleanup so RemoveAll succeeds.
defer os.Chmod(tmpDir, 0o755)
// Step should fail because the destination is read-only.
_, stepErr := bck.Step(-1)
// Whether or not Step reported the error, Commit must propagate the
// error from backup_finish and close the destination connection.
conn, commitErr := bck.Commit()
if stepErr == nil && commitErr == nil {
// If neither errored, the test setup didn't achieve its goal.
// Close the returned connection to avoid a leak in this edge case
// and skip.
if conn != nil {
conn.Close()
}
t.Log("chmod did not restrict writes on this platform")
t.Skip("could not provoke backup error on this platform; skipping")
}
if conn != nil {
t.Fatal("Commit() must return nil conn on error")
}
// The critical assertion: the destination connection must be closed.
// After Close(), db is set to 0.
if bck.dstConn.db != 0 {
t.Fatal("Commit() did not close the destination connection on error")
}
}
// execConn is a test helper that executes a SQL statement on a raw conn.
func execConn(c *conn, sql string) error {
s, err := c.prepare(context.Background(), sql)
if err != nil {
return err
}
defer s.Close()
_, err = s.(*stmt).ExecContext(context.Background(), nil)
return err
}