Merge pull request #10177 from ThomasWaldmann/win32-timestamp-tests

win32: no ctime, archive creation time as birthtime; run timestamp tests on win32, fixes #8730
This commit is contained in:
TW
2026-08-22 14:43:15 +02:00
committed by GitHub
6 changed files with 54 additions and 29 deletions
+2 -1
View File
@@ -1227,7 +1227,8 @@ class MetadataCollector:
attrs["mtime"] = safe_ns(st.st_mtime_ns)
if not self.noatime:
attrs["atime"] = safe_ns(st.st_atime_ns)
if not self.noctime:
if not self.noctime and not is_win32:
# win32: st_ctime is the file creation time, that is archived as birthtime, see #8730.
attrs["ctime"] = safe_ns(st.st_ctime_ns)
if not self.nobirthtime:
birthtime_ns = get_birthtime_ns(st, path, fd=fd)
+3
View File
@@ -96,6 +96,9 @@ def get_birthtime_ns(st, path, fd=None):
if hasattr(st, "st_birthtime_ns"):
# Added in Python 3.12, but not always available.
return st.st_birthtime_ns
elif is_win32:
# Python < 3.12 on win32: st_ctime is the file creation time, see #8730.
return st.st_ctime_ns
elif is_darwin and is_darwin_feature_64_bit_inode:
return _get_birthtime_ns(fd or path, follow_symlinks=False)
elif hasattr(st, "st_birthtime"):
+22 -10
View File
@@ -22,6 +22,9 @@ except: # noqa
from ..fuse_impl import llfuse, has_any_fuse, has_llfuse, has_pyfuse3, has_mfusepy, ENOATTR # NOQA
from .. import platform
# import these directly: the borg.testsuite.platform subpackage shadows the platform name above.
from ..platform import get_birthtime_ns, set_times
from ..platformflags import is_win32, is_darwin
# Does this version of llfuse support ns precision?
@@ -47,6 +50,9 @@ else:
if sys.platform.startswith("netbsd"):
st_mtime_ns_round = -4 # 10us - strange: only >1 microsecond resolution here?
if is_win32:
st_mtime_ns_round = -2 # 100ns - the resolution of a FILETIME, see platform.windows.set_times
def same_ts_ns(ts_ns1, ts_ns2):
"""Compare two timestamps (both in nanoseconds) to determine whether they are (roughly) equal."""
@@ -207,16 +213,19 @@ def are_fifos_supported():
@functools.lru_cache
def is_utime_fully_supported():
# note: platform.set_times is used rather than os.utime, because os.utime can not set the
# timestamps of a symlink itself on win32 (it raises NotImplementedError there), see #8730.
with unopened_tempfile() as filepath:
# Some filesystems (such as SSHFS) don't support utime on symlinks
# Some filesystems (such as SSHFS) don't support setting the timestamps of symlinks
if are_symlinks_supported():
os.symlink("something", filepath)
else:
open(filepath, "w").close()
try:
os.utime(filepath, (1000, 2000), follow_symlinks=False)
atime_ns, mtime_ns = 1000 * 10**9, 2000 * 10**9
set_times(filepath, atime_ns=atime_ns, mtime_ns=mtime_ns, follow_symlinks=False)
new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_atime == 1000 and new_stats.st_mtime == 2000:
if new_stats.st_atime_ns == atime_ns and new_stats.st_mtime_ns == mtime_ns:
return True
except OSError:
pass
@@ -227,20 +236,23 @@ def is_utime_fully_supported():
@functools.lru_cache
def is_birthtime_fully_supported():
if not hasattr(os.stat_result, "st_birthtime"):
return False
# note: platform.set_times is used rather than the "os.utime twice" trick, because that only
# works on the BSDs and on macOS - on win32, the birthtime needs SetFileTime, see #8730.
with unopened_tempfile() as filepath:
# Some filesystems (such as SSHFS) don't support utime on symlinks
# Some filesystems (such as SSHFS) don't support setting the timestamps of symlinks
if are_symlinks_supported():
os.symlink("something", filepath)
else:
open(filepath, "w").close()
try:
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime(filepath, (atime, birthtime), follow_symlinks=False)
os.utime(filepath, (atime, mtime), follow_symlinks=False)
birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9
set_times(filepath, atime_ns=atime_ns, mtime_ns=mtime_ns, birthtime_ns=birthtime_ns, follow_symlinks=False)
new_stats = os.stat(filepath, follow_symlinks=False)
if new_stats.st_birthtime == birthtime and new_stats.st_mtime == mtime and new_stats.st_atime == atime:
if (
get_birthtime_ns(new_stats, filepath) == birthtime_ns
and new_stats.st_mtime_ns == mtime_ns
and new_stats.st_atime_ns == atime_ns
):
return True
except OSError:
pass
+11 -6
View File
@@ -252,19 +252,24 @@ def test_unix_socket(archivers, request, monkeypatch):
def test_nobirthtime(archivers, request):
archiver = request.getfixturevalue(archivers)
create_test_files(archiver.input_path)
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime("input/file1", (atime, birthtime))
os.utime("input/file1", (atime, mtime))
birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9
platform.set_times("input/file1", atime_ns=atime_ns, mtime_ns=mtime_ns, birthtime_ns=birthtime_ns)
cmd(archiver, "repo-create", RK_ENCRYPTION)
cmd(archiver, "create", "test", "input", "--nobirthtime")
with changedir("output"):
cmd(archiver, "extract", "test")
sti = os.stat("input/file1")
sto = os.stat("output/input/file1")
assert same_ts_ns(sti.st_birthtime * 1e9, birthtime * 1e9)
assert same_ts_ns(sto.st_birthtime * 1e9, mtime * 1e9)
assert same_ts_ns(platform.get_birthtime_ns(sti, "input/file1"), birthtime_ns)
birthtime_out = platform.get_birthtime_ns(sto, "output/input/file1")
if is_win32:
# win32 keeps the creation time the extracted file got when it was created.
assert not same_ts_ns(birthtime_out, birthtime_ns)
else:
# posix: setting an mtime older than the birthtime pulls the birthtime back to it.
assert same_ts_ns(birthtime_out, mtime_ns)
assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)
assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9)
assert same_ts_ns(sto.st_mtime_ns, mtime_ns)
def test_create_stdin(archivers, request):
+8 -6
View File
@@ -175,7 +175,7 @@ def test_basic_functionality(archivers, request):
unexpected = {"type": "modified", "added": 0, "removed": 0}
assert unexpected not in get_changes("input/file_touched", joutput)
if not content_only:
# on win32, ctime is the file creation time and does not change.
# on win32, no ctime is archived, see #8730.
expected = {"mtime"} if is_win32 else {"mtime", "ctime"}
assert expected.issubset({c["type"] for c in get_changes("input/file_touched", joutput)})
else:
@@ -274,18 +274,20 @@ def test_time_diffs(archivers, request):
cmd(archiver, "create", "archive2", "input")
output = cmd(archiver, "diff", "archive1", "archive2", "--format", "'{mtime}{ctime} {path}{NL}'")
assert "mtime" in output
assert "ctime" in output # Should show up on Windows as well since it is a new file.
if is_win32:
assert "ctime" not in output # win32: no ctime is archived, see #8730
else:
assert "ctime" in output
granularity_sleep()
os.chmod("input/test_file", 0o777)
cmd(archiver, "create", "archive3", "input")
output = cmd(archiver, "diff", "archive2", "archive3", "--format", "'{mtime}{ctime} {path}{NL}'")
assert "mtime" not in output
# Checking platform because ctime should not be shown on Windows since it wasn't recreated.
if not is_win32:
assert "ctime" in output
if is_win32:
assert "ctime" not in output # win32: no ctime is archived, see #8730
else:
assert "ctime" not in output
assert "ctime" in output
def test_sort_by_option(archivers, request):
@@ -310,6 +310,7 @@ def test_atime(archivers, request):
assert same_ts_ns(sto.st_atime_ns, atime * 10**9)
@pytest.mark.skipif(is_win32, reason="no os.pread and no O_NOATIME on win32")
@pytest.mark.skipif(not is_utime_fully_supported(), reason="cannot properly setup and execute test without utime")
def test_atime_open_updates_atime(archiver):
# simulate a platform where already the open() updates the atime (no O_NOATIME support,
@@ -356,19 +357,20 @@ def test_atime_open_updates_atime(archiver):
def test_birthtime(archivers, request):
archiver = request.getfixturevalue(archivers)
create_test_files(archiver.input_path)
birthtime, mtime, atime = 946598400, 946684800, 946771200
os.utime("input/file1", (atime, birthtime))
os.utime("input/file1", (atime, mtime))
birthtime_ns, mtime_ns, atime_ns = 946598400 * 10**9, 946684800 * 10**9, 946771200 * 10**9
platform.set_times("input/file1", atime_ns=atime_ns, mtime_ns=mtime_ns, birthtime_ns=birthtime_ns)
cmd(archiver, "repo-create", RK_ENCRYPTION)
cmd(archiver, "create", "test", "input")
with changedir("output"):
cmd(archiver, "extract", "test")
sti = os.stat("input/file1")
sto = os.stat("output/input/file1")
assert same_ts_ns(sti.st_birthtime * 1e9, sto.st_birthtime * 1e9)
assert same_ts_ns(sto.st_birthtime * 1e9, birthtime * 1e9)
birthtime_in = get_birthtime_ns(sti, "input/file1")
birthtime_out = get_birthtime_ns(sto, "output/input/file1")
assert same_ts_ns(birthtime_in, birthtime_out)
assert same_ts_ns(birthtime_out, birthtime_ns)
assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns)
assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9)
assert same_ts_ns(sto.st_mtime_ns, mtime_ns)
@pytest.mark.skipif(not is_win32, reason="Windows-only test")