package fileutil import ( "bytes" "errors" "io/fs" "log/slog" "os" "path/filepath" "testing" ) // errSimulatedFailure is a sentinel used in callback-error tests. var errSimulatedFailure = errors.New("simulated write failure") func TestAtomicWrite_Success(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "target.txt") // Create target with known content and non-default permissions. if err := os.WriteFile(target, []byte("original"), 0o755); err != nil { //nolint:mnd t.Fatalf("setup: write target: %v", err) } err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { _, writeErr := tmp.WriteString("replaced") return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } // Verify content was replaced. got, err := os.ReadFile(target) if err != nil { t.Fatalf("read target: %v", err) } if string(got) != "replaced" { t.Errorf("content: got %q, want %q", got, "replaced") } // Verify permissions preserved. info, err := os.Stat(target) if err != nil { t.Fatalf("stat target: %v", err) } if info.Mode().Perm() != 0o755 { //nolint:mnd t.Errorf("permissions: got %o, want %o", info.Mode().Perm(), 0o755) //nolint:mnd } // Verify no temp file remains. tmpPath := target + tmpSuffix if _, err := os.Stat(tmpPath); !errors.Is(err, fs.ErrNotExist) { t.Errorf("temp file should not exist, got err: %v", err) } } func TestAtomicWrite_NewFile(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "newfile.txt") err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { _, writeErr := tmp.WriteString("brand new") return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } got, err := os.ReadFile(target) if err != nil { t.Fatalf("read target: %v", err) } if string(got) != "brand new" { t.Errorf("content: got %q, want %q", got, "brand new") } // Default permissions for a new file should be 0644. info, err := os.Stat(target) if err != nil { t.Fatalf("stat target: %v", err) } if info.Mode().Perm() != 0o644 { //nolint:mnd t.Errorf("permissions: got %o, want %o", info.Mode().Perm(), 0o644) //nolint:mnd } // No temp file should remain. tmpPath := target + tmpSuffix if _, err := os.Stat(tmpPath); !errors.Is(err, fs.ErrNotExist) { t.Errorf("temp file should not exist, got err: %v", err) } } func TestAtomicWrite_CallbackError(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "unchanged.txt") original := []byte("keep me") if err := os.WriteFile(target, original, 0o644); err != nil { //nolint:mnd t.Fatalf("setup: write target: %v", err) } err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { // Write partial data then return an error. _, _ = tmp.WriteString("partial") return errSimulatedFailure }) if !errors.Is(err, errSimulatedFailure) { t.Fatalf("expected callback error, got: %v", err) } // Original content must be untouched. got, err := os.ReadFile(target) if err != nil { t.Fatalf("read target: %v", err) } if !bytes.Equal(got, original) { t.Errorf("content: got %q, want %q", got, original) } // Temp file must be cleaned up. tmpPath := target + tmpSuffix if _, err := os.Stat(tmpPath); !errors.Is(err, fs.ErrNotExist) { t.Errorf("temp file should not exist after callback error, got err: %v", err) } } func TestAtomicWrite_OrphanCleanup(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "song.mp3") tmpPath := target + tmpSuffix // Simulate orphaned temp file from a previous crash. if err := os.WriteFile(tmpPath, []byte("orphaned data"), 0o644); err != nil { //nolint:mnd t.Fatalf("setup: create orphan: %v", err) } err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { _, writeErr := tmp.WriteString("real data") return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } // Target should have the correct content. got, err := os.ReadFile(target) if err != nil { t.Fatalf("read target: %v", err) } if string(got) != "real data" { t.Errorf("content: got %q, want %q", got, "real data") } // No temp file should remain. if _, err := os.Stat(tmpPath); !errors.Is(err, fs.ErrNotExist) { t.Errorf("orphan temp file should have been cleaned up, got err: %v", err) } } func TestAtomicWrite_SameDirectoryTempFile(t *testing.T) { t.Parallel() dir := t.TempDir() subdir := filepath.Join(dir, "music") if err := os.MkdirAll(subdir, 0o755); err != nil { //nolint:mnd t.Fatalf("setup: mkdir: %v", err) } target := filepath.Join(subdir, "track.flac") expectedTmp := target + tmpSuffix var observedTmpPath string err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { observedTmpPath = tmp.Name() _, writeErr := tmp.WriteString("flac data") return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } // Verify the temp file was created in the same directory as the target. if observedTmpPath != expectedTmp { t.Errorf("temp path: got %q, want %q", observedTmpPath, expectedTmp) } // The temp file's directory must match the target's directory. if filepath.Dir(observedTmpPath) != filepath.Dir(target) { t.Errorf( "temp dir %q differs from target dir %q — cross-device rename would fail", filepath.Dir(observedTmpPath), filepath.Dir(target), ) } } func TestAtomicWrite_PermissionPreservation(t *testing.T) { t.Parallel() modes := []fs.FileMode{0o644, 0o755, 0o600} //nolint:mnd for _, mode := range modes { t.Run(mode.String(), func(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "file.dat") if err := os.WriteFile(target, []byte("old"), mode); err != nil { t.Fatalf("setup: write target: %v", err) } err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { _, writeErr := tmp.WriteString("new") return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } info, err := os.Stat(target) if err != nil { t.Fatalf("stat target: %v", err) } if info.Mode().Perm() != mode { t.Errorf("permissions: got %o, want %o", info.Mode().Perm(), mode) } }) } } func TestAtomicWrite_SyncAndClose(t *testing.T) { t.Parallel() dir := t.TempDir() target := filepath.Join(dir, "large.bin") // Write 1 MiB of data. const size = 1 << 20 //nolint:mnd data := bytes.Repeat([]byte("x"), size) err := AtomicWrite(slog.Default(), target, func(tmp *os.File) error { _, writeErr := tmp.Write(data) return writeErr }) if err != nil { t.Fatalf("AtomicWrite: %v", err) } info, err := os.Stat(target) if err != nil { t.Fatalf("stat target: %v", err) } if info.Size() != size { t.Errorf("file size: got %d, want %d", info.Size(), size) } got, err := os.ReadFile(target) if err != nil { t.Fatalf("read target: %v", err) } if !bytes.Equal(got, data) { t.Errorf("content mismatch for %d byte file", size) } }