testsuite: run the generic timestamp tests on win32, see #8730

is_utime_fully_supported() and is_birthtime_fully_supported() probed via
os.utime, which can neither set the timestamps of a symlink itself nor the
birthtime on win32 - so they returned False there and all the tests they
guard were skipped. Probe via platform.set_times() instead, which does the
right thing on all platforms.

Also: a FILETIME has a resolution of 100ns, use that for st_mtime_ns_round
on win32 (rather than the 1s fallback).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-08-22 13:32:16 +02:00
co-authored by Claude Opus 5
parent c99d03a209
commit 8d1a36bfd1
3 changed files with 41 additions and 22 deletions
+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):
@@ -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")