@@ -52,23 +52,35 @@ struct DirectoryWatcherTest {
5252 }
5353 }
5454
55+ /// Polls `condition` until it returns true or `timeout` elapses. Used only to wait for the
56+ /// handler's async invocation after a mutation, never to guess how long watcher setup takes.
57+ private func waitUntil( timeout: Duration = . seconds( 10 ) , condition: ( ) -> Bool ) async throws {
58+ let deadline = ContinuousClock . now + timeout
59+ while !condition( ) && ContinuousClock . now < deadline {
60+ try await Task . sleep ( for: . milliseconds( 50 ) )
61+ }
62+ }
63+
5564 @Test func testWatchingExistingDirectory( ) async throws {
5665 try await withTempDir { tempPath in
57-
5866 let watcher = DirectoryWatcher ( directoryPath: tempPath, log: nil )
67+ var readyIterator = await watcher. readyEvents. makeAsyncIterator ( )
5968 let createdPaths = CreatedPaths ( )
6069 let name = " newFile "
6170
62- await watcher. startWatching { [ createdPaths] paths in
71+ try await watcher. startWatching { [ createdPaths] paths in
6372 for path in paths where path. lastComponent? . string == name {
6473 createdPaths. paths. append ( path)
6574 }
6675 }
6776
68- try await Task . sleep ( for: . milliseconds( 100 ) )
77+ // Wait for the watcher to actually resume watching, instead of guessing a sleep
78+ // duration that can race the watcher's own poll cadence.
79+ await readyIterator. next ( )
80+
6981 let newFile = tempPath. appending ( name)
7082 FileManager . default. createFile ( atPath: newFile. string, contents: nil )
71- try await Task . sleep ( for : . milliseconds ( 500 ) )
83+ try await waitUntil { !createdPaths . paths . isEmpty }
7284
7385 #expect( !createdPaths. paths. isEmpty, " directory watcher failed to detect new file " )
7486 #expect( createdPaths. paths. first!. lastComponent? . string == name)
@@ -81,22 +93,25 @@ struct DirectoryWatcherTest {
8193 let childPath = tempPath. appending ( uuid)
8294
8395 let watcher = DirectoryWatcher ( directoryPath: childPath, log: nil )
96+ var readyIterator = await watcher. readyEvents. makeAsyncIterator ( )
8497 let createdPaths = CreatedPaths ( )
8598 let name = " newFile "
8699
87- await watcher. startWatching { [ createdPaths] paths in
100+ try await watcher. startWatching { [ createdPaths] paths in
88101 for path in paths where path. lastComponent? . string == name {
89102 createdPaths. paths. append ( path)
90103 }
91104 }
92105
93- try await Task . sleep ( for: . milliseconds( 100 ) )
94106 try FileManager . default. createDirectory ( atPath: childPath. string, withIntermediateDirectories: true )
95107
96- try await Task . sleep ( for: DirectoryWatcher . watchPeriod)
108+ // Wait for the watcher to actually resume watching, instead of guessing a sleep
109+ // duration that can race the watcher's own poll cadence.
110+ await readyIterator. next ( )
111+
97112 let newFile = childPath. appending ( name)
98113 FileManager . default. createFile ( atPath: newFile. string, contents: nil )
99- try await Task . sleep ( for : . milliseconds ( 500 ) )
114+ try await waitUntil { !createdPaths . paths . isEmpty }
100115
101116 #expect( !createdPaths. paths. isEmpty, " directory watcher failed to detect parent directory " )
102117 #expect( createdPaths. paths. first!. lastComponent? . string == name)
@@ -110,23 +125,25 @@ struct DirectoryWatcherTest {
110125 let childPath = tempPath. appending ( parent) . appending ( child)
111126
112127 let watcher = DirectoryWatcher ( directoryPath: childPath, log: nil )
128+ var readyIterator = await watcher. readyEvents. makeAsyncIterator ( )
113129 let createdPaths = CreatedPaths ( )
114130 let name = " newFile "
115131
116- await watcher. startWatching { paths in
132+ try await watcher. startWatching { paths in
117133 for path in paths where path. lastComponent? . string == name {
118134 createdPaths. paths. append ( path)
119135 }
120136 }
121137
122- try await Task . sleep ( for: . milliseconds( 100 ) )
123138 try FileManager . default. createDirectory ( atPath: childPath. string, withIntermediateDirectories: true )
124139
125- try await Task . sleep ( for: DirectoryWatcher . watchPeriod)
140+ // Wait for the watcher to actually resume watching, instead of guessing a sleep
141+ // duration that can race the watcher's own poll cadence.
142+ await readyIterator. next ( )
126143
127144 let newFile = childPath. appending ( name)
128145 FileManager . default. createFile ( atPath: newFile. string, contents: nil )
129- try await Task . sleep ( for : . milliseconds ( 500 ) )
146+ try await waitUntil { !createdPaths . paths . isEmpty }
130147
131148 #expect( !createdPaths. paths. isEmpty, " directory watcher failed to detect parent directory " )
132149 #expect( createdPaths. paths. first!. lastComponent? . string == name)
@@ -139,36 +156,41 @@ struct DirectoryWatcherTest {
139156 try FileManager . default. createDirectory ( atPath: dirPath. string, withIntermediateDirectories: true )
140157
141158 let watcher = DirectoryWatcher ( directoryPath: dirPath, log: nil )
159+ var readyIterator = await watcher. readyEvents. makeAsyncIterator ( )
142160 let createdPaths = CreatedPaths ( )
143161 let beforeDelete = " beforeDelete "
144162 let afterDelete = " afterDelete "
145163
146- await watcher. startWatching { [ createdPaths] paths in
164+ try await watcher. startWatching { [ createdPaths] paths in
147165 for path in paths
148166 where path. lastComponent? . string == beforeDelete || path. lastComponent? . string == afterDelete {
149167 createdPaths. paths. append ( path)
150168 }
151169 }
152170
153- try await Task . sleep ( for: . milliseconds( 100 ) )
171+ // Wait for the watcher to actually resume watching, instead of guessing a sleep
172+ // duration that can race the watcher's own poll cadence.
173+ await readyIterator. next ( )
174+
154175 let file1 = dirPath. appending ( beforeDelete)
155176 FileManager . default. createFile ( atPath: file1. string, contents: nil )
156- try await Task . sleep ( for : . milliseconds ( 100 ) )
177+ try await waitUntil { createdPaths . paths . contains { $0 . lastComponent ? . string == beforeDelete } }
157178
158179 try FileManager . default. removeItem ( atPath: dirPath. string)
159- try await Task . sleep ( for: . milliseconds( 100 ) )
160180 try FileManager . default. createDirectory ( atPath: dirPath. string, withIntermediateDirectories: true )
161- try await Task . sleep ( for: DirectoryWatcher . watchPeriod)
181+
182+ // `readyEvents` yields once per resume, so this waits however long the watcher
183+ // actually takes to notice the delete, then re-arm on the recreated directory —
184+ // no guessing needed even though this transition takes an unknown amount of time.
185+ await readyIterator. next ( )
162186
163187 let file2 = dirPath. appending ( afterDelete)
164188 FileManager . default. createFile ( atPath: file2. string, contents: nil )
165-
166- try await Task . sleep ( for: . milliseconds( 500 ) )
189+ try await waitUntil { createdPaths. paths. contains { $0. lastComponent? . string == afterDelete } }
167190
168191 #expect( !createdPaths. paths. isEmpty, " directory watcher failed to detect new file " )
169192 #expect(
170193 Set ( createdPaths. paths. compactMap { $0. lastComponent? . string } ) == Set ( [ beforeDelete, afterDelete] ) )
171194 }
172-
173195 }
174196}
0 commit comments