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 }