Forráskód Böngészése

Merge pull request #9190 from ThomasWaldmann/backport/pr-9151-to-1.4-maint

Backport: add granularity_sleep, fixes #9150 (from #9151) to 1.4-maint
TW 2 hete
szülő
commit
2a263ece85
2 módosított fájl, 52 hozzáadás és 16 törlés
  1. 41 0
      src/borg/testsuite/__init__.py
  2. 11 16
      src/borg/testsuite/archiver.py

+ 41 - 0
src/borg/testsuite/__init__.py

@@ -25,6 +25,7 @@ from .. import platform
 # Note: this is used by borg.selftest; do not use or import pytest functionality here.
 
 from ..fuse_impl import llfuse, has_pyfuse3, has_llfuse
+from ..platformflags import is_win32, is_darwin
 
 # Does this version of llfuse support ns precision?
 have_fuse_mtime_ns = hasattr(llfuse.EntryAttributes, 'st_mtime_ns') if llfuse else False
@@ -60,6 +61,46 @@ def same_ts_ns(ts_ns1, ts_ns2):
     return diff_ts <= diff_max
 
 
+def granularity_sleep(*, ctime_quirk=False):
+    """Sleep long enough to overcome filesystem timestamp granularity and related platform quirks.
+
+    Purpose
+    - Ensure that successive file operations land on different timestamp "ticks" across filesystems
+      and operating systems, so tests that compare mtime/ctime are reliable.
+
+    Default rationale (ctime_quirk=False)
+    - macOS: Some volumes may still be HFS+ (1 s timestamp granularity). To be safe across APFS and HFS+,
+      sleep 1.0 s on Darwin.
+    - Windows/NTFS: Although NTFS stores timestamps with 100 ns units, actual updates can be delayed by
+      scheduling/metadata behavior. Sleep a short but noticeable amount (0.2 s).
+    - Linux/BSD and others: Modern filesystems (ext4, XFS, Btrfs, ZFS, UFS2, etc.) typically have
+      sub-second granularity; a small delay (0.02 s) is sufficient in practice.
+
+    Windows ctime quirk (ctime_quirk=True)
+    - On Windows, ``stat().st_ctime`` is the file creation time, not "metadata change time" as on Unix.
+    - NTFS implements a feature called "file system tunneling" that preserves certain metadata — including
+      creation time — for short intervals when a file is deleted and a new file with the same name is
+      created in the same directory. The default tunneling window is about 15 seconds.
+    - Consequence: If a test deletes a file and quickly recreates it with the same name, the creation time
+      (st_ctime) may remain unchanged for up to ~15 s, causing flakiness when tests expect a changed ctime.
+    - When ``ctime_quirk=True`` this helper sleeps long enough on Windows (15.0 s) to exceed the tunneling
+      window so the new file receives a fresh creation time. On non-Windows platforms this flag has no
+      special effect beyond the normal, short sleep.
+
+    Parameters
+    - ctime_quirk: bool (default False)
+      If True, apply the Windows NTFS tunneling workaround (15 s sleep on Windows). Ignored elsewhere.
+    """
+    if is_darwin:
+        duration = 1.0
+    elif is_win32:
+        duration = 0.2 if not ctime_quirk else 15.0
+    else:
+        # Default for Linux/BSD and others with fine-grained timestamps
+        duration = 0.02
+    time.sleep(duration)
+
+
 @contextmanager
 def unopened_tempfile():
     with tempfile.TemporaryDirectory() as tempdir:

+ 11 - 16
src/borg/testsuite/archiver.py

@@ -57,7 +57,7 @@ from ..logger import setup_logging
 from ..remote import RemoteRepository, PathNotAllowed
 from ..repository import Repository
 from . import has_lchflags, llfuse
-from . import BaseTestCase, changedir, environment_variable, no_selinux, same_ts_ns
+from . import BaseTestCase, changedir, environment_variable, no_selinux, same_ts_ns, granularity_sleep
 from . import are_symlinks_supported, are_hardlinks_supported, are_fifos_supported, is_utime_fully_supported, is_birthtime_fully_supported
 from .platform import fakeroot_detected, is_darwin, is_freebsd, is_win32
 from .upgrader import make_attic_repo
@@ -383,7 +383,7 @@ class ArchiverTestCaseBase(BaseTestCase):
             if e.errno not in (errno.EINVAL, errno.ENOSYS):
                 raise
             have_root = False
-        time.sleep(1)  # "empty" must have newer timestamp than other files
+        granularity_sleep()  # ensure "empty" has a newer timestamp than other files across filesystems
         self.create_regular_file('empty', size=0)
         return have_root
 
@@ -2074,7 +2074,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
 
         clearly incomplete: only tests for the weird "unchanged" status for now"""
         self.create_regular_file('file1', size=1024 * 80)
-        time.sleep(1)  # file2 must have newer timestamps than file1
+        granularity_sleep()  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=1024 * 80)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         output = self.cmd('create', '--list', self.repository_location + '::test', 'input')
@@ -2090,7 +2090,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
     def test_file_status_cs_cache_mode(self):
         """test that a changed file with faked "previous" mtime still gets backed up in ctime,size cache_mode"""
         self.create_regular_file('file1', contents=b'123')
-        time.sleep(1)  # file2 must have newer timestamps than file1
+        granularity_sleep()  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=10)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         output = self.cmd('create', '--list', '--files-cache=ctime,size', self.repository_location + '::test1', 'input')
@@ -2105,7 +2105,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
     def test_file_status_ms_cache_mode(self):
         """test that a chmod'ed file with no content changes does not get chunked again in mtime,size cache_mode"""
         self.create_regular_file('file1', size=10)
-        time.sleep(1)  # file2 must have newer timestamps than file1
+        granularity_sleep()  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=10)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         output = self.cmd('create', '--list', '--files-cache=mtime,size', self.repository_location + '::test1', 'input')
@@ -2119,7 +2119,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
     def test_file_status_rc_cache_mode(self):
         """test that files get rechunked unconditionally in rechunk,ctime cache mode"""
         self.create_regular_file('file1', size=10)
-        time.sleep(1)  # file2 must have newer timestamps than file1
+        granularity_sleep()  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=10)
         self.cmd('init', '--encryption=repokey', self.repository_location)
         output = self.cmd('create', '--list', '--files-cache=rechunk,ctime', self.repository_location + '::test1', 'input')
@@ -2131,7 +2131,7 @@ class ArchiverTestCase(ArchiverTestCaseBase):
         """test that excluded paths are listed"""
 
         self.create_regular_file('file1', size=1024 * 80)
-        time.sleep(1)  # file2 must have newer timestamps than file1
+        granularity_sleep()  # file2 must have newer timestamps than file1
         self.create_regular_file('file2', size=1024 * 80)
         if has_lchflags:
             self.create_regular_file('file3', size=1024 * 80)
@@ -4753,7 +4753,7 @@ class DiffArchiverTestCase(ArchiverTestCaseBase):
         self.create_regular_file('file_replaced', contents=b'0' * 4096)
         os.unlink('input/file_removed')
         os.unlink('input/file_removed2')
-        time.sleep(1)  # macOS HFS+ has a 1s timestamp granularity
+        granularity_sleep()  # cover FS timestamp granularity differences (e.g. HFS+ 1s)
         Path('input/file_touched').touch()
         os.rmdir('input/dir_replaced_with_file')
         self.create_regular_file('dir_replaced_with_file', size=8192)
@@ -5053,20 +5053,15 @@ class DiffArchiverTestCase(ArchiverTestCaseBase):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.create_regular_file("test_file", size=10)
         self.cmd('create', self.repository_location + '::archive1', 'input')
-        time.sleep(0.1)
+        granularity_sleep()
         os.unlink("input/test_file")
-        if is_win32:
-            # Sleeping for 15s because Windows doesn't refresh ctime if file is deleted and recreated within 15 seconds.
-            time.sleep(15)
-        elif is_darwin:
-            time.sleep(1)  # HFS has a 1s timestamp granularity
+        granularity_sleep(ctime_quirk=True)
         self.create_regular_file("test_file", size=15)
         self.cmd('create', self.repository_location + '::archive2', 'input')
         output = self.cmd("diff", self.repository_location + "::archive1", "archive2")
         self.assert_in("mtime", output)
         self.assert_in("ctime", output)  # Should show up on windows as well since it is a new file.
-        if is_darwin:
-            time.sleep(1)  # HFS has a 1s timestamp granularity
+        granularity_sleep()
         os.chmod("input/test_file", 0o777)
         self.cmd('create', self.repository_location + '::archive3', 'input')
         output = self.cmd("diff", self.repository_location + "::archive2", "archive3")