44import asyncio
55import os
66import shutil
7+ import warnings
78from pathlib import Path
89from tempfile import NamedTemporaryFile
10+ from typing import TYPE_CHECKING
911from unittest .mock import patch
1012
1113import pytest
1214
15+ if TYPE_CHECKING :
16+ from collections .abc import Callable
17+
1318from marimo ._dependencies .dependencies import DependencyManager
1419from marimo ._utils import async_path
1520from marimo ._utils .file_watcher import FileWatcherManager , PollingFileWatcher
1621
1722
23+ async def _wait_for (
24+ predicate : Callable [[], bool ],
25+ * ,
26+ timeout : float = 0.5 ,
27+ interval : float = 0.05 ,
28+ ) -> None :
29+ loop = asyncio .get_event_loop ()
30+ deadline = loop .time () + timeout
31+ while loop .time () < deadline :
32+ if predicate ():
33+ return
34+ await asyncio .sleep (interval )
35+ # Surface a timeout so reruns/flakes leave a breadcrumb instead of
36+ # failing silently at the next assert with a misleading count.
37+ warnings .warn (
38+ f"_wait_for timed out after { timeout } s waiting on "
39+ f"{ getattr (predicate , '__name__' , repr (predicate ))} " ,
40+ stacklevel = 2 ,
41+ )
42+
43+
44+ @pytest .mark .flaky (reruns = 3 )
1845async def test_polling_file_watcher () -> None :
1946 with NamedTemporaryFile (delete = False ) as tmp_file :
2047 tmp_path = Path (tmp_file .name )
@@ -37,7 +64,7 @@ async def test_callback(path: Path):
3764 f .write ("modification" )
3865
3966 # Wait for the watcher to detect the change
40- await asyncio . sleep ( 0.2 )
67+ await _wait_for ( lambda : len ( callback_calls ) == 1 )
4168
4269 # Stop / cleanup
4370 watcher .stop ()
@@ -48,6 +75,7 @@ async def test_callback(path: Path):
4875 assert callback_calls [0 ] == tmp_path
4976
5077
78+ @pytest .mark .flaky (reruns = 3 )
5179async def test_file_watcher_manager () -> None :
5280 # Create two temporary files
5381 with (
@@ -90,7 +118,9 @@ async def callback3(path: Path) -> None:
90118 f .write ("modification1" )
91119
92120 # Wait for callbacks
93- await asyncio .sleep (0.2 )
121+ await _wait_for (
122+ lambda : len (callback1_calls ) == 1 and len (callback2_calls ) == 1
123+ )
94124
95125 # Both callbacks should be called for file1
96126 assert len (callback1_calls ) == 1
@@ -102,12 +132,16 @@ async def callback3(path: Path) -> None:
102132 # Remove one callback from file1
103133 manager .remove_callback (tmp_path1 , callback1 )
104134
135+ # Space writes so the second mtime is distinguishable from the first
136+ # on filesystems with coarse mtime granularity (e.g. HFS+).
137+ await asyncio .sleep (0.05 )
138+
105139 # Modify file1 again
106140 with open (tmp_path1 , "w" ) as f : # noqa: ASYNC230
107141 f .write ("modification2" )
108142
109143 # Wait for callbacks
110- await asyncio . sleep ( 0. 2 )
144+ await _wait_for ( lambda : len ( callback2_calls ) == 2 )
111145
112146 # Only callback2 should be called again
113147 assert len (callback1_calls ) == 1 # unchanged
@@ -119,7 +153,7 @@ async def callback3(path: Path) -> None:
119153 f .write ("modification3" )
120154
121155 # Wait for callbacks
122- await asyncio . sleep ( 0.2 )
156+ await _wait_for ( lambda : len ( callback3_calls ) == 1 )
123157
124158 # callback3 should be called for file2
125159 assert len (callback1_calls ) == 1
@@ -137,7 +171,8 @@ async def callback3(path: Path) -> None:
137171 with open (tmp_path2 , "w" ) as f : # noqa: ASYNC230
138172 f .write ("modification4" )
139173
140- # Wait for potential callbacks
174+ # Wait for potential callbacks (negative assertion — keep a fixed
175+ # budget since there's nothing to poll on).
141176 await asyncio .sleep (0.2 )
142177
143178 # No new calls should happen
0 commit comments