diff --git a/e2e/run_test.go b/e2e/run_test.go index f54d16df0..c89c15875 100644 --- a/e2e/run_test.go +++ b/e2e/run_test.go @@ -2,6 +2,7 @@ package e2e import ( "fmt" + "os" "strings" "testing" "time" @@ -320,6 +321,46 @@ func TestRunDryRun(t *testing.T) { assert.Assert(t, ensureS3Object(s3client, bucket, files[2], "content")) } +// --dry-run run (cp s3://bucket/file dir/ from stdin) +func TestRunDryRunDownloadFromStdin(t *testing.T) { + t.Parallel() + + s3client, s5cmd := setup(t) + + bucket := s3BucketFromTestName(t) + createBucket(t, s3client, bucket) + + // Feed more than one read buffer's worth of commands so stdin is still + // being read while the first downloads complete. A dry-run download must + // hand out a file that is safe to close, i.e. one that does not alias + // stdin. + const numFiles = 200 + + var lines []string + expected := make(map[int]compareFunc) + for i := 0; i < numFiles; i++ { + filename := fmt.Sprintf("file%03d.txt", i) + putFile(t, s3client, bucket, filename, "content") + + lines = append(lines, fmt.Sprintf("cp s3://%v/%s dir/", bucket, filename)) + expected[i] = equals("cp s3://%v/%s dir/%s", bucket, filename, filename) + } + + input := strings.NewReader(strings.Join(lines, "\n")) + cmd := s5cmd("--dry-run", "run") + result := icmd.RunCmd(cmd, icmd.WithStdin(input)) + + result.Assert(t, icmd.Success) + + assertLines(t, result.Stdout(), expected, sortInput(true)) + + assertLines(t, result.Stderr(), map[int]compareFunc{}) + + // not even outermost directory should be created + _, err := os.Stat(cmd.Dir + "/dir") + assert.Assert(t, os.IsNotExist(err)) +} + func TestRunFixDataRace_Issue301(t *testing.T) { t.Parallel() diff --git a/storage/fs.go b/storage/fs.go index 294fe2a1e..07707db23 100644 --- a/storage/fs.go +++ b/storage/fs.go @@ -218,7 +218,7 @@ func (f *Filesystem) MkdirAll(path string) error { // Create creates a new os.File. func (f *Filesystem) Create(path string) (*os.File, error) { if f.dryRun { - return &os.File{}, nil + return os.OpenFile(os.DevNull, os.O_WRONLY, 0) } return os.Create(path) @@ -237,7 +237,7 @@ func (f *Filesystem) Open(path string) (*os.File, error) { // CreateTemp creates a new temporary file func (f *Filesystem) CreateTemp(dir, pattern string) (*os.File, error) { if f.dryRun { - return &os.File{}, nil + return os.OpenFile(os.DevNull, os.O_WRONLY, 0) } file, err := os.CreateTemp(dir, pattern) diff --git a/storage/fs_test.go b/storage/fs_test.go index c44305375..e27d0980b 100644 --- a/storage/fs_test.go +++ b/storage/fs_test.go @@ -1,6 +1,10 @@ package storage -import "testing" +import ( + "os" + "path/filepath" + "testing" +) func TestFilesystemImplementsStorageInterface(t *testing.T) { var i interface{} = new(Filesystem) @@ -8,3 +12,71 @@ func TestFilesystemImplementsStorageInterface(t *testing.T) { t.Errorf("expected %t to implement Storage interface", i) } } + +func TestFilesystemCreateDryRun(t *testing.T) { + t.Parallel() + + fs := &Filesystem{dryRun: true} + tmpDir := t.TempDir() + + testcases := []struct { + name string + create func() (*os.File, error) + }{ + { + name: "Create", + create: func() (*os.File, error) { + return fs.Create(filepath.Join(tmpDir, "should-not-exist.txt")) + }, + }, + { + name: "CreateTemp", + create: func() (*os.File, error) { + return fs.CreateTemp(tmpDir, "dryrun-*.txt") + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + f, err := tc.create() + if err != nil { + t.Fatalf("%s() returned error: %v", tc.name, err) + } + if f == nil { + t.Fatalf("%s() returned nil file", tc.name) + } + + if f.Name() != os.DevNull { + t.Fatalf("expected file name %q, got %q", os.DevNull, f.Name()) + } + + // the handle must not alias stdin, otherwise closing it would + // close stdin as well. + if f.Fd() == 0 { + t.Fatal("expected file descriptor other than stdin") + } + + if _, err := f.Write([]byte("content")); err != nil { + t.Fatalf("Write() returned error: %v", err) + } + + if err := f.Close(); err != nil { + t.Fatalf("Close() returned error: %v", err) + } + + if _, err := os.Stdin.Stat(); err != nil { + t.Fatalf("stdin is not usable after Close(): %v", err) + } + }) + } + + // no file should be created on disk during dry run + entries, err := os.ReadDir(tmpDir) + if err != nil { + t.Fatalf("ReadDir failed: %v", err) + } + if len(entries) != 0 { + t.Fatalf("expected no files in temp dir during dry run, got %d", len(entries)) + } +}