Skip to content

Commit 66c617d

Browse files
committed
test: cover poll notification support
Add a pollable file to the test filesystem and exercise the new Operations.poll() and notify_poll() APIs. The test opens the synthetic file, starts a userspace poll(2) waiter, and waits until the filesystem has received and stored a PollHandle. It then triggers readiness through the existing setxattr command channel. The filesystem marks the file ready, calls notify_poll() and the test verifies that the blocked poller wakes with POLLPRI. This covers the notification path from the low-level FUSE poll callback, through the Python PollHandle wrapper, to fuse_lowlevel_notify_poll(). Signed-off-by: Christopher Obbard <christopher.obbard@linaro.org>
1 parent 35e41d5 commit 66c617d

1 file changed

Lines changed: 96 additions & 7 deletions

File tree

test/test_fs.py

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import multiprocessing
2323
import os
24+
import select
2425
import stat
2526
import threading
2627
import time
@@ -34,6 +35,7 @@
3435
FileInfo,
3536
FUSEError,
3637
InodeT,
38+
PollHandle,
3739
ReaddirToken,
3840
RequestContext,
3941
)
@@ -118,6 +120,38 @@ def test_notify_store(testfs):
118120
assert not fs_state.read_called
119121

120122

123+
def test_notify_poll(testfs):
124+
(mnt_dir, fs_state) = testfs
125+
path = os.path.join(mnt_dir, 'pollable')
126+
127+
with open(path, 'rb', buffering=0) as fh:
128+
poller = select.poll()
129+
poller.register(fh.fileno(), select.POLLPRI)
130+
131+
events = []
132+
133+
def poll_wait():
134+
events.extend(poller.poll(5000))
135+
136+
thread = threading.Thread(target=poll_wait)
137+
thread.start()
138+
139+
deadline = time.monotonic() + 5
140+
while time.monotonic() < deadline and not fs_state.poll_handle_received:
141+
time.sleep(0.01)
142+
143+
assert fs_state.poll_called
144+
assert fs_state.poll_handle_received
145+
assert not events
146+
147+
pyfuse3.setxattr(mnt_dir, 'command', b'poll_ready')
148+
thread.join(5)
149+
assert not thread.is_alive()
150+
assert events
151+
assert events[0][0] == fh.fileno()
152+
assert events[0][1] & select.POLLPRI
153+
154+
121155
def test_entry_timeout(testfs):
122156
(mnt_dir, fs_state) = testfs
123157
fs_state.entry_timeout = 1
@@ -175,11 +209,17 @@ def __init__(self, cross_process):
175209
self.hello_name = b"message"
176210
self.hello_inode = cast(InodeT, pyfuse3.ROOT_INODE + 1)
177211
self.hello_data = b"hello world\n"
212+
self.poll_name = b"pollable"
213+
self.poll_inode = cast(InodeT, pyfuse3.ROOT_INODE + 2)
214+
self.poll_handle: PollHandle | None = None
178215
self.status = cross_process
179216
self.lookup_cnt = 0
180217
self.status.getattr_called = False
181218
self.status.lookup_called = False
182219
self.status.read_called = False
220+
self.status.poll_called = False
221+
self.status.poll_handle_received = False
222+
self.status.poll_ready = False
183223
self.status.entry_timeout = 99999
184224
self.status.attr_timeout = 99999
185225

@@ -191,6 +231,9 @@ async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> Ent
191231
elif inode == self.hello_inode:
192232
entry.st_mode = stat.S_IFREG | 0o644
193233
entry.st_size = len(self.hello_data)
234+
elif inode == self.poll_inode:
235+
entry.st_mode = stat.S_IFREG | 0o644
236+
entry.st_size = 0
194237
else:
195238
raise pyfuse3.FUSEError(errno.ENOENT)
196239

@@ -212,17 +255,25 @@ async def forget(self, inode_list):
212255
if inode == self.hello_inode:
213256
self.lookup_cnt -= 1
214257
assert self.lookup_cnt >= 0
258+
elif inode == self.poll_inode:
259+
pass
215260
else:
216261
assert inode == pyfuse3.ROOT_INODE
217262

218263
async def lookup(
219264
self, parent_inode: InodeT, name: bytes, ctx: RequestContext
220265
) -> EntryAttributes:
221-
if parent_inode != pyfuse3.ROOT_INODE or name != self.hello_name:
266+
if parent_inode != pyfuse3.ROOT_INODE:
222267
raise pyfuse3.FUSEError(errno.ENOENT)
223-
self.lookup_cnt += 1
268+
224269
self.status.lookup_called = True
225-
return await self.getattr(self.hello_inode, ctx)
270+
if name == self.hello_name:
271+
self.lookup_cnt += 1
272+
return await self.getattr(self.hello_inode, ctx)
273+
if name == self.poll_name:
274+
return await self.getattr(self.poll_inode, ctx)
275+
276+
raise pyfuse3.FUSEError(errno.ENOENT)
226277

227278
async def opendir(self, inode, ctx):
228279
if inode != pyfuse3.ROOT_INODE:
@@ -232,23 +283,53 @@ async def opendir(self, inode, ctx):
232283

233284
async def readdir(self, fh: FileHandleT, start_id: int, token: ReaddirToken) -> None:
234285
assert fh == pyfuse3.ROOT_INODE
235-
if start_id == 0:
236-
pyfuse3.readdir_reply(token, self.hello_name, await self.getattr(self.hello_inode), 1)
237-
return
286+
entries = (
287+
(self.hello_name, self.hello_inode),
288+
(self.poll_name, self.poll_inode),
289+
)
290+
291+
for idx, (name, inode) in enumerate(entries):
292+
if idx < start_id:
293+
continue
294+
if not pyfuse3.readdir_reply(token, name, await self.getattr(inode), idx + 1):
295+
break
238296

239297
async def open(self, inode, flags, ctx):
240-
if inode != self.hello_inode:
298+
if inode not in (self.hello_inode, self.poll_inode):
241299
raise pyfuse3.FUSEError(errno.ENOENT)
242300
if flags & os.O_RDWR or flags & os.O_WRONLY:
243301
raise pyfuse3.FUSEError(errno.EACCES)
244302
# For simplicity, we use the inode as file handle
245303
return FileInfo(fh=FileHandleT(inode))
246304

247305
async def read(self, fh, off, size):
306+
if fh == self.poll_inode:
307+
return b''
308+
248309
assert fh == self.hello_inode
249310
self.status.read_called = True
250311
return self.hello_data[off : off + size]
251312

313+
async def poll(
314+
self,
315+
inode: InodeT,
316+
fh: FileHandleT,
317+
poll_handle: PollHandle | None,
318+
ctx: RequestContext,
319+
) -> int:
320+
assert inode == self.poll_inode
321+
assert fh == self.poll_inode
322+
323+
self.status.poll_called = True
324+
if poll_handle is not None:
325+
self.poll_handle = poll_handle
326+
self.status.poll_handle_received = True
327+
328+
if self.status.poll_ready:
329+
return select.POLLPRI
330+
331+
return 0
332+
252333
async def setxattr(self, inode, name, value, ctx):
253334
if inode != pyfuse3.ROOT_INODE or name != b'command':
254335
raise FUSEError(errno.ENOTSUP)
@@ -267,6 +348,14 @@ async def setxattr(self, inode, name, value, ctx):
267348

268349
elif value == b'terminate':
269350
pyfuse3.terminate()
351+
352+
elif value == b'poll_ready':
353+
self.status.poll_ready = True
354+
if self.poll_handle is None:
355+
raise FUSEError(errno.EINVAL)
356+
pyfuse3.notify_poll(self.poll_handle)
357+
self.poll_handle = None
358+
270359
else:
271360
raise FUSEError(errno.EINVAL)
272361

0 commit comments

Comments
 (0)