Skip to content

Commit 36ffeb9

Browse files
committed
Proper tests for the async functionality
1 parent f84edea commit 36ffeb9

6 files changed

Lines changed: 185 additions & 88 deletions

File tree

src/hotplug.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
//! Support for hotplug events.
22
//!
3-
//! This module is currently Linux-specific. It uses the udev netlink socket to listen for events
4-
//! from a udev implementation.
3+
//! The recommended way to support device hotplug in applications is to use the
4+
//! [`hotplug::enumerate`] function, which returns an iterator over all devices that are or will be
5+
//! plugged into the system.
56
//!
6-
//! The recommended way to support device hotplug is to use the [`hotplug::enumerate`] function,
7-
//! which returns an iterator over all devices that are or will be plugged into the system.
7+
//! # Platform Support
8+
//!
9+
//! Hotplug functionality is supported on Linux and FreeBSD, as follows:
10+
//!
11+
//! | OS | Details |
12+
//! |----|---------|
13+
//! | Linux | Uses the `NETLINK_KOBJECT_UEVENT` socket. Requires `udev`. |
14+
//! | FreeBSD | Uses `devd`'s seqpacket socket at `/var/run/devd.seqpacket.pipe`. |
815
//!
916
//! [`hotplug::enumerate`]: crate::hotplug::enumerate
1017
@@ -114,6 +121,8 @@ impl HotplugMonitor {
114121
///
115122
/// The [`HotplugMonitor`] will be put in non-blocking mode while the [`AsyncIter`] is alive
116123
/// (if it isn't already).
124+
///
125+
/// When using the `"tokio"` Cargo feature, this must be called while inside a tokio context.
117126
#[cfg_attr(docsrs, doc(cfg(any(doc, feature = "tokio", feature = "async-io"))))]
118127
#[cfg(any(doc, feature = "tokio", feature = "async-io"))]
119128
pub fn async_iter(&self) -> io::Result<AsyncIter<'_>> {

src/hotplug/async.rs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,29 +32,38 @@ impl<'a> AsyncIter<'a> {
3232

3333
#[cfg(test)]
3434
mod tests {
35-
use std::{io, pin::pin};
35+
use std::io;
3636

37-
use crate::{
38-
hotplug::HotplugMonitor, test::AssertPending, uinput::UinputDevice,
39-
util::r#async::with_runtime,
40-
};
37+
use crate::{hotplug::HotplugMonitor, uinput::UinputDevice, util::r#async::test::AsyncTest};
4138

4239
#[test]
4340
fn smoke() -> io::Result<()> {
44-
with_runtime(|rt| {
45-
const DEVNAME: &str = "-@-rust-async-hotplug-test-@-";
41+
env_logger::try_init().ok();
4642

47-
let mon = HotplugMonitor::new()?;
43+
const DEVNAME: &str = "-@-rust-async-hotplug-test-@-";
4844

49-
let events = mon.async_iter()?;
50-
let mut fut = pin!(events.next_event());
51-
rt.block_on(AssertPending(fut.as_mut()));
52-
53-
let _uinput = UinputDevice::builder()?.build(DEVNAME)?;
54-
55-
rt.block_on(fut)?;
45+
let mon = HotplugMonitor::new()?;
5646

47+
let mut uinput = None;
48+
let fut = async {
49+
// Wait for our test device to arrive:
50+
loop {
51+
let evdev = mon.async_iter()?.next_event().await?;
52+
if evdev.name()? == DEVNAME {
53+
return Ok(evdev);
54+
}
55+
}
56+
};
57+
AsyncTest::new(fut, || {
58+
uinput = Some(UinputDevice::builder()?.build(DEVNAME)?);
59+
println!("unblocked");
5760
Ok(())
5861
})
62+
// This test might take a few tries since unrelated events need to be filtered out, and
63+
// unrelated messages may arrive at the socket, causing a wakeup that results in `Pending`.
64+
.allowed_polls(1024)
65+
.run()?;
66+
67+
Ok(())
5968
}
6069
}

src/reader.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,8 @@ impl EventReader {
942942
///
943943
/// The underlying device will be put in non-blocking mode while the returned [`AsyncEvents`]
944944
/// is alive (if it isn't already).
945+
///
946+
/// When using the `"tokio"` Cargo feature, this must be called while inside a tokio context.
945947
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-io"))))]
946948
#[cfg(any(feature = "tokio", feature = "async-io"))]
947949
pub fn async_events(&mut self) -> io::Result<AsyncEvents<'_>> {
@@ -952,6 +954,8 @@ impl EventReader {
952954
///
953955
/// The underlying device will be put in non-blocking mode while the returned [`AsyncReports`]
954956
/// is alive (if it isn't already).
957+
///
958+
/// When using the `"tokio"` Cargo feature, this must be called while inside a tokio context.
955959
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio", feature = "async-io"))))]
956960
#[cfg(any(feature = "tokio", feature = "async-io"))]
957961
pub fn async_reports(&mut self) -> io::Result<AsyncReports<'_>> {

src/reader/async.rs

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -77,49 +77,43 @@ impl<'a> AsyncEvents<'a> {
7777

7878
#[cfg(test)]
7979
mod tests {
80-
use std::{io, pin::pin};
80+
use std::io;
8181

8282
use crate::{
8383
event::{Rel, RelEvent, Syn},
84-
test::{AssertPending, check_events, pair},
85-
util::r#async::with_runtime,
84+
test::{check_events, pair},
85+
util::r#async::test::AsyncTest,
8686
};
8787

8888
#[test]
8989
fn smoke() -> io::Result<()> {
90-
with_runtime(|rt| {
91-
let (uinput, evdev) = pair(|b| b.with_rel_axes([Rel::DIAL]))?;
92-
let mut reader = evdev.into_reader()?;
93-
let mut events = reader.async_events()?;
90+
let (uinput, evdev) = pair(|b| b.with_rel_axes([Rel::DIAL]))?;
91+
let mut reader = evdev.into_reader()?;
9492

95-
{
96-
let mut fut = pin!(events.next_event());
97-
rt.block_on(AssertPending(fut.as_mut()));
98-
99-
uinput.write(&[RelEvent::new(Rel::DIAL, 1).into()])?;
100-
101-
let event = rt.block_on(fut)?;
102-
check_events([event], [RelEvent::new(Rel::DIAL, 1).into()]);
103-
}
93+
{
94+
let event = AsyncTest::new(async { reader.async_events()?.next_event().await }, || {
95+
uinput.write(&[RelEvent::new(Rel::DIAL, 1).into()])
96+
})
97+
.run()?;
10498

105-
drop(events);
106-
let ev = reader.events().next().unwrap()?;
107-
check_events([ev], [Syn::REPORT.into()]);
99+
check_events([event], [RelEvent::new(Rel::DIAL, 1).into()]);
100+
}
108101

109-
let mut reports = reader.async_reports()?;
110-
let mut fut = pin!(reports.next_report());
111-
rt.block_on(AssertPending(fut.as_mut()));
102+
let ev = reader.events().next().unwrap()?;
103+
check_events([ev], [Syn::REPORT.into()]);
112104

113-
uinput.write(&[RelEvent::new(Rel::DIAL, 2).into()])?;
105+
let report = AsyncTest::new(
106+
async { reader.async_reports()?.next_report().await },
107+
|| uinput.write(&[RelEvent::new(Rel::DIAL, 2).into()]),
108+
)
109+
.run()?;
114110

115-
let report = rt.block_on(fut)?;
116-
assert_eq!(report.len(), 2);
117-
check_events(
118-
report,
119-
[RelEvent::new(Rel::DIAL, 2).into(), Syn::REPORT.into()],
120-
);
111+
assert_eq!(report.len(), 2);
112+
check_events(
113+
report,
114+
[RelEvent::new(Rel::DIAL, 2).into(), Syn::REPORT.into()],
115+
);
121116

122-
Ok(())
123-
})
117+
Ok(())
124118
}
125119
}

src/test.rs

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
#![allow(dead_code)]
2-
31
use std::{
4-
fmt,
52
hash::{BuildHasher, Hasher, RandomState},
63
io,
74
iter::zip,
8-
pin::Pin,
9-
task::{Context, Poll},
105
};
116

127
use crate::{
@@ -16,12 +11,13 @@ use crate::{
1611
uinput::{Builder, UinputDevice},
1712
};
1813

19-
fn hash() -> u64 {
20-
RandomState::new().build_hasher().finish()
21-
}
22-
2314
/// Creates a [`UinputDevice`] and [`Evdev`] that are connected to each other.
15+
#[allow(dead_code)]
2416
pub fn pair(b: impl FnOnce(Builder) -> io::Result<Builder>) -> io::Result<(UinputDevice, Evdev)> {
17+
fn hash() -> u64 {
18+
RandomState::new().build_hasher().finish()
19+
}
20+
2521
let hash = hash();
2622
let name = format!("-@-rust-evdevil-device-{hash}-@-");
2723

@@ -73,20 +69,3 @@ pub fn check_events(
7369
panic!("expected {expected:?}, got {actual:?}");
7470
}
7571
}
76-
77-
/// A `Future` that polls its argument once and panics unless the inner poll results in `Pending`.
78-
pub struct AssertPending<'a, F>(pub Pin<&'a mut F>);
79-
80-
impl<'a, F: Future> Future for AssertPending<'a, F>
81-
where
82-
F::Output: fmt::Debug,
83-
{
84-
type Output = ();
85-
86-
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
87-
match self.0.as_mut().poll(cx) {
88-
Poll::Ready(val) => panic!("expected `Pending`, got `Ready`: {val:?}"),
89-
Poll::Pending => Poll::Ready(()),
90-
}
91-
}
92-
}

src/util/async.rs

Lines changed: 114 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,9 @@ use asyncio_impl::*;
116116
#[cfg(feature = "async-io")]
117117
mod asyncio_impl {
118118
use std::{
119-
io,
119+
future, io,
120120
os::fd::{BorrowedFd, RawFd},
121+
pin::pin,
121122
task::Poll,
122123
};
123124

@@ -129,7 +130,7 @@ mod asyncio_impl {
129130
impl Impl {
130131
pub fn new(fd: RawFd) -> io::Result<Self> {
131132
let fd = unsafe { BorrowedFd::borrow_raw(fd) };
132-
Async::new(fd).map(Self)
133+
Async::new_nonblocking(fd).map(Self)
133134
}
134135

135136
pub async fn asyncify<T>(
@@ -138,13 +139,33 @@ mod asyncio_impl {
138139
) -> io::Result<T> {
139140
loop {
140141
match op() {
141-
Poll::Pending => self.0.readable().await?,
142+
Poll::Pending => optimistic(self.0.readable()).await?,
142143
Poll::Ready(res) => return res,
143144
}
144145
}
145146
}
146147
}
147148

149+
// This "optimization" is copied from async-io.
150+
// async-io is apparently very buggy (see smol-rs/async-io#78), so it ends up being required for
151+
// things to work right.
152+
// Specifically, the `.readable()` future is permanently `Pending`, even after the reactor
153+
// schedules the future again, so `asyncify` would just never complete.
154+
async fn optimistic(fut: impl Future<Output = io::Result<()>>) -> io::Result<()> {
155+
let mut polled = false;
156+
let mut fut = pin!(fut);
157+
158+
future::poll_fn(|cx| {
159+
if !polled {
160+
polled = true;
161+
fut.as_mut().poll(cx)
162+
} else {
163+
Poll::Ready(Ok(()))
164+
}
165+
})
166+
.await
167+
}
168+
148169
#[cfg(test)]
149170
pub struct Runtime;
150171

@@ -169,14 +190,95 @@ pub struct Impl;
169190
#[cfg(doc)]
170191
pub struct Runtime;
171192

172-
/// Calls `f` with an instance of the selected async runtime.
173-
///
174-
/// Allows writing async-runtime-agnostic tests.
175-
///
176-
/// The only supported API is `runtime.block_on(future)`.
177193
#[cfg(test)]
178-
pub fn with_runtime<R>(f: impl FnOnce(&Runtime) -> io::Result<R>) -> io::Result<R> {
179-
let rt = Runtime::new()?;
180-
let _guard = rt.enter();
181-
f(&rt)
194+
pub mod test {
195+
use std::{fmt, future, panic::resume_unwind, pin::pin, sync::mpsc, thread};
196+
197+
use super::*;
198+
199+
pub struct AsyncTest<F, U> {
200+
future: F,
201+
unblocker: U,
202+
allowed_polls: usize,
203+
}
204+
205+
impl<F, U> AsyncTest<F, U> {
206+
pub fn new(future: F, unblocker: U) -> Self {
207+
Self {
208+
future,
209+
unblocker,
210+
allowed_polls: 1,
211+
}
212+
}
213+
214+
/// Sets the number of allowed future polls after the `unblocker` has been run.
215+
///
216+
/// By default, this is 1, expecting the future to complete immediately after the waker has
217+
/// been notified.
218+
/// Higher values may be needed if the API-under-test is system-global and may have to
219+
/// process some irrelevant events until it becomes `Ready`.
220+
pub fn allowed_polls(mut self, allowed_polls: usize) -> Self {
221+
self.allowed_polls = allowed_polls;
222+
self
223+
}
224+
225+
/// Polls `future`, expecting `Poll::Pending`. Then runs `unblocker`, and expects the waker to
226+
/// be invoked and the `future` to be `Poll::Ready`.
227+
pub fn run<T>(self) -> io::Result<T>
228+
where
229+
F: Future<Output = io::Result<T>> + Send,
230+
F::Output: Send,
231+
U: FnOnce() -> io::Result<()>,
232+
T: fmt::Debug,
233+
{
234+
let (sender, recv) = mpsc::sync_channel(0);
235+
thread::scope(|s| {
236+
let h = s.spawn(move || -> io::Result<_> {
237+
let rt = Runtime::new()?;
238+
let _guard = rt.enter();
239+
let mut fut = pin!(self.future);
240+
let mut poll_count = 0;
241+
242+
rt.block_on(future::poll_fn(|cx| {
243+
if poll_count == 0 {
244+
match fut.as_mut().poll(cx) {
245+
Poll::Ready(val) => {
246+
panic!("expected future to be `Pending`, but it is `Ready({val:?})`")
247+
}
248+
Poll::Pending => {
249+
// Waker is now scheduled to be woken when the event of interest occurs.
250+
println!("future is pending; scheduling wakeup");
251+
poll_count += 1;
252+
sender.send(()).unwrap();
253+
return Poll::Pending;
254+
}
255+
}
256+
} else {
257+
// This is called when the `Waker` has been woken up.
258+
match fut.as_mut().poll(cx) {
259+
Poll::Ready(out) => Poll::Ready(out),
260+
Poll::Pending => {
261+
if poll_count >= self.allowed_polls {
262+
panic!("future still `Pending` after {poll_count} polls");
263+
}
264+
poll_count += 1;
265+
Poll::Pending
266+
}
267+
}
268+
}
269+
}))
270+
});
271+
272+
recv.recv().unwrap();
273+
274+
// We've been signaled to invoke `unblocker`.
275+
(self.unblocker)()?;
276+
277+
match h.join() {
278+
Ok(res) => res,
279+
Err(payload) => resume_unwind(payload),
280+
}
281+
})
282+
}
283+
}
182284
}

0 commit comments

Comments
 (0)