Files
picoclaw/pkg/fileutil/file_test.go
T
perhapzz 0276554d9c test(fileutil,health): add unit tests for WriteFileAtomic and health server
Add comprehensive test coverage for two previously untested packages:

pkg/fileutil (9 tests):
- Basic write and read-back
- File permissions (0600)
- Overwrite existing files
- Empty data handling
- Nested directory auto-creation
- No temp files left after success
- Large file (1MB) handling
- Concurrent write safety
- Invalid path error handling

pkg/health (15 tests):
- Health endpoint returns 200 with status, uptime, pid
- Ready endpoint returns 503 when not ready
- Ready endpoint returns 200 when ready
- Ready fails when any registered check fails
- Ready passes with all checks passing
- Reload rejects non-POST methods
- Reload returns 503 when no reload func set
- Reload calls registered function on success
- Reload returns 500 on function error
- SetReady toggle behavior
- Multiple health checks interaction
- RegisterOnMux works with custom ServeMux
- NewServer defaults
- StartContext graceful shutdown on cancel
- statusString helper
2026-03-20 10:41:08 +00:00

177 lines
4.0 KiB
Go

package fileutil
import (
"os"
"path/filepath"
"sync"
"testing"
)
func TestWriteFileAtomic_Basic(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
data := []byte("hello picoclaw")
err := WriteFileAtomic(path, data, 0o644)
if err != nil {
t.Fatalf("WriteFileAtomic failed: %v", err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile failed: %v", err)
}
if string(got) != string(data) {
t.Errorf("got %q, want %q", got, data)
}
}
func TestWriteFileAtomic_Permissions(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "secret.txt")
err := WriteFileAtomic(path, []byte("secret"), 0o600)
if err != nil {
t.Fatalf("WriteFileAtomic failed: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatalf("Stat failed: %v", err)
}
// On Unix, check file mode (ignoring directory bits)
if got := info.Mode().Perm(); got != 0o600 {
t.Errorf("permissions = %o, want %o", got, 0o600)
}
}
func TestWriteFileAtomic_Overwrite(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "overwrite.txt")
// Write initial content
if err := WriteFileAtomic(path, []byte("old"), 0o644); err != nil {
t.Fatalf("first write failed: %v", err)
}
// Overwrite
if err := WriteFileAtomic(path, []byte("new"), 0o644); err != nil {
t.Fatalf("second write failed: %v", err)
}
got, _ := os.ReadFile(path)
if string(got) != "new" {
t.Errorf("got %q after overwrite, want %q", got, "new")
}
}
func TestWriteFileAtomic_EmptyData(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "empty.txt")
err := WriteFileAtomic(path, []byte{}, 0o644)
if err != nil {
t.Fatalf("WriteFileAtomic with empty data failed: %v", err)
}
got, _ := os.ReadFile(path)
if len(got) != 0 {
t.Errorf("expected empty file, got %d bytes", len(got))
}
}
func TestWriteFileAtomic_CreatesParentDirs(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "a", "b", "c", "deep.txt")
err := WriteFileAtomic(path, []byte("deep"), 0o644)
if err != nil {
t.Fatalf("WriteFileAtomic with nested dirs failed: %v", err)
}
got, _ := os.ReadFile(path)
if string(got) != "deep" {
t.Errorf("got %q, want %q", got, "deep")
}
}
func TestWriteFileAtomic_NoTempFileOnSuccess(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "clean.txt")
if err := WriteFileAtomic(path, []byte("data"), 0o644); err != nil {
t.Fatalf("WriteFileAtomic failed: %v", err)
}
// Verify no temp files remain
entries, _ := os.ReadDir(dir)
for _, e := range entries {
if e.Name() != "clean.txt" {
t.Errorf("unexpected file remaining: %s", e.Name())
}
}
}
func TestWriteFileAtomic_LargeFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "large.bin")
// 1MB of data
data := make([]byte, 1<<20)
for i := range data {
data[i] = byte(i % 256)
}
if err := WriteFileAtomic(path, data, 0o644); err != nil {
t.Fatalf("WriteFileAtomic with large file failed: %v", err)
}
got, _ := os.ReadFile(path)
if len(got) != len(data) {
t.Errorf("file size = %d, want %d", len(got), len(data))
}
}
func TestWriteFileAtomic_Concurrent(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "concurrent.txt")
var wg sync.WaitGroup
errs := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
data := []byte(string(rune('A' + n)))
if err := WriteFileAtomic(path, data, 0o644); err != nil {
errs <- err
}
}(i)
}
wg.Wait()
close(errs)
for err := range errs {
t.Errorf("concurrent write error: %v", err)
}
// File should exist and contain exactly 1 byte (last writer wins)
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile after concurrent writes failed: %v", err)
}
if len(got) != 1 {
t.Errorf("expected 1 byte after concurrent writes, got %d", len(got))
}
}
func TestWriteFileAtomic_InvalidPath(t *testing.T) {
// /dev/null/impossible is not a valid path on any OS
err := WriteFileAtomic("/dev/null/impossible/file.txt", []byte("data"), 0o644)
if err == nil {
t.Error("expected error for invalid path, got nil")
}
}