Files
lmcache--lmcache/tests/v1/test_event_notifier.py
2026-07-13 12:24:33 +08:00

247 lines
7.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
import os
import select
# Third Party
import pytest
# First Party
from lmcache.v1.platform import (
HAS_EVENTFD,
EventfdNotifier,
EventNotifier,
PipeNotifier,
consume_fd,
create_event_notifier,
)
from lmcache.v1.platform import event_notifier as _canonical_ev_mod
class TestEventNotifierAPI:
"""Test the public API on the current platform."""
def test_create_returns_event_notifier(self):
"""Factory returns an EventNotifier subclass."""
n = create_event_notifier()
try:
assert isinstance(n, EventNotifier)
finally:
n.close()
def test_fileno_is_nonnegative(self):
"""fileno() returns a valid fd."""
n = create_event_notifier()
try:
assert n.fileno() >= 0
finally:
n.close()
def test_notify_then_consume(self):
"""notify() makes the fd readable; consume() drains it."""
n = create_event_notifier()
try:
n.notify()
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
events = poller.poll(100)
assert len(events) > 0
n.consume()
finally:
n.close()
def test_multiple_notify_single_consume(self):
"""Multiple notify() calls are coalesced by one consume()."""
n = create_event_notifier()
try:
n.notify()
n.notify()
n.notify()
n.consume()
# After consume, fd should not be readable
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
events = poller.poll(50)
assert len(events) == 0
finally:
n.close()
def test_consume_without_notify_is_noop(self):
"""consume() without prior notify() does not block or raise."""
n = create_event_notifier()
try:
n.consume() # should not block or raise
finally:
n.close()
def test_close_is_idempotent(self):
"""Calling close() twice does not raise."""
n = create_event_notifier()
n.close()
n.close() # should not raise
def test_context_manager(self):
"""EventNotifier works as a context manager."""
with create_event_notifier() as n:
n.notify()
n.consume()
assert n.fileno() >= 0
class TestPipeNotifier:
"""Force the pipe-based fallback path."""
def test_both_fds_closed(self):
"""close() releases both read and write fds."""
n = PipeNotifier()
r = n.fileno()
# Second fd via notify/consume cycle: after close, both must be gone.
n.notify()
n.consume()
n.close()
with pytest.raises(OSError):
os.fstat(r)
def test_notify_when_pipe_full_is_noop(self):
"""notify() does not raise when pipe buffer is full."""
n = PipeNotifier()
try:
# A POSIX pipe's capacity is at most 1 MiB on current
# Linux/macOS kernels and each notify() writes a single
# byte, so (1 << 21) iterations comfortably saturate it
# while keeping the test fast - notify() itself swallows
# BlockingIOError once the buffer is full.
for _ in range(1 << 21):
n.notify()
# Still a no-op once the pipe is saturated.
n.notify()
finally:
n.close()
def test_pollable(self):
"""PipeNotifier fd is pollable with select.poll."""
n = PipeNotifier()
try:
n.notify()
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
events = poller.poll(100)
assert len(events) > 0
finally:
n.close()
def test_multiple_create_close_cycles(self):
"""Create and close multiple notifiers without leaking."""
for _ in range(20):
n = PipeNotifier()
n.notify()
n.consume()
n.close()
class TestPipeEOF:
"""Regression test: pipe EOF must not spin the drain loop.
``os.read`` returns ``b''`` (not ``BlockingIOError``) when the
write end is closed. Both ``PipeNotifier.consume`` and the
module-level ``consume_fd`` helper share this drain loop, so
they must treat an empty read as "drained" -- otherwise a dead
pipe would hang a background event loop.
"""
def test_consume_fd_returns_on_pipe_eof(self, monkeypatch):
"""consume_fd() returns when the pipe write end is closed."""
monkeypatch.setattr(_canonical_ev_mod, "HAS_EVENTFD", False)
r, w = os.pipe()
os.set_blocking(r, False)
try:
os.close(w) # write end closed -> read end at EOF
consume_fd(r) # must return promptly, not hang
finally:
os.close(r)
class TestConsumeFd:
"""Test the consume_fd utility function."""
def test_consume_fd_after_notify(self):
"""consume_fd() drains a notifier's fd."""
n = create_event_notifier()
try:
n.notify()
consume_fd(n.fileno())
# After consume, fd should not be readable
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
events = poller.poll(50)
assert len(events) == 0
finally:
n.close()
def test_consume_fd_without_signal(self):
"""consume_fd() on unsignaled fd does not block."""
n = create_event_notifier()
try:
consume_fd(n.fileno()) # should not block
finally:
n.close()
class TestBackendSelection:
"""Validate which concrete backend the factory picks."""
def test_factory_matches_platform_capability(self):
"""create_event_notifier() picks the backend for this OS."""
n = create_event_notifier()
try:
if HAS_EVENTFD:
assert isinstance(n, EventfdNotifier)
else:
assert isinstance(n, PipeNotifier)
finally:
n.close()
class TestForcedPipeFallback:
"""Exercise the pipe fallback even when eventfd is available.
On Linux CI ``create_event_notifier()`` would normally return
an ``EventfdNotifier``; monkeypatch ``HAS_EVENTFD`` to force
the pipe path so the non-Linux code is covered on every OS.
"""
@pytest.fixture
def force_pipe(self, monkeypatch):
monkeypatch.setattr(_canonical_ev_mod, "HAS_EVENTFD", False)
yield
def test_factory_returns_pipe_notifier(self, force_pipe):
n = create_event_notifier()
try:
assert isinstance(n, PipeNotifier)
finally:
n.close()
def test_notify_consume_roundtrip(self, force_pipe):
n = create_event_notifier()
try:
n.notify()
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
assert poller.poll(100)
n.consume()
assert not poller.poll(50)
finally:
n.close()
def test_consume_fd_uses_pipe_path(self, force_pipe):
n = create_event_notifier()
try:
n.notify()
consume_fd(n.fileno())
poller = select.poll()
poller.register(n.fileno(), select.POLLIN)
assert not poller.poll(50)
finally:
n.close()