diff options
| author | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:58:06 +0200 |
|---|---|---|
| committer | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 21:58:06 +0200 |
| commit | 1043b58c164ac48757cdb373967ead2b34aa7388 (patch) | |
| tree | bc8286a404c17a5eec6a0de9b9adb1aa6201a413 /errors_test.go | |
| parent | 5a2d70be3ce8524c366f4dd303b950974f05aa7c (diff) | |
| download | sourcewatch-1043b58c164ac48757cdb373967ead2b34aa7388.tar.gz sourcewatch-1043b58c164ac48757cdb373967ead2b34aa7388.zip | |
Add structured error handling with retry logic
- errors.go: retry wrapper for transient errors (ENOENT),
structured error logging (disk full, permission denied, symlink loops)
- sync.go: continue on individual file errors, log clearly, skip broken
symlinks instead of failing entire sync
- watcher.go: log all errors with context, handle readlink failures
- Added 11 tests for error handling scenarios
- 50 tests passing
Diffstat (limited to 'errors_test.go')
| -rw-r--r-- | errors_test.go | 150 |
1 files changed, 150 insertions, 0 deletions
diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..553939d --- /dev/null +++ b/errors_test.go @@ -0,0 +1,150 @@ +package main + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + "syscall" + "testing" +) + +func TestCopyFile_Normal(t *testing.T) { + src := filepath.Join(t.TempDir(), "src.txt") + dst := filepath.Join(t.TempDir(), "dst.txt") + os.WriteFile(src, []byte("hello"), 0644) + + if err := copyFile(src, dst, 0644); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(dst) + if string(data) != "hello" { + t.Errorf("content = %q, want %q", string(data), "hello") + } +} + +func TestCopyFile_SrcNotExist(t *testing.T) { + dst := filepath.Join(t.TempDir(), "dst.txt") + err := copyFile("/nonexistent/file", dst, 0644) + if err == nil { + t.Fatal("expected error for nonexistent source") + } + if !os.IsNotExist(err) { + t.Errorf("expected ErrNotExist, got: %v", err) + } +} + +func TestCopyFile_DstPermDenied(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("running as root, permission test not applicable") + } + + src := filepath.Join(t.TempDir(), "src.txt") + os.WriteFile(src, []byte("hello"), 0644) + + dst := "/proc/nonexistent/file" + err := copyFile(src, dst, 0644) + if err == nil { + t.Fatal("expected error for invalid dst") + } +} + +func TestRetry_TransientError(t *testing.T) { + calls := 0 + err := retry("test", func() error { + calls++ + if calls < 3 { + return &fs.PathError{Op: "open", Path: "/tmp/test", Err: fs.ErrNotExist} + } + return nil + }) + if err != nil { + t.Errorf("expected success after retries, got: %v", err) + } + if calls != 3 { + t.Errorf("calls = %d, want 3", calls) + } +} + +func TestRetry_PermanentError(t *testing.T) { + calls := 0 + err := retry("test", func() error { + calls++ + return &fs.PathError{Op: "write", Path: "/tmp/test", Err: syscall.ENOSPC} + }) + if err == nil { + t.Fatal("expected error") + } + if calls != 1 { + t.Errorf("calls = %d, want 1 (no retries for ENOSPC)", calls) + } +} + +func TestIsRetryable(t *testing.T) { + tests := []struct { + err error + expected bool + }{ + {fs.ErrNotExist, true}, + {&fs.PathError{Op: "open", Path: "/tmp", Err: fs.ErrNotExist}, true}, + {&fs.PathError{Op: "write", Path: "/tmp", Err: syscall.ENOSPC}, false}, + {syscall.ENOSPC, false}, + {syscall.EACCES, false}, + } + + for _, tt := range tests { + if got := isRetryable(tt.err); got != tt.expected { + t.Errorf("isRetryable(%v) = %v, want %v", tt.err, got, tt.expected) + } + } +} + +func TestLogError_DiskFull(t *testing.T) { + logError("copy", "test.txt", syscall.ENOSPC) +} + +func TestLogError_PermissionDenied(t *testing.T) { + logError("write", "test.txt", syscall.EACCES) +} + +func TestLogError_NotExist(t *testing.T) { + logError("stat", "test.txt", fs.ErrNotExist) +} + +func TestLogError_Nil(t *testing.T) { + logError("test", "file.txt", nil) +} + +func TestSyncError(t *testing.T) { + err := &SyncError{ + Path: "test.txt", + Op: "copy", + Err: syscall.ENOSPC, + } + + if !strings.Contains(err.Error(), "copy") { + t.Errorf("error should contain op, got: %s", err.Error()) + } + if !strings.Contains(err.Error(), "test.txt") { + t.Errorf("error should contain path, got: %s", err.Error()) + } + if err.Unwrap() != syscall.ENOSPC { + t.Errorf("unwrap should return underlying error") + } +} + +func TestCopyFileWithRetry_Success(t *testing.T) { + src := filepath.Join(t.TempDir(), "src.txt") + dst := filepath.Join(t.TempDir(), "dst.txt") + os.WriteFile(src, []byte("content"), 0644) + + if err := copyFileWithRetry(src, dst, 0644); err != nil { + t.Fatal(err) + } + + data, _ := os.ReadFile(dst) + if string(data) != "content" { + t.Errorf("content = %q, want %q", string(data), "content") + } +} |
