-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy patherror_handler_test.go
More file actions
344 lines (285 loc) · 9.82 KB
/
error_handler_test.go
File metadata and controls
344 lines (285 loc) · 9.82 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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// error_handler_test.go: Testing Argus Error Handling
//
// Copyright (c) 2025 AGILira - A. Giordano
// Series: an AGILira fragment
// SPDX-License-Identifier: MPL-2.0
package argus
import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
func TestErrorHandler_FileReadError(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
var capturedError error
var capturedPath string
var callbackCalled bool
var callbackCount int
var mu sync.Mutex
errorHandler := func(err error, path string) {
mu.Lock()
defer mu.Unlock()
capturedError = err
capturedPath = path
t.Logf("ErrorHandler called: %v", err)
}
config := Config{
PollInterval: 50 * time.Millisecond,
ErrorHandler: errorHandler,
}
// Note: We don't create the file initially to avoid the callback being called
watcher, err := UniversalConfigWatcherWithConfig(configPath, func(config map[string]interface{}) {
mu.Lock()
defer mu.Unlock()
callbackCalled = true
callbackCount++
t.Logf("Config callback called (count: %d): %+v", callbackCount, config)
}, config)
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer func() {
if err := watcher.Stop(); err != nil {
t.Logf("Failed to stop watcher: %v", err)
}
}()
// The watcher should already be started since UniversalConfigWatcherWithConfig auto-starts
// Wait for initial setup
time.Sleep(200 * time.Millisecond)
// Strategy: Create a file that can't be read due to permissions
// On Windows and Unix, we'll create a file with no read permissions
// Create the file first
if err := os.WriteFile(configPath, []byte(`{"test": true}`), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Wait for potential initial callback (this is expected behavior)
time.Sleep(300 * time.Millisecond)
// Reset callback tracking after potential initial call
mu.Lock()
initialCallbackCount := callbackCount
callbackCalled = false // Reset for the error test
callbackCount = 0 // Reset counter
capturedError = nil // Reset error tracking
mu.Unlock()
t.Logf("Initial callback count: %d, now testing error scenario", initialCallbackCount)
// Remove all permissions (including read) to cause read error
if err := os.Chmod(configPath, 0000); err != nil {
t.Fatalf("Failed to remove file permissions: %v", err)
}
// Restore permissions at the end for cleanup
defer func() {
if err := os.Chmod(configPath, 0644); err != nil {
t.Logf("Failed to restore file permissions: %v", err)
}
if err := os.Remove(configPath); err != nil {
t.Logf("Failed to remove config file: %v", err)
}
}()
// Trigger a change by updating file timestamp via touch
// This is more reliable cross-platform than trying to write to it
time.Sleep(100 * time.Millisecond)
// Use a more direct approach: temporarily restore write permission,
// modify content, then remove all permissions again
if err := os.Chmod(configPath, 0644); err != nil {
t.Logf("Failed to restore write permission: %v", err)
}
if err := os.WriteFile(configPath, []byte(`{"test": "modified"}`), 0644); err != nil {
t.Logf("Failed to modify config file: %v", err)
}
if err := os.Chmod(configPath, 0000); err != nil {
t.Logf("Failed to remove permissions again: %v", err)
}
// Wait for error to be captured with extended retry logic for CI
maxRetries := 20 // Extended for macOS CI timing
var finalCallbackCalled bool
var finalCallbackCount int
var finalError error
for i := 0; i < maxRetries; i++ {
mu.Lock()
finalCallbackCalled = callbackCalled
finalCallbackCount = callbackCount
finalError = capturedError
mu.Unlock()
// If we got an error, that's what we want (regardless of callback state)
if finalError != nil {
t.Logf("Error captured after %d attempts: %v", i+1, finalError)
break
}
time.Sleep(200 * time.Millisecond) // Extended timing for CI
}
mu.Lock()
defer mu.Unlock()
t.Logf("Final state - Callback called: %v, Callback count: %d, Error: %v",
finalCallbackCalled, finalCallbackCount, finalError)
// On some platforms (especially macOS), the behavior might be different
// The key requirement is that errors should be captured by ErrorHandler
if finalError == nil {
t.Skip("No read error was captured - this might be platform-dependent behavior (some systems allow reading despite permission restrictions)")
}
if capturedPath != configPath {
t.Errorf("Expected path %s, got %s", configPath, capturedPath)
}
// Check for various types of read errors that might occur across platforms
errorMsg := finalError.Error()
if !strings.Contains(errorMsg, "failed to read config file") &&
!strings.Contains(errorMsg, "permission denied") &&
!strings.Contains(errorMsg, "access is denied") &&
!strings.Contains(errorMsg, "operation not permitted") &&
!strings.Contains(errorMsg, "is a directory") {
t.Errorf("Expected error message about file read failure, got: %v", finalError)
}
// If callback was called despite the error, that's not ideal but might be platform behavior
if finalCallbackCalled {
t.Logf("Warning: Callback was called despite read error - this suggests platform-specific behavior")
}
}
func TestErrorHandler_ParseError(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
var capturedError error
var capturedPath string
var mu sync.Mutex
errorHandler := func(err error, path string) {
mu.Lock()
defer mu.Unlock()
capturedError = err
capturedPath = path
}
config := Config{
PollInterval: 50 * time.Millisecond,
ErrorHandler: errorHandler,
}
watcher, err := UniversalConfigWatcherWithConfig(configPath, func(config map[string]interface{}) {
// Should not be called due to parse error
t.Error("Callback should not be called when parsing fails")
}, config)
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer func() {
if err := watcher.Stop(); err != nil {
t.Logf("Failed to stop watcher: %v", err)
}
}()
// Write invalid JSON
if err := os.WriteFile(configPath, []byte(`{"test": invalid_json`), 0644); err != nil {
t.Fatalf("Failed to write invalid JSON: %v", err)
}
// Wait for error to be captured
time.Sleep(200 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if capturedError == nil {
t.Fatal("Expected parse error to be captured by ErrorHandler")
}
if capturedPath != configPath {
t.Errorf("Expected path %s, got %s", configPath, capturedPath)
}
if !strings.Contains(capturedError.Error(), "failed to parse") {
t.Errorf("Expected error message about parsing, got: %v", capturedError)
}
}
func TestErrorHandler_DefaultBehavior(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// Test that default error handler doesn't panic
config := Config{
PollInterval: 50 * time.Millisecond,
// No ErrorHandler set - should use default
}
watcher, err := UniversalConfigWatcherWithConfig(configPath, func(config map[string]interface{}) {
// Should not be called
}, config)
if err != nil {
t.Fatalf("Failed to create watcher: %v", err)
}
defer func() {
if err := watcher.Stop(); err != nil {
t.Logf("Failed to stop watcher: %v", err)
}
}()
// Write invalid JSON to trigger error
if err := os.WriteFile(configPath, []byte(`{"test": invalid`), 0644); err != nil {
t.Fatalf("Failed to write invalid JSON: %v", err)
}
// Wait a bit - should not panic
time.Sleep(200 * time.Millisecond)
// If we get here without panic, the default error handler works
t.Log("✅ Default error handler works without panicking")
}
func TestErrorHandler_StatError(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
var capturedError error
var capturedPath string
var mu sync.Mutex
errorHandler := func(err error, path string) {
mu.Lock()
defer mu.Unlock()
capturedError = err
capturedPath = path
}
config := Config{
PollInterval: 50 * time.Millisecond,
ErrorHandler: errorHandler,
}
watcher := New(config)
if err := watcher.Start(); err != nil {
t.Fatalf("Failed to start watcher: %v", err)
}
defer func() {
if err := watcher.Stop(); err != nil {
t.Logf("Failed to stop watcher: %v", err)
}
}()
// Create a file first
if err := os.WriteFile(configPath, []byte(`{"test": true}`), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
// Watch the file
if err := watcher.Watch(configPath, func(event ChangeEvent) {
// This callback is fine for normal operation
}); err != nil {
t.Fatalf("Failed to watch file: %v", err)
}
// Wait for initial stat
time.Sleep(100 * time.Millisecond)
// Create a directory with the same name to cause stat error
if err := os.Remove(configPath); err != nil {
t.Fatalf("Failed to remove file: %v", err)
}
if err := os.Mkdir(configPath, 0755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
defer func() {
if err := os.RemoveAll(configPath); err != nil {
t.Logf("Failed to cleanup configPath: %v", err)
}
}()
// Change the directory to make it inaccessible (permission denied)
if err := os.Chmod(configPath, 0000); err != nil {
t.Fatalf("Failed to change directory permissions: %v", err)
}
// Wait for error to be captured
time.Sleep(300 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
// This test might be flaky on some systems, so we'll be more lenient
// The main goal is to test that ErrorHandler gets called for non-NotExist errors
if capturedError != nil {
if capturedPath != configPath {
t.Errorf("Expected path %s, got %s", configPath, capturedPath)
}
if !strings.Contains(capturedError.Error(), "failed to stat file") {
t.Errorf("Expected error message about stat failure, got: %v", capturedError)
}
t.Log("✅ ErrorHandler correctly captured stat error")
} else {
// On some systems, the directory might still be readable, so this isn't a hard failure
t.Skip("Stat error was not captured - this might be system-dependent")
}
}