From 95614b6eecd9afa30bc92c816235e9c73a656177 Mon Sep 17 00:00:00 2001 From: Maks Snegov Date: Wed, 4 Feb 2026 21:56:25 -0800 Subject: [PATCH] Add metadata preservation tests for copy_direntry Verify that timestamps, permissions, and ownership are correctly preserved when copying files, directories, and symlinks. Tests account for filesystem behavior where reading a file updates its atime by capturing source timestamps before the copy operation. --- tests/test_fs.py | 123 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_rsync.py | 3 -- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/tests/test_fs.py b/tests/test_fs.py index 7db4e25..09347e6 100644 --- a/tests/test_fs.py +++ b/tests/test_fs.py @@ -216,6 +216,129 @@ class TestCopyDirEntry: assert os.path.islink(dst_link) assert os.readlink(dst_link) == target_path + def test_copy_file_preserves_times(self, tmp_path): + """Test that file timestamps are preserved""" + src_path = os.path.join(str(tmp_path), "source.txt") + dst_path = os.path.join(str(tmp_path), "dest.txt") + + with open(src_path, "w") as f: + f.write("test content") + + # set specific timestamps: atime=1000000000, mtime=2000000000 + os.utime(src_path, (1000000000, 2000000000)) + + # capture original times before copy (copy will update src atime) + orig_stat = os.lstat(src_path) + + entry = fs.PseudoDirEntry(src_path) + fs.copy_direntry(entry, dst_path) + + dst_stat = os.lstat(dst_path) + assert dst_stat.st_atime == orig_stat.st_atime + assert dst_stat.st_mtime == orig_stat.st_mtime + + def test_copy_file_preserves_permissions(self, tmp_path): + """Test that file permissions are preserved""" + src_path = os.path.join(str(tmp_path), "source.txt") + dst_path = os.path.join(str(tmp_path), "dest.txt") + + with open(src_path, "w") as f: + f.write("test content") + os.chmod(src_path, 0o600) + + entry = fs.PseudoDirEntry(src_path) + fs.copy_direntry(entry, dst_path) + + src_stat = os.lstat(src_path) + dst_stat = os.lstat(dst_path) + assert dst_stat.st_mode == src_stat.st_mode + + def test_copy_directory_preserves_times(self, tmp_path): + """Test that directory timestamps are preserved""" + src_path = os.path.join(str(tmp_path), "srcdir") + dst_path = os.path.join(str(tmp_path), "dstdir") + + os.mkdir(src_path) + # set specific timestamps + os.utime(src_path, (1000000000, 2000000000)) + + entry = fs.PseudoDirEntry(src_path) + fs.copy_direntry(entry, dst_path) + + src_stat = os.lstat(src_path) + dst_stat = os.lstat(dst_path) + assert dst_stat.st_atime == src_stat.st_atime + assert dst_stat.st_mtime == src_stat.st_mtime + + def test_copy_directory_preserves_permissions(self, tmp_path): + """Test that directory permissions are preserved""" + src_path = os.path.join(str(tmp_path), "srcdir") + dst_path = os.path.join(str(tmp_path), "dstdir") + + os.mkdir(src_path) + os.chmod(src_path, 0o700) + + entry = fs.PseudoDirEntry(src_path) + fs.copy_direntry(entry, dst_path) + + src_stat = os.lstat(src_path) + dst_stat = os.lstat(dst_path) + assert dst_stat.st_mode == src_stat.st_mode + + def test_copy_symlink_preserves_times_if_supported(self, tmp_path): + """Test that symlink timestamps are preserved (OS-dependent)""" + target_path = os.path.join(str(tmp_path), "target.txt") + src_link = os.path.join(str(tmp_path), "source_link") + dst_link = os.path.join(str(tmp_path), "dest_link") + + with open(target_path, "w") as f: + f.write("target") + os.symlink(target_path, src_link) + + # set symlink timestamps if supported + if os.utime in os.supports_follow_symlinks: + os.utime(src_link, (1000000000, 2000000000), + follow_symlinks=False) + # capture original times before copy + orig_stat = os.lstat(src_link) + + # find the real os.DirEntry for the symlink + entry = None + with os.scandir(str(tmp_path)) as it: + for e in it: + if e.name == "source_link": + entry = e + break + + fs.copy_direntry(entry, dst_link) + + if os.utime in os.supports_follow_symlinks: + dst_stat = os.lstat(dst_link) + assert dst_stat.st_atime == orig_stat.st_atime + assert dst_stat.st_mtime == orig_stat.st_mtime + else: + # just verify the symlink was created + assert os.path.islink(dst_link) + + def test_copy_file_preserves_ownership_if_root(self, tmp_path): + """Test that file ownership is preserved (requires root)""" + if os.geteuid() != 0: + pytest.skip("Ownership tests require root privileges") + + src_path = os.path.join(str(tmp_path), "source.txt") + dst_path = os.path.join(str(tmp_path), "dest.txt") + + with open(src_path, "w") as f: + f.write("test content") + + entry = fs.PseudoDirEntry(src_path) + fs.copy_direntry(entry, dst_path) + + src_stat = os.lstat(src_path) + dst_stat = os.lstat(dst_path) + assert dst_stat.st_uid == src_stat.st_uid + assert dst_stat.st_gid == src_stat.st_gid + class TestHardlinkDirBasic: """Test suite for basic hardlink_dir functionality.""" diff --git a/tests/test_rsync.py b/tests/test_rsync.py index fd6dda4..f965771 100644 --- a/tests/test_rsync.py +++ b/tests/test_rsync.py @@ -175,9 +175,6 @@ class TestRsync: assert os.path.lexists(dst_fpath) check_identical_file(src_fpath, dst_fpath) - # TODO add tests for changing ownership - # TODO add tests for changing times (?) - class TestRsyncBasic: """Test suite for basic rsync functionality."""