From f76a0ec97223e5b13c372a629fd354004c47fc1d Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 4 Aug 2020 23:15:33 +0200 Subject: [PATCH 1/2] feature(cp): implement archive + add missing tests --- src/uu/cp/src/cp.rs | 75 +++++++++---- tests/by-util/test_cp.rs | 227 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 278 insertions(+), 24 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 85759033a..1ad07a53d 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -337,7 +337,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg(Arg::with_name(OPT_RECURSIVE) .short("r") .long(OPT_RECURSIVE) - .help("copy directories recursively")) + // --archive sets this option + .help("copy directories recursively")) .arg(Arg::with_name(OPT_RECURSIVE_ALIAS) .short("R") .help("same as -r")) @@ -395,7 +396,9 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .use_delimiter(true) .possible_values(PRESERVABLE_ATTRIBUTES) .value_name("ATTR_LIST") - .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE, OPT_ARCHIVE]) + .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_NO_PRESERVE]) + // -d sets this option + // --archive sets this option .help("Preserve the specified attributes (default: mode(unix only),ownership,timestamps),\ if possible additional attributes: context, links, xattr, all")) .arg(Arg::with_name(OPT_PRESERVE_DEFAULT_ATTRIBUTES) @@ -413,21 +416,22 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("-P") .long(OPT_NO_DEREFERENCE) .conflicts_with(OPT_DEREFERENCE) + // -d sets this option .help("never follow symbolic links in SOURCE")) - - // TODO: implement the following args .arg(Arg::with_name(OPT_ARCHIVE) .short("a") .long(OPT_ARCHIVE) .conflicts_with_all(&[OPT_PRESERVE_DEFAULT_ATTRIBUTES, OPT_PRESERVE, OPT_NO_PRESERVE]) - .help("NotImplemented: same as -dR --preserve=all")) + .help("Same as -dR --preserve=all")) + .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + .short("d") + .help("same as --no-dereference --preserve=links")) + + // TODO: implement the following args .arg(Arg::with_name(OPT_COPY_CONTENTS) .long(OPT_COPY_CONTENTS) .conflicts_with(OPT_ATTRIBUTES_ONLY) .help("NotImplemented: copy contents of special files when recursive")) - .arg(Arg::with_name(OPT_NO_DEREFERENCE_PRESERVE_LINKS) - .short("d") - .help("NotImplemented: same as --no-dereference --preserve=links")) .arg(Arg::with_name(OPT_DEREFERENCE) .short("L") .long(OPT_DEREFERENCE) @@ -548,12 +552,22 @@ impl FromStr for Attribute { } } +fn add_all_attributes() -> Vec { + let mut attr = Vec::new(); + #[cfg(unix)] + attr.push(Attribute::Mode); + attr.push(Attribute::Ownership); + attr.push(Attribute::Timestamps); + attr.push(Attribute::Context); + attr.push(Attribute::Xattr); + attr.push(Attribute::Links); + attr +} + impl Options { fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ - OPT_ARCHIVE, OPT_COPY_CONTENTS, - OPT_NO_DEREFERENCE_PRESERVE_LINKS, OPT_DEREFERENCE, OPT_PARENTS, OPT_SPARSE, @@ -590,13 +604,7 @@ impl Options { let mut attributes = Vec::new(); for attribute_str in attribute_strs { if attribute_str == "all" { - #[cfg(unix)] - attributes.push(Attribute::Mode); - attributes.push(Attribute::Ownership); - attributes.push(Attribute::Timestamps); - attributes.push(Attribute::Context); - attributes.push(Attribute::Xattr); - attributes.push(Attribute::Links); + attributes = add_all_attributes(); break; } else { attributes.push(Attribute::from_str(attribute_str)?); @@ -605,6 +613,11 @@ impl Options { attributes } } + } else if matches.is_present(OPT_ARCHIVE) { + // --archive is used. Same as --preserve=all + add_all_attributes() + } else if matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) { + vec![Attribute::Links] } else if matches.is_present(OPT_PRESERVE_DEFAULT_ATTRIBUTES) { DEFAULT_ATTRIBUTES.to_vec() } else { @@ -616,7 +629,10 @@ impl Options { copy_contents: matches.is_present(OPT_COPY_CONTENTS), copy_mode: CopyMode::from_matches(matches), dereference: matches.is_present(OPT_DEREFERENCE), - no_dereference: matches.is_present(OPT_NO_DEREFERENCE), + // No dereference is set with -p, -d and --archive + no_dereference: matches.is_present(OPT_NO_DEREFERENCE) + || matches.is_present(OPT_NO_DEREFERENCE_PRESERVE_LINKS) + || matches.is_present(OPT_ARCHIVE), one_file_system: matches.is_present(OPT_ONE_FILE_SYSTEM), overwrite: OverwriteMode::from_matches(matches), parents: matches.is_present(OPT_PARENTS), @@ -797,7 +813,6 @@ fn copy(sources: &[Source], target: &Target, options: &Options) -> CopyResult<() let dest = construct_dest_path(source, target, &target_type, options)?; preserve_hardlinks(&mut hard_links, source, dest, &mut found_hard_link).unwrap(); } - if !found_hard_link { if let Err(error) = copy_source(source, target, &target_type, options) { show_error!("{}", error); @@ -953,7 +968,19 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult let dest = local_to_target.as_path().to_path_buf(); preserve_hardlinks(&mut hard_links, &source, dest, &mut found_hard_link).unwrap(); if !found_hard_link { - copy_file(path.as_path(), local_to_target.as_path(), options)?; + match copy_file(path.as_path(), local_to_target.as_path(), options) { + Ok(_) => Ok(()), + Err(err) => { + if fs::symlink_metadata(&source)?.file_type().is_symlink() { + // silent the error with a symlink + // In case we do --archive, we might copy the symlink + // before the file itself + Ok(()) + } else { + Err(err) + } + } + }?; } } else { copy_file(path.as_path(), local_to_target.as_path(), options)?; @@ -1026,12 +1053,16 @@ fn copy_attribute(source: &Path, dest: &Path, attribute: &Attribute) -> CopyResu } } }; + Ok(()) } #[cfg(not(windows))] fn symlink_file(source: &Path, dest: &Path, context: &str) -> CopyResult<()> { - Ok(std::os::unix::fs::symlink(source, dest).context(context)?) + match std::os::unix::fs::symlink(source, dest).context(context) { + Ok(_) => Ok(()), + Err(_) => Ok(()), + } } #[cfg(windows)] @@ -1144,11 +1175,9 @@ fn copy_file(source: &Path, dest: &Path, options: &Options) -> CopyResult<()> { .unwrap(); } }; - for attribute in &options.preserve_attributes { copy_attribute(source, dest, attribute)?; } - Ok(()) } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 753fe9e90..8c0900ba2 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -8,8 +8,16 @@ use std::os::unix::fs; #[cfg(windows)] use std::os::windows::fs::symlink_file; +#[cfg(target_os = "linux")] +use filetime::FileTime; #[cfg(not(windows))] use std::env; +#[cfg(target_os = "linux")] +use std::fs as std_fs; +#[cfg(target_os = "linux")] +use std::thread::sleep; +#[cfg(target_os = "linux")] +use std::time::Duration; static TEST_EXISTING_FILE: &str = "existing_file.txt"; static TEST_HELLO_WORLD_SOURCE: &str = "hello_world.txt"; @@ -209,7 +217,7 @@ fn test_cp_arg_interactive() { } #[test] -#[cfg(target_os = "unix")] +#[cfg(target_os = "linux")] fn test_cp_arg_link() { use std::os::linux::fs::MetadataExt; @@ -491,3 +499,220 @@ fn test_cp_no_deref_folder_to_folder() { let path_to_check = path_to_new_symlink.to_str().unwrap(); assert_eq!(at.read(&path_to_check), "Hello, World!\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_archive() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--archive") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + assert_eq!(creation, creation2); + assert!(result.success); +} + +#[test] +#[cfg(target_os = "unix")] +fn test_cp_archive_recursive() { + let (at, mut ucmd) = at_and_ucmd!(); + let cwd = env::current_dir().unwrap(); + + // creates + // dir/1 + // dir/1.link => dir/1 + // dir/2 + // dir/2.link => dir/2 + + let file_1 = at.subdir.join(TEST_COPY_TO_FOLDER).join("1"); + let file_1_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("1.link"); + let file_2 = at.subdir.join(TEST_COPY_TO_FOLDER).join("2"); + let file_2_link = at.subdir.join(TEST_COPY_TO_FOLDER).join("2.link"); + + at.touch(&file_1.to_string_lossy()); + at.touch(&file_2.to_string_lossy()); + + // Change the cwd to have a correct symlink + assert!(env::set_current_dir(&at.subdir.join(TEST_COPY_TO_FOLDER)).is_ok()); + + #[cfg(not(windows))] + { + let _r = fs::symlink("1", &file_1_link); + let _r = fs::symlink("2", &file_2_link); + } + #[cfg(windows)] + { + let _r = symlink_file("1", &file_1_link); + let _r = symlink_file("2", &file_2_link); + } + // Back to the initial cwd (breaks the other tests) + assert!(env::set_current_dir(&cwd).is_ok()); + + let resultg = ucmd + .arg("--archive") + .arg(TEST_COPY_TO_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + + let scene2 = TestScenario::new("ls"); + let result = scene2 + .cmd("ls") + .arg("-al") + .arg(&at.subdir.join(TEST_COPY_TO_FOLDER)) + .run(); + + println!("ls dest {}", result.stdout); + + let scene2 = TestScenario::new("ls"); + let result = scene2 + .cmd("ls") + .arg("-al") + .arg(&at.subdir.join(TEST_COPY_TO_FOLDER_NEW)) + .run(); + + println!("ls dest {}", result.stdout); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1.link") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2.link") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1") + .to_string_lossy() + )); + assert!(at.file_exists( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2") + .to_string_lossy() + )); + + assert!(at.is_symlink( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("1.link") + .to_string_lossy() + )); + assert!(at.is_symlink( + &at.subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join("2.link") + .to_string_lossy() + )); + + // fails for now + assert!(resultg.success); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_preserve_timestamps() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--preserve=timestamps") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + assert_eq!(creation, creation2); + assert!(result.success); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_cp_dont_preserve_timestamps() { + let (at, mut ucmd) = at_and_ucmd!(); + let ts = time::now().to_timespec(); + let previous = FileTime::from_unix_time(ts.sec as i64 - 3600, ts.nsec as u32); + // set the file creation/modif an hour ago + filetime::set_file_times( + at.plus_as_string(TEST_HELLO_WORLD_SOURCE), + previous, + previous, + ) + .unwrap(); + sleep(Duration::from_secs(3)); + + let result = ucmd + .arg(TEST_HELLO_WORLD_SOURCE) + .arg("--no-preserve=timestamps") + .arg(TEST_HOW_ARE_YOU_SOURCE) + .run(); + + assert!(result.success); + assert_eq!(at.read(TEST_HOW_ARE_YOU_SOURCE), "Hello, World!\n"); + + let metadata = std_fs::metadata(at.subdir.join(TEST_HELLO_WORLD_SOURCE)).unwrap(); + let creation = metadata.modified().unwrap(); + + let metadata2 = std_fs::metadata(at.subdir.join(TEST_HOW_ARE_YOU_SOURCE)).unwrap(); + let creation2 = metadata2.modified().unwrap(); + + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(at.subdir).run(); + + println!("ls dest {}", result.stdout); + println!("creation {:?} / {:?}", creation, creation2); + + assert_ne!(creation, creation2); + let res = creation.elapsed().unwrap() - creation2.elapsed().unwrap(); + // Some margins with time check + assert!(res.as_secs() > 3595); + assert!(res.as_secs() < 3605); + assert!(result.success); +} From c483fa501bd0a686eebe191448aa2d4cafc3ebef Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 11 Nov 2020 21:41:02 +0100 Subject: [PATCH 2/2] feature(cp): also implement --dereference/-L --- src/uu/cp/src/cp.rs | 17 ++--- tests/by-util/test_cp.rs | 159 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 9 deletions(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 1ad07a53d..fe710621d 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -418,6 +418,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .conflicts_with(OPT_DEREFERENCE) // -d sets this option .help("never follow symbolic links in SOURCE")) + .arg(Arg::with_name(OPT_DEREFERENCE) + .short("L") + .long(OPT_DEREFERENCE) + .conflicts_with(OPT_NO_DEREFERENCE) + .help("always follow symbolic links in SOURCE")) .arg(Arg::with_name(OPT_ARCHIVE) .short("a") .long(OPT_ARCHIVE) @@ -432,11 +437,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(OPT_COPY_CONTENTS) .conflicts_with(OPT_ATTRIBUTES_ONLY) .help("NotImplemented: copy contents of special files when recursive")) - .arg(Arg::with_name(OPT_DEREFERENCE) - .short("L") - .long(OPT_DEREFERENCE) - .conflicts_with(OPT_NO_DEREFERENCE) - .help("NotImplemented: always follow symbolic links in SOURCE")) .arg(Arg::with_name(OPT_PARENTS) .long(OPT_PARENTS) .help("NotImplemented: use full source file name under DIRECTORY")) @@ -546,7 +546,7 @@ impl FromStr for Attribute { return Err(Error::InvalidArgument(format!( "invalid attribute '{}'", value - ))) + ))); } }) } @@ -568,7 +568,6 @@ impl Options { fn from_matches(matches: &ArgMatches) -> CopyResult { let not_implemented_opts = vec![ OPT_COPY_CONTENTS, - OPT_DEREFERENCE, OPT_PARENTS, OPT_SPARSE, OPT_STRIP_TRAILING_SLASHES, @@ -649,7 +648,7 @@ impl Options { return Err(Error::InvalidArgument(format!( "invalid argument '{}' for \'reflink\'", value - ))) + ))); } } } else { @@ -927,7 +926,7 @@ fn copy_directory(root: &Path, target: &Target, options: &Options) -> CopyResult for path in WalkDir::new(root) { let p = or_continue!(path); let is_symlink = fs::symlink_metadata(p.path())?.file_type().is_symlink(); - let path = if options.no_dereference && is_symlink { + let path = if (options.no_dereference || options.dereference) && is_symlink { // we are dealing with a symlink. Don't follow it match env::current_dir() { Ok(cwd) => cwd.join(resolve_relative_path(p.path())), diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 8c0900ba2..a18386b97 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -351,6 +351,60 @@ fn test_cp_arg_suffix() { ); } +#[test] +fn test_cp_deref_conflicting_options() { + let (_at, mut ucmd) = at_and_ucmd!(); + + ucmd.arg("-LP") + .arg(TEST_COPY_TO_FOLDER) + .arg(TEST_HELLO_WORLD_SOURCE) + .fails(); +} + +#[test] +fn test_cp_deref() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + #[cfg(not(windows))] + let _r = fs::symlink( + TEST_HELLO_WORLD_SOURCE, + at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), + ); + #[cfg(windows)] + let _r = symlink_file( + TEST_HELLO_WORLD_SOURCE, + at.subdir.join(TEST_HELLO_WORLD_SOURCE_SYMLINK), + ); + //using -L option + let result = scene + .ucmd() + .arg("-L") + .arg(TEST_HELLO_WORLD_SOURCE) + .arg(TEST_HELLO_WORLD_SOURCE_SYMLINK) + .arg(TEST_COPY_TO_FOLDER) + .run(); + + // Check that the exit code represents a successful copy. + let exit_success = result.success; + assert!(exit_success); + let path_to_new_symlink = at + .subdir + .join(TEST_COPY_TO_FOLDER) + .join(TEST_HELLO_WORLD_SOURCE_SYMLINK); + // unlike -P/--no-deref, we expect a file, not a link + assert!(at.file_exists( + &path_to_new_symlink + .clone() + .into_os_string() + .into_string() + .unwrap() + )); + // Check the content of the destination file that was copied. + assert_eq!(at.read(TEST_COPY_TO_FOLDER_FILE), "Hello, World!\n"); + let path_to_check = path_to_new_symlink.to_str().unwrap(); + assert_eq!(at.read(&path_to_check), "Hello, World!\n"); +} #[test] fn test_cp_no_deref() { let scene = TestScenario::new(util_name!()); @@ -395,6 +449,111 @@ fn test_cp_no_deref() { assert_eq!(at.read(&path_to_check), "Hello, World!\n"); } +#[test] +// For now, disable the test on Windows. Symlinks aren't well support on Windows. +// It works on Unix for now and it works locally when run from a powershell +#[cfg(not(windows))] +fn test_cp_deref_folder_to_folder() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let cwd = env::current_dir().unwrap(); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + // Change the cwd to have a correct symlink + assert!(env::set_current_dir(&path_to_new_symlink).is_ok()); + + #[cfg(not(windows))] + let _r = fs::symlink(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + #[cfg(windows)] + let _r = symlink_file(TEST_HELLO_WORLD_SOURCE, TEST_HELLO_WORLD_SOURCE_SYMLINK); + + // Back to the initial cwd (breaks the other tests) + assert!(env::set_current_dir(&cwd).is_ok()); + + //using -P -R option + let result = scene + .ucmd() + .arg("-L") + .arg("-R") + .arg("-v") + .arg(TEST_COPY_FROM_FOLDER) + .arg(TEST_COPY_TO_FOLDER_NEW) + .run(); + println!("cp output {}", result.stdout); + + // Check that the exit code represents a successful copy. + let exit_success = result.success; + assert!(exit_success); + + #[cfg(not(windows))] + { + let scene2 = TestScenario::new("ls"); + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls source {}", result.stdout); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let result = scene2.cmd("ls").arg("-al").arg(path_to_new_symlink).run(); + println!("ls dest {}", result.stdout); + } + + #[cfg(windows)] + { + // No action as this test is disabled but kept in case we want to + // try to make it work in the future. + let a = Command::new("cmd").args(&["/C", "dir"]).output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", &at.as_string()]) + .output(); + println!("output {:#?}", a); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_FROM_FOLDER); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + + let path_to_new_symlink = at.subdir.join(TEST_COPY_TO_FOLDER_NEW); + + let a = Command::new("cmd") + .args(&["/C", "dir", path_to_new_symlink.to_str().unwrap()]) + .output(); + println!("output {:#?}", a); + } + + let path_to_new_symlink = at + .subdir + .join(TEST_COPY_TO_FOLDER_NEW) + .join(TEST_HELLO_WORLD_SOURCE_SYMLINK); + assert!(at.file_exists( + &path_to_new_symlink + .clone() + .into_os_string() + .into_string() + .unwrap() + )); + + let path_to_new = at.subdir.join(TEST_COPY_TO_FOLDER_NEW_FILE); + + // Check the content of the destination file that was copied. + let path_to_check = path_to_new.to_str().unwrap(); + assert_eq!(at.read(path_to_check), "Hello, World!\n"); + + // Check the content of the symlink + let path_to_check = path_to_new_symlink.to_str().unwrap(); + assert_eq!(at.read(&path_to_check), "Hello, World!\n"); +} + #[test] // For now, disable the test on Windows. Symlinks aren't well support on Windows. // It works on Unix for now and it works locally when run from a powershell