Files
goget/internal/output/writer_test.go
T

233 lines
6.4 KiB
Go
Raw Normal View History

//go:build linux || freebsd
// +build linux freebsd
package output
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestCloseAtomicSuccess verifies the happy path: temp file is renamed to
// the final destination and no temp file remains.
func TestCloseAtomicSuccess(t *testing.T) {
dir := tempDir(t)
outputPath := filepath.Join(dir, "output.txt")
w, err := NewWriter(&WriterConfig{
Output: outputPath,
Atomic: true,
})
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
payload := []byte("hello world")
if _, err := w.Write(payload); err != nil {
t.Fatalf("Write: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
got, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(got) != string(payload) {
t.Errorf("payload = %q, want %q", got, payload)
}
// No leftover temp files in the directory.
entries, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("ReadDir: %v", err)
}
for _, e := range entries {
if strings.Contains(e.Name(), ".tmp.") {
t.Errorf("temp file %q should have been renamed away", e.Name())
}
}
}
// TestCloseKeepsTempOnRenameError is a regression guard for the BACKLOG
// entry "Atomic write data loss on cross-filesystem rename". The previous
// implementation called os.Remove(tempPath) on any rename error, deleting
// the only copy of the downloaded data. After the fix, the temp file must
// remain on disk so the user can recover the partial download manually.
//
// We force a rename error by pre-creating the destination as a directory —
// os.Rename will refuse to overwrite a non-empty directory with a file.
func TestCloseKeepsTempOnRenameError(t *testing.T) {
dir := tempDir(t)
outputPath := filepath.Join(dir, "output.txt")
// Block the rename by making the target path a directory.
if err := os.Mkdir(outputPath, 0755); err != nil {
t.Fatalf("mkdir: %v", err)
}
w, err := NewWriter(&WriterConfig{
Output: outputPath,
Atomic: true,
})
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
payload := []byte("recoverable download data")
if _, err := w.Write(payload); err != nil {
t.Fatalf("Write: %v", err)
}
tempPath := w.tempFile.Name()
if err := w.Close(); err == nil {
t.Fatal("expected Close() to return an error when target is a directory")
}
// The temp file MUST still exist — the data must be recoverable.
if _, err := os.Stat(tempPath); err != nil {
t.Errorf("temp file %q should still exist after Close error, got: %v", tempPath, err)
}
// And it should still hold the full payload.
got, err := os.ReadFile(tempPath)
if err != nil {
t.Fatalf("ReadFile(temp): %v", err)
}
if string(got) != string(payload) {
t.Errorf("temp file payload = %q, want %q", got, payload)
}
}
// TestCopyFileAndRemove covers the EXDEV fallback helper directly. We do
// not have an easy way to mount two different filesystems in a unit test,
// so we just verify the copy + remove behaviour on the same filesystem.
func TestCopyFileAndRemove(t *testing.T) {
dir := tempDir(t)
src := filepath.Join(dir, "src.tmp")
dst := filepath.Join(dir, "dst.bin")
payload := []byte("cross-fs fallback payload")
if err := os.WriteFile(src, payload, 0644); err != nil {
t.Fatalf("WriteFile(src): %v", err)
}
if err := copyFileAndRemove(src, dst); err != nil {
t.Fatalf("copyFileAndRemove: %v", err)
}
got, err := os.ReadFile(dst)
if err != nil {
t.Fatalf("ReadFile(dst): %v", err)
}
if string(got) != string(payload) {
t.Errorf("dst = %q, want %q", got, payload)
}
if _, err := os.Stat(src); !os.IsNotExist(err) {
t.Errorf("src should be removed, got stat err = %v", err)
}
}
// TestCopyFileAndRemoveOverwritesExisting verifies the fallback truncates
// any existing file at the destination (matching os.Rename semantics).
func TestCopyFileAndRemoveOverwritesExisting(t *testing.T) {
dir := tempDir(t)
src := filepath.Join(dir, "src.tmp")
dst := filepath.Join(dir, "dst.bin")
if err := os.WriteFile(src, []byte("new"), 0644); err != nil {
t.Fatalf("WriteFile(src): %v", err)
}
if err := os.WriteFile(dst, []byte("OLD CONTENT"), 0644); err != nil {
t.Fatalf("WriteFile(dst): %v", err)
}
if err := copyFileAndRemove(src, dst); err != nil {
t.Fatalf("copyFileAndRemove: %v", err)
}
got, err := os.ReadFile(dst)
if err != nil {
t.Fatalf("ReadFile(dst): %v", err)
}
if string(got) != "new" {
t.Errorf("dst = %q, want %q", got, "new")
}
}
// TestNewWriterCreateDirsMissingParent verifies the curl --create-dirs
// equivalent: when CreateDirs is true and the parent directory of Output
// does not exist, NewWriter must create the full chain via MkdirAll and
// successfully write the file. This is the happy path for "goget
// --output ./data/archives/file.zip" against a fresh tree.
func TestNewWriterCreateDirsMissingParent(t *testing.T) {
dir := tempDir(t)
outputPath := filepath.Join(dir, "deeply", "nested", "subdir", "file.dat")
w, err := NewWriter(&WriterConfig{
Output: outputPath,
Atomic: true,
CreateDirs: true,
})
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
payload := []byte("created in a fresh dir tree")
if _, err := w.Write(payload); err != nil {
t.Fatalf("Write: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
got, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(got) != string(payload) {
t.Errorf("payload = %q, want %q", got, payload)
}
}
// TestNewWriterCreateDirsDisabledLeavesError confirms that without
// CreateDirs the writer still surfaces the underlying "parent dir missing"
// error — CreateDirs is opt-in, not a silent behaviour change for existing
// users.
func TestNewWriterCreateDirsDisabledLeavesError(t *testing.T) {
dir := tempDir(t)
outputPath := filepath.Join(dir, "missing", "file.dat")
_, err := NewWriter(&WriterConfig{
Output: outputPath,
Atomic: true,
CreateDirs: false,
})
if err == nil {
t.Fatal("expected NewWriter to fail when parent dir is missing and CreateDirs=false")
}
}
// TestNewWriterCreateDirsIgnoredForStdout makes sure the - / stdout path is
// untouched by the new flag: nothing to create, no spurious error.
func TestNewWriterCreateDirsIgnoredForStdout(t *testing.T) {
w, err := NewWriter(&WriterConfig{
Output: "-",
Atomic: false,
CreateDirs: true,
})
if err != nil {
t.Fatalf("NewWriter: %v", err)
}
if w.file != os.Stdout {
t.Errorf("expected writer to point at stdout, got %v", w.file)
}
w.Close()
}