From 17412cf4f067a5625c7825891d15cb43a2781fb1 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 18 Aug 2026 23:42:03 +0200 Subject: [PATCH] tests: fix inode-order dependence in test_multiple_link_exclusion, fixes #8740 create processes directory entries in inode order (scandir_inorder), so which of the two hardlinks becomes the hardlink master depends on the inode numbers the filesystem assigned to the dirs a and b. On linux ext4 these follow creation order, but e.g. FreeBSD ufs dirpref spreads new dirs over cylinder groups, so b may get a lower inode than a - then b/hardlink became the master and diff of only input/b succeeded (rc 0) instead of warning about the excluded hardlink source (rc 1). Determine master/slave from the actual dir inode numbers and exclude the master's dir, making the test work on any filesystem. Also drop the FreeBSD/NetBSD skip: this failure was traversal-order dependence, not the ctime quirks of #9147/#9153. Co-Authored-By: Claude Fable 5 --- src/borg/testsuite/archiver.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/borg/testsuite/archiver.py b/src/borg/testsuite/archiver.py index 9a4768ee46..98d5f71954 100644 --- a/src/borg/testsuite/archiver.py +++ b/src/borg/testsuite/archiver.py @@ -5450,10 +5450,6 @@ def test_time_diffs(self): @requires_hardlinks - @pytest.mark.skipif( - (not are_hardlinks_supported()) or is_freebsd or is_netbsd, - reason='Skip when hardlinks unsupported or on FreeBSD/NetBSD due to differing ctime/link handling; see #9147, #9153.', - ) def test_multiple_link_exclusion(self): path_a = os.path.join(self.input_path, 'a') path_b = os.path.join(self.input_path, 'b') @@ -5463,12 +5459,20 @@ def test_multiple_link_exclusion(self): hl_b = os.path.join(path_b, 'hardlink') self.create_regular_file(hl_a, contents=b'123456') os.link(hl_a, hl_b) + # create processes dir entries in inode order (scandir_inorder), so the hardlink in the + # dir with the lower inode number becomes the hardlink master. dirs do not necessarily + # get their inode numbers assigned in creation order (e.g. ufs dirpref spreads dirs over + # cylinder groups), so determine master/slave from the actual inode numbers, see #8740. + if os.stat(path_a).st_ino < os.stat(path_b).st_ino: + hl_master, slave_dir = hl_a, 'b' + else: + hl_master, slave_dir = hl_b, 'a' self.cmd('init', '--encryption=repokey', self.repository_location) self.cmd('create', self.repository_location + '::test0', 'input') - os.unlink(hl_a) # Don't duplicate warning message- one is enough. + os.unlink(hl_master) # Don't duplicate warning message- one is enough. self.cmd('create', self.repository_location + '::test1', 'input') - output = self.cmd('diff', '--pattern=+ fm:input/b', '--pattern=! **/', self.repository_location + '::test0', 'test1', exit_code=EXIT_WARNING) + output = self.cmd('diff', '--pattern=+ fm:input/' + slave_dir, '--pattern=! **/', self.repository_location + '::test0', 'test1', exit_code=EXIT_WARNING) lines = output.splitlines() self.assert_line_exists(lines, 'cannot find hardlink source for.*skipping compare.')