From c0cff447e029092d9a826b6d437ad56aca10b139 Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 09:22:29 +1030 Subject: [PATCH 1/2] Skip the symlinked-temp test where there's no O_NOFOLLOW On Windows, O_EXCL on a dangling symlink creates its target, so write_atomic can't refuse one there (mkstemp, which it replaced, couldn't either). The docstring now says so. --- rnfs/fsutil.py | 7 ++++--- tests/test_fsutil.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rnfs/fsutil.py b/rnfs/fsutil.py index d28ad76..ffd4fe4 100644 --- a/rnfs/fsutil.py +++ b/rnfs/fsutil.py @@ -132,9 +132,10 @@ def _temp_path(directory: str) -> str: def write_atomic(path: str, data: bytes, *, mode: int = 0o644, durable: bool = True, exclusive: bool = False): """Replace path with data, so a crash or power loss leaves the old or the new file whole. Raises OSError. - data goes to a new temporary file in path's directory (O_EXCL and O_NOFOLLOW: never an existing file or - symlink, created with mode, which the umask can only narrow), synced to disk and renamed over path (retried - on Windows while path is in use); a symlink at path is replaced, not written through. If durable, the + data goes to a new temporary file in path's directory (O_EXCL, plus O_NOFOLLOW where it exists, i.e. not on + Windows: never an existing file or symlink; created with mode, which the umask can only narrow), synced to + disk and renamed over path (retried on Windows while path is in use); a symlink at path is replaced, not + written through. If durable, the directory is then synced so the rename survives a power loss too. With exclusive, an existing path is kept and FileExistsError raised instead. On any failure before the rename the temporary file is removed and path is left as it was. diff --git a/tests/test_fsutil.py b/tests/test_fsutil.py index 4857e93..e1a2fc8 100644 --- a/tests/test_fsutil.py +++ b/tests/test_fsutil.py @@ -206,7 +206,8 @@ def test_symlink_at_path_is_replaced_not_followed(self): self.assertFalse(os.path.islink(self.path)) self.assert_only_file(b"new") - @unittest.skipUnless(CAN_SYMLINK, "symlinks") + # Windows has no O_NOFOLLOW, and O_EXCL there creates a dangling symlink's target (as mkstemp would) + @unittest.skipUnless(CAN_SYMLINK and fsutil.O_NOFOLLOW, "symlinks and O_NOFOLLOW") def test_refuses_a_symlink_at_the_temporary_name(self): # Someone able to guess the name plants a symlink there: it is neither followed nor removed temp = os.path.join(self.tmp.name, fsutil.PARTIAL_PREFIX + "planted") From 6fd25559eac7aa10924da4ff22214519802406bf Mon Sep 17 00:00:00 2001 From: Afi Date: Tue, 13 Oct 2026 10:01:08 +1030 Subject: [PATCH 2/2] Wait for the publisher in test_remote_pin_within_quota The remote pin worker refreshes the publisher just after recording the pin, so checking provided() right after the pin appears raced on slow runners (failed once on Windows, Python 3.13). --- tests/test_node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_node.py b/tests/test_node.py index 4a85129..0a69e29 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -353,7 +353,8 @@ def test_remote_pin_within_quota(self): self.assertTrue(self.a.backend.remote_pin(IDENTITY, Cid.parse(cid), True)) source = remote_source(IDENTITY) self.assertTrue(self.wait_for(lambda: source in self.a.storage.pins().get(Cid.parse(cid), set()))) - self.assertIn(Cid.parse(cid), self.a.publisher.provided()) + # The worker refreshes the publisher just after pinning + self.assertTrue(self.wait_for(lambda: Cid.parse(cid) in self.a.publisher.provided())) self.assertTrue(self.a.backend.remote_pin(IDENTITY, Cid.parse(cid), False)) self.assertEqual(self.a.storage.pins(), {})