github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/cp/cmd_test.go (about)

     1  // Copyright 2016 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // created by Manoel Vilela <manoel_vilela@engineer.com>
     6  
     7  package cp
     8  
     9  import (
    10  	"bufio"
    11  	"bytes"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"math/rand"
    17  	"os"
    18  	"path/filepath"
    19  	"reflect"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/mvdan/u-root-coreutils/pkg/uio"
    24  	"golang.org/x/sys/unix"
    25  )
    26  
    27  const (
    28  	maxSizeFile = 1000
    29  	maxDirDepth = 5
    30  	maxFiles    = 5
    31  )
    32  
    33  // randomFile create a random file with random content
    34  func randomFile(fpath, prefix string) (*os.File, error) {
    35  	f, err := os.CreateTemp(fpath, prefix)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	// generate random content for files
    40  	bytes := []byte{}
    41  	for i := 0; i < rand.Intn(maxSizeFile); i++ {
    42  		bytes = append(bytes, byte(i))
    43  	}
    44  	f.Write(bytes)
    45  
    46  	return f, nil
    47  }
    48  
    49  // createFilesTree create a random files tree
    50  func createFilesTree(root string, maxDepth, depth int) error {
    51  	// create more one dir if don't achieve the maxDepth
    52  	if depth < maxDepth {
    53  		newDir, err := os.MkdirTemp(root, fmt.Sprintf("cpdir_%d_", depth))
    54  		if err != nil {
    55  			return err
    56  		}
    57  
    58  		if err = createFilesTree(newDir, maxDepth, depth+1); err != nil {
    59  			return err
    60  		}
    61  	}
    62  	// generate random files
    63  	for i := 0; i < maxFiles; i++ {
    64  		f, err := randomFile(root, fmt.Sprintf("cpfile_%d_", i))
    65  		if err != nil {
    66  			return err
    67  		}
    68  		f.Close()
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  func TestRunSimple(t *testing.T) {
    75  	tmpDir := t.TempDir()
    76  	file1, err := randomFile(tmpDir, "src-")
    77  	if err != nil {
    78  		t.Errorf("failed to create tmp dir: %q", err)
    79  	}
    80  
    81  	for _, tt := range []struct {
    82  		name    string
    83  		flag    flags
    84  		args    []string
    85  		input   string
    86  		wantErr error
    87  	}{
    88  		{
    89  			name: "NoFlags-Success-",
    90  			args: []string{file1.Name(), filepath.Join(tmpDir, "destination")},
    91  		},
    92  		{
    93  			name: "AskYes-Success-",
    94  			args: []string{file1.Name(), filepath.Join(tmpDir, "destination")},
    95  			flag: flags{
    96  				ask: true,
    97  			},
    98  			input: "yes\n",
    99  		},
   100  		{
   101  			name: "AskYes-Fail1-",
   102  			args: []string{file1.Name(), filepath.Join(tmpDir, "destination")},
   103  			flag: flags{
   104  				ask: true,
   105  			},
   106  			input:   "yes",
   107  			wantErr: io.EOF,
   108  		},
   109  		{
   110  			name: "AskYes-Fail2-",
   111  			args: []string{file1.Name(), filepath.Join(tmpDir, "destination")},
   112  			flag: flags{
   113  				ask: true,
   114  			},
   115  			input:   "no\n",
   116  			wantErr: ErrSkip,
   117  		},
   118  		{
   119  			name: "Verbose",
   120  			args: []string{file1.Name(), filepath.Join(tmpDir, "destination")},
   121  			flag: flags{
   122  				verbose: true,
   123  			},
   124  			wantErr: ErrSkip,
   125  		},
   126  		{
   127  			name:    "SameFile-NoFlags",
   128  			args:    []string{file1.Name(), file1.Name()},
   129  			wantErr: ErrSkip,
   130  		},
   131  		{
   132  			name:    "NoFlags-Fail-SrcNotExist",
   133  			args:    []string{"src", filepath.Join(tmpDir, "destination")},
   134  			wantErr: fs.ErrNotExist,
   135  		},
   136  		{
   137  			name:    "NoFlags-Fail-DstcNotExist",
   138  			args:    []string{file1.Name(), "dst"},
   139  			wantErr: fs.ErrNotExist,
   140  		},
   141  		{
   142  			name:    "NoFlags-ToManyArgs-",
   143  			args:    []string{file1.Name(), "dst", "src"},
   144  			wantErr: unix.ENOTDIR,
   145  		},
   146  	} {
   147  		t.Run(tt.name, func(t *testing.T) {
   148  			var out bytes.Buffer
   149  			var inBuf bytes.Buffer
   150  			fmt.Fprintf(&inBuf, "%s", tt.input)
   151  			in := bufio.NewReader(&inBuf)
   152  			if err := run(RunParams{Stdin: in, Stderr: &out}, tt.args, tt.flag); err != nil {
   153  				if !errors.Is(err, tt.wantErr) {
   154  					t.Errorf(`run(tt.args, tt.flag, &out, in) = %q, not %q`, err.Error(), tt.wantErr)
   155  				}
   156  				return
   157  			}
   158  
   159  			if err := IsEqualTree(Default, tt.args[0], tt.args[1]); err != nil {
   160  				t.Errorf(`EqualTree(Default, tt.args[0], tt.args[1]) = %q, not nil`, err)
   161  			}
   162  		})
   163  
   164  		t.Run(tt.name+"PreCallBack", func(t *testing.T) {
   165  			var out bytes.Buffer
   166  			var inBuf bytes.Buffer
   167  			fmt.Fprintf(&inBuf, "%s", tt.input)
   168  			in := bufio.NewReader(&inBuf)
   169  			f := setupPreCallback(tt.flag.recursive, tt.flag.ask, tt.flag.force, &out, *in)
   170  			srcfi, err := os.Stat(tt.args[0])
   171  			// If the src file does not exist, there is no point in continue, but it is not an error so the say.
   172  			// Also we catch that error in the previous test
   173  			if errors.Is(err, fs.ErrNotExist) {
   174  				return
   175  			}
   176  			if err := f(tt.args[0], tt.args[1], srcfi); !errors.Is(err, ErrSkip) {
   177  				t.Logf(`preCallback(tt.args[0], tt.args[1], srcfi) = %q, not ErrSkip`, err)
   178  			}
   179  		})
   180  
   181  		t.Run(tt.name+"PostCallBack", func(t *testing.T) {
   182  			var out bytes.Buffer
   183  			f := setupPostCallback(tt.flag.verbose, &out)
   184  			f(tt.args[0], tt.args[1])
   185  			if tt.flag.verbose {
   186  				if out.String() != fmt.Sprintf("%q -> %q\n", tt.args[0], tt.args[1]) {
   187  					t.Errorf("postCallback(tt.args[0], tt.args[1]) = %q, not %q", out.String(), fmt.Sprintf("%q -> %q\n", tt.args[0], tt.args[1]))
   188  				}
   189  			}
   190  		})
   191  	}
   192  }
   193  
   194  // TestCpSrcDirectory tests copying source to destination without recursive
   195  // cmd-line equivalent: cp ~/dir ~/dir2
   196  func TestCpSrcDirectory(t *testing.T) {
   197  	var f flags
   198  
   199  	tempDir := t.TempDir()
   200  	tempDirTwo := t.TempDir()
   201  
   202  	// capture log output to verify
   203  	var logBytes bytes.Buffer
   204  	var in bufio.Reader
   205  
   206  	if err := run(RunParams{Stdin: &in, Stderr: &logBytes}, []string{tempDir, tempDirTwo}, f); err != nil {
   207  		t.Fatalf(`run([]string{tempDir, tempDirTwo}, f, &logBytes, &in) = %q, not nil`, err)
   208  	}
   209  
   210  	outString := fmt.Sprintf("cp: -r not specified, omitting directory %s", tempDir)
   211  	capturedString := logBytes.String()
   212  	if !strings.Contains(capturedString, outString) {
   213  		t.Fatal("strings.Contains(capturedString, outString) = false, not true")
   214  	}
   215  }
   216  
   217  // TestCpRecursive tests the recursive mode copy
   218  // Copy dir hierarchies src-dir to dst-dir
   219  // whose src-dir and dst-dir already exists
   220  // cmd-line equivalent: $ cp -R src-dir/ dst-dir/
   221  func TestCpRecursive(t *testing.T) {
   222  	var f flags
   223  	f.recursive = true
   224  
   225  	tempDir := t.TempDir()
   226  
   227  	srcDir := filepath.Join(tempDir, "src")
   228  	if err := os.Mkdir(srcDir, 0o755); err != nil {
   229  		t.Fatalf(`os.Mkdir(srcDir, 0o755) = %q, not nil`, err)
   230  	}
   231  	dstDir := filepath.Join(tempDir, "dst-exists")
   232  	if err := os.Mkdir(dstDir, 0o755); err != nil {
   233  		t.Fatalf(`os.Mkdir(dstDir, 0o755) = %q, not nil`, err)
   234  	}
   235  
   236  	if err := createFilesTree(srcDir, maxDirDepth, 0); err != nil {
   237  		t.Fatalf(`createFilesTree(srcDir, maxDirDepth, 0) = %q, not nil`, err)
   238  	}
   239  
   240  	t.Run("existing-dst-dir", func(t *testing.T) {
   241  		var out bytes.Buffer
   242  		var in bufio.Reader
   243  		if err := run([]string{srcDir, dstDir}, f, &out, &in); err != nil {
   244  			t.Fatalf(`run([]string{srcDir, dstDir}, f, &out, &in) = %q, not nil`, err)
   245  		}
   246  		// Because dstDir already existed, a new dir was created inside it.
   247  		realDestination := filepath.Join(dstDir, filepath.Base(srcDir))
   248  		if err := IsEqualTree(Default, srcDir, realDestination); err != nil {
   249  			t.Fatalf(`IsEqualTree(Default, srcDir, realDestination) = %q, not nil`, err)
   250  		}
   251  	})
   252  
   253  	t.Run("non-existing-dst-dir", func(t *testing.T) {
   254  		var out bytes.Buffer
   255  		var in bufio.Reader
   256  		notExistDstDir := filepath.Join(tempDir, "dst-does-not-exist")
   257  		if err := run([]string{srcDir, notExistDstDir}, f, &out, &in); err != nil {
   258  			t.Fatalf(`run([]string{srcDir, notExistDstDir}, f, &out, &in) = %q, not nil`, err)
   259  		}
   260  
   261  		if err := IsEqualTree(Default, srcDir, notExistDstDir); err != nil {
   262  			t.Fatalf(`IsEqualTree(Default, srcDir, notExistDstDir) = %q, not nil`, err)
   263  		}
   264  	})
   265  }
   266  
   267  // Other test to verify the CopyRecursive
   268  // whose dir$n and dst-dir already exists
   269  // cmd-line equivalent: $ cp -R dir1/ dir2/ dir3/ dst-dir/
   270  //
   271  // dst-dir will content dir{1, 3}
   272  // $ dst-dir/
   273  // ..	dir1/
   274  // ..	dir2/
   275  // ..   dir3/
   276  func TestCpRecursiveMultiple(t *testing.T) {
   277  	var f flags
   278  	f.recursive = true
   279  	tempDir := t.TempDir()
   280  
   281  	dstTest := filepath.Join(tempDir, "destination")
   282  	if err := os.Mkdir(dstTest, 0o755); err != nil {
   283  		t.Fatalf(`os.Mkdir(dstTest, 0o755) = %q, not nil`, err)
   284  	}
   285  
   286  	// create multiple random directories sources
   287  	srcDirs := []string{}
   288  	for i := 0; i < maxDirDepth; i++ {
   289  		srcTest := t.TempDir()
   290  
   291  		if err := createFilesTree(srcTest, maxDirDepth, 0); err != nil {
   292  			t.Fatalf(`createFilesTree(srcTest, maxDirDepth, 0) = %q, not nil`, err)
   293  		}
   294  
   295  		srcDirs = append(srcDirs, srcTest)
   296  
   297  	}
   298  	var out bytes.Buffer
   299  	var in bufio.Reader
   300  	args := srcDirs
   301  	args = append(args, dstTest)
   302  	if err := run(args, f, &out, &in); err != nil {
   303  		t.Fatalf(`run(args, f, &out, &in) = %q, not nil`, err)
   304  	}
   305  	// Make sure we can do it twice.
   306  	f.force = true
   307  	if err := run(args, f, &out, &in); err != nil {
   308  		t.Fatalf(`run(args, f, &out, &in) = %q, not nil`, err)
   309  	}
   310  	for _, src := range srcDirs {
   311  		_, srcFile := filepath.Split(src)
   312  
   313  		dst := filepath.Join(dstTest, srcFile)
   314  		if err := IsEqualTree(Default, src, dst); err != nil {
   315  			t.Fatalf(`IsEqualTree(Default, src, dst) = %q, not nil`, err)
   316  		}
   317  	}
   318  }
   319  
   320  // using -P don't follow symlinks, create other symlink
   321  // cmd-line equivalent: $ cp -P symlink symlink-copy
   322  func TestCpSymlink(t *testing.T) {
   323  	tempDir := t.TempDir()
   324  
   325  	f, err := randomFile(tempDir, "src-")
   326  	if err != nil {
   327  		t.Fatalf(`randomFile(tempDir, "src-") = %q, not nil`, err)
   328  	}
   329  	defer f.Close()
   330  
   331  	srcFpath := f.Name()
   332  	srcFname := filepath.Base(srcFpath)
   333  
   334  	newName := filepath.Join(tempDir, srcFname+"_link")
   335  	if err := os.Symlink(srcFname, newName); err != nil {
   336  		t.Fatalf(`os.Symlink(srcFname, newName) = %q, not nil`, err)
   337  	}
   338  
   339  	t.Run("no-follow-symlink", func(t *testing.T) {
   340  		var out bytes.Buffer
   341  		var in bufio.Reader
   342  		var f flags
   343  		f.noFollowSymlinks = true
   344  
   345  		dst := filepath.Join(tempDir, "dst-no-follow")
   346  		if err := run([]string{newName, dst}, f, &out, &in); err != nil {
   347  			t.Fatalf(`run([]string{newName, dst}, f, &out, &in) = %q, not nil`, err)
   348  		}
   349  		if err := IsEqualTree(NoFollowSymlinks, newName, dst); err != nil {
   350  			t.Fatalf(`IsEqualTree(NoFollowSymlinks, newName, dst) =%q, not nil`, err)
   351  		}
   352  	})
   353  
   354  	t.Run("follow-symlink", func(t *testing.T) {
   355  		var out bytes.Buffer
   356  		var in bufio.Reader
   357  		var f flags
   358  		f.noFollowSymlinks = false
   359  
   360  		dst := filepath.Join(tempDir, "dst-follow")
   361  		if err := run([]string{newName, dst}, f, &out, &in); err != nil {
   362  			t.Fatalf(`run([]string{newName, dst}, f, &out, &in) =%q, not nil`, err)
   363  		}
   364  		if err := IsEqualTree(Default, newName, dst); err != nil {
   365  			t.Fatalf(`IsEqualTree(Default, newName, dst) = %q, not nil`, err)
   366  		}
   367  	})
   368  }
   369  
   370  // isEqualFile compare two files by checksum
   371  func isEqualFile(fpath1, fpath2 string) error {
   372  	file1, err := os.Open(fpath1)
   373  	if err != nil {
   374  		return err
   375  	}
   376  	defer file1.Close()
   377  	file2, err := os.Open(fpath2)
   378  	if err != nil {
   379  		return err
   380  	}
   381  	defer file2.Close()
   382  
   383  	if !uio.ReaderAtEqual(file1, file2) {
   384  		return fmt.Errorf("%q and %q do not have equal content", fpath1, fpath2)
   385  	}
   386  	return nil
   387  }
   388  
   389  func readDirNames(path string) ([]string, error) {
   390  	entries, err := os.ReadDir(path)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	var basenames []string
   395  	for _, entry := range entries {
   396  		basenames = append(basenames, entry.Name())
   397  	}
   398  	return basenames, nil
   399  }
   400  
   401  func stat(o Options, path string) (os.FileInfo, error) {
   402  	if o.NoFollowSymlinks {
   403  		return os.Lstat(path)
   404  	}
   405  	return os.Stat(path)
   406  }
   407  
   408  // IsEqualTree compare the content in the file trees in src and dst paths
   409  func IsEqualTree(o Options, src, dst string) error {
   410  	srcInfo, err := stat(o, src)
   411  	if err != nil {
   412  		return err
   413  	}
   414  	dstInfo, err := stat(o, dst)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	if sm, dm := srcInfo.Mode()&os.ModeType, dstInfo.Mode()&os.ModeType; sm != dm {
   419  		return fmt.Errorf("mismatched mode: %q has mode %s while %q has mode %s", src, sm, dst, dm)
   420  	}
   421  
   422  	switch {
   423  	case srcInfo.Mode().IsDir():
   424  		srcEntries, err := readDirNames(src)
   425  		if err != nil {
   426  			return err
   427  		}
   428  		dstEntries, err := readDirNames(dst)
   429  		if err != nil {
   430  			return err
   431  		}
   432  		// os.ReadDir guarantees these are sorted.
   433  		if !reflect.DeepEqual(srcEntries, dstEntries) {
   434  			return fmt.Errorf("directory contents did not match:\n%q had %v\n%q had %v", src, srcEntries, dst, dstEntries)
   435  		}
   436  		for _, basename := range srcEntries {
   437  			if err := IsEqualTree(o, filepath.Join(src, basename), filepath.Join(dst, basename)); err != nil {
   438  				return err
   439  			}
   440  		}
   441  		return nil
   442  
   443  	case srcInfo.Mode().IsRegular():
   444  		return isEqualFile(src, dst)
   445  
   446  	case srcInfo.Mode()&os.ModeSymlink == os.ModeSymlink:
   447  		srcTarget, err := os.Readlink(src)
   448  		if err != nil {
   449  			return err
   450  		}
   451  		dstTarget, err := os.Readlink(dst)
   452  		if err != nil {
   453  			return err
   454  		}
   455  		if srcTarget != dstTarget {
   456  			return fmt.Errorf("target mismatch: symlink %q had target %q, while %q had target %q", src, srcTarget, dst, dstTarget)
   457  		}
   458  		return nil
   459  
   460  	default:
   461  		return fmt.Errorf("unsupported mode: %s", srcInfo.Mode())
   462  	}
   463  }