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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-08-09 01:12:50 +00:00
1 parent 4a4797e959
commit 699c569cc2
2 files changed
+57 -1

No files matched your search

+1 -1
View File
@@ -143,7 +143,7 @@ Acceptance criteria:
- [x] Add an explicit one-filesystem policy. - [x] Add an explicit one-filesystem policy.
- [x] Make Python and external implementations provide equivalent guarantees or - [x] Make Python and external implementations provide equivalent guarantees or
document every difference. document every difference.
- [ ] Test supported behavior on Linux and macOS. - [x] Test supported behavior on Linux and macOS.
### Make dry-run genuinely non-mutating ### Make dry-run genuinely non-mutating
+56
View File
@@ -317,6 +317,27 @@ class TestCopyDirEntry:
dst_stat = os.lstat(dst_path) dst_stat = os.lstat(dst_path)
assert dst_stat.st_mode == src_stat.st_mode 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): def test_copy_directory_preserves_times(self, tmp_path):
"""Test that directory timestamps are preserved""" """Test that directory timestamps are preserved"""
src_path = os.path.join(str(tmp_path), "srcdir") src_path = os.path.join(str(tmp_path), "srcdir")
@@ -410,6 +431,41 @@ class TestCopyDirEntry:
# just verify the symlink was created # just verify the symlink was created
assert os.path.islink(dst_link) 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): def test_copy_file_preserves_ownership_if_root(self, tmp_path):
"""Test that file ownership is preserved (requires root)""" """Test that file ownership is preserved (requires root)"""
if os.geteuid() != 0: if os.geteuid() != 0: