From 699c569cc20f8c6fe3f3100f002f7c3e583a16af Mon Sep 17 00:00:00 2001 From: Maksim Shulga Date: Sun, 9 Aug 2026 01:12:50 +0000 Subject: [PATCH] Test nanosecond mtime precision on file and symlink copies (#22) * Test nanosecond mtime precision on file and symlink copies Existing preserves-times tests only set whole-second timestamps via os.utime(path, (sec, sec)), so a backend that silently truncated to second resolution would still pass them. The Python backend's nanosecond-precision guarantee (documented in README's Limitations section) was otherwise untested at sub-second granularity. Combined with CI already running the full suite on both ubuntu-latest and macos-latest, this closes the "Test supported behavior on Linux and macOS" item in IMPROVEMENTS.md. Co-Authored-By: Claude Sonnet 5 * Rename nanosecond timestamp tests to cover atime too Copilot review feedback on PR #22: both tests assert st_atime_ns and st_mtime_ns, but the names/docstrings said "mtime" only. Co-Authored-By: Claude Sonnet 5 * Assert symlink entry is found before use in nanosecond test Copilot review feedback on PR #22: if the scandir() loop didn't find the symlink, entry stayed None and copy_direntry() would fail with a less-informative error than an explicit assertion. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Maxim Shulga Co-authored-by: Claude Sonnet 5 --- IMPROVEMENTS.md | 2 +- tests/test_fs.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index e58ffb6..96ddedd 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -143,7 +143,7 @@ Acceptance criteria: - [x] Add an explicit one-filesystem policy. - [x] Make Python and external implementations provide equivalent guarantees or document every difference. -- [ ] Test supported behavior on Linux and macOS. +- [x] Test supported behavior on Linux and macOS. ### Make dry-run genuinely non-mutating diff --git a/tests/test_fs.py b/tests/test_fs.py index 5b65816..395b7d5 100644 --- a/tests/test_fs.py +++ b/tests/test_fs.py @@ -317,6 +317,27 @@ class TestCopyDirEntry: dst_stat = os.lstat(dst_path) assert dst_stat.st_mode == src_stat.st_mode + def test_copy_file_preserves_nanosecond_timestamps(self, tmp_path): + """README documents nanosecond mtime precision for the Python + backend; whole-second os.utime() calls can't catch a backend + that silently truncates to second resolution, so this sets + sub-second atime/mtime explicitly.""" + 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.utime(src_path, ns=(1_000_000_000_123456789, + 2_000_000_000_987654321)) + + src_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_mtime_ns == src_stat.st_mtime_ns + assert dst_stat.st_atime_ns == src_stat.st_atime_ns + def test_copy_directory_preserves_times(self, tmp_path): """Test that directory timestamps are preserved""" src_path = os.path.join(str(tmp_path), "srcdir") @@ -410,6 +431,41 @@ class TestCopyDirEntry: # just verify the symlink was created assert os.path.islink(dst_link) + def test_copy_symlink_preserves_nanosecond_timestamps_if_supported( + self, tmp_path): + """Same nanosecond-precision claim as + test_copy_file_preserves_nanosecond_timestamps(), but for the + follow_symlinks=False code path in copy_direntry().""" + 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) + + if os.utime not in os.supports_follow_symlinks: + pytest.skip("OS doesn't support setting symlink timestamps") + + os.utime(src_link, ns=(1_000_000_000_123456789, + 2_000_000_000_987654321), + follow_symlinks=False) + orig_stat = os.lstat(src_link) + + entry = None + with os.scandir(str(tmp_path)) as it: + for e in it: + if e.name == "source_link": + entry = e + break + + assert entry is not None, "source_link not found via scandir" + fs.copy_direntry(entry, dst_link) + + dst_stat = os.lstat(dst_link) + assert dst_stat.st_mtime_ns == orig_stat.st_mtime_ns + assert dst_stat.st_atime_ns == orig_stat.st_atime_ns + def test_copy_file_preserves_ownership_if_root(self, tmp_path): """Test that file ownership is preserved (requires root)""" if os.geteuid() != 0: