master
go 208 lines 4.69 KB
Raw
1 package atomicfile
2
3 import (
4 "bytes"
5 "fmt"
6 "os"
7 "path/filepath"
8 "runtime"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 // TestNew_Success verifies atomic file creation
16 func TestNew_Success(t *testing.T) {
17 dir := t.TempDir()
18 path := filepath.Join(dir, "test.txt")
19
20 af, err := New(path, 0644)
21 require.NoError(t, err)
22 defer func() { _ = af.Abort() }()
23
24 // Verify temp file exists
25 assert.FileExists(t, af.File.Name())
26
27 // Verify temp file is in same directory
28 assert.Equal(t, dir, filepath.Dir(af.File.Name()))
29 }
30
31 // TestClose_Success verifies atomic replacement
32 func TestClose_Success(t *testing.T) {
33 dir := t.TempDir()
34 path := filepath.Join(dir, "test.txt")
35
36 af, err := New(path, 0644)
37 require.NoError(t, err)
38
39 content := []byte("test content")
40 _, err = af.Write(content)
41 require.NoError(t, err)
42
43 tempName := af.File.Name()
44
45 require.NoError(t, af.Close())
46
47 // Verify target file exists with correct content
48 data, err := os.ReadFile(path)
49 require.NoError(t, err)
50 assert.Equal(t, content, data)
51
52 // Verify temp file removed
53 assert.NoFileExists(t, tempName)
54 }
55
56 // TestAbort_Success verifies cleanup
57 func TestAbort_Success(t *testing.T) {
58 dir := t.TempDir()
59 path := filepath.Join(dir, "test.txt")
60
61 af, err := New(path, 0644)
62 require.NoError(t, err)
63
64 tempName := af.File.Name()
65
66 require.NoError(t, af.Abort())
67
68 // Verify temp file removed
69 assert.NoFileExists(t, tempName)
70
71 // Verify target not created
72 assert.NoFileExists(t, path)
73 }
74
75 // TestAbort_ErrorHandling tests error capture
76 func TestAbort_ErrorHandling(t *testing.T) {
77 dir := t.TempDir()
78 path := filepath.Join(dir, "test.txt")
79
80 af, err := New(path, 0644)
81 require.NoError(t, err)
82
83 // Close file to force close error
84 af.File.Close()
85
86 // Remove temp file to force remove error
87 os.Remove(af.File.Name())
88
89 err = af.Abort()
90 // Should get both errors
91 require.Error(t, err)
92 assert.Contains(t, err.Error(), "abort failed")
93 }
94
95 // TestClose_CloseError verifies cleanup on close failure
96 func TestClose_CloseError(t *testing.T) {
97 dir := t.TempDir()
98 path := filepath.Join(dir, "test.txt")
99
100 af, err := New(path, 0644)
101 require.NoError(t, err)
102
103 tempName := af.File.Name()
104
105 // Close file to force close error
106 af.File.Close()
107
108 err = af.Close()
109 require.Error(t, err)
110
111 // Verify temp file cleaned up even on error
112 assert.NoFileExists(t, tempName)
113 }
114
115 // TestReadFrom verifies io.Copy integration
116 func TestReadFrom(t *testing.T) {
117 dir := t.TempDir()
118 path := filepath.Join(dir, "test.txt")
119
120 af, err := New(path, 0644)
121 require.NoError(t, err)
122 defer func() { _ = af.Abort() }()
123
124 content := []byte("test content from reader")
125 n, err := af.ReadFrom(bytes.NewReader(content))
126 require.NoError(t, err)
127 assert.Equal(t, int64(len(content)), n)
128 }
129
130 // TestFilePermissions verifies mode is set correctly
131 func TestFilePermissions(t *testing.T) {
132 dir := t.TempDir()
133 path := filepath.Join(dir, "test.txt")
134
135 af, err := New(path, 0600)
136 require.NoError(t, err)
137
138 _, err = af.Write([]byte("test"))
139 require.NoError(t, err)
140
141 require.NoError(t, af.Close())
142
143 info, err := os.Stat(path)
144 require.NoError(t, err)
145
146 // On Unix, check exact permissions
147 if runtime.GOOS != "windows" {
148 mode := info.Mode().Perm()
149 assert.Equal(t, os.FileMode(0600), mode)
150 }
151 }
152
153 // TestMultipleAbortsSafe verifies calling Abort multiple times is safe
154 func TestMultipleAbortsSafe(t *testing.T) {
155 dir := t.TempDir()
156 path := filepath.Join(dir, "test.txt")
157
158 af, err := New(path, 0644)
159 require.NoError(t, err)
160
161 tempName := af.File.Name()
162
163 // First abort should succeed
164 require.NoError(t, af.Abort())
165 assert.NoFileExists(t, tempName, "temp file should be removed after first abort")
166
167 // Second abort should handle gracefully (file already gone)
168 err = af.Abort()
169 // Error is acceptable since file is already removed, but it should not panic
170 t.Logf("Second Abort() returned: %v", err)
171 }
172
173 // TestNoTempFilesAfterOperations verifies no .tmp-* files remain after operations
174 func TestNoTempFilesAfterOperations(t *testing.T) {
175 const testIterations = 5
176
177 tests := []struct {
178 name string
179 operation func(*File) error
180 }{
181 {"close", (*File).Close},
182 {"abort", (*File).Abort},
183 }
184
185 for _, tt := range tests {
186 t.Run(tt.name, func(t *testing.T) {
187 dir := t.TempDir()
188
189 // Perform multiple operations
190 for i := range testIterations {
191 path := filepath.Join(dir, fmt.Sprintf("test%d.txt", i))
192
193 af, err := New(path, 0644)
194 require.NoError(t, err)
195
196 _, err = af.Write([]byte("test data"))
197 require.NoError(t, err)
198
199 require.NoError(t, tt.operation(af))
200 }
201
202 // Check for any .tmp-* files
203 tmpFiles, err := filepath.Glob(filepath.Join(dir, ".tmp-*"))
204 require.NoError(t, err)
205 assert.Empty(t, tmpFiles, "should be no temp files after %s", tt.name)
206 })
207 }
208 }