diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6b02fd1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,44 @@
+name: CI
+
+on: [push, pull_request]
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@nightly
+ with:
+ components: clippy
+ - name: Clippy
+ run: cargo clippy --all-features --all-targets -- -Dwarnings
+ - name: Test
+ run: cargo test --all-features
+ doc:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@nightly
+ - name: Build docs
+ run: |
+ cargo doc --all-features --no-deps
+ printf '' $(cargo tree | head -1 | cut -d' ' -f1 | tr '-' '_') > target/doc/index.html
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: target/doc
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: doc
+ if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index acc0338..b230bcd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7,8 +7,10 @@ name = "axpoll"
version = "0.1.0"
dependencies = [
"bitflags",
+ "futures",
"linux-raw-sys",
"spin",
+ "tokio",
]
[[package]]
@@ -17,14 +19,183 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tokio"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+dependencies = [
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
diff --git a/Cargo.toml b/Cargo.toml
index 2282781..6509be9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,3 +19,7 @@ linux-raw-sys = { version = "0.11", default-features = false, features = [
"general",
] }
spin = { version = "0.10", default-features = false, features = ["spin_mutex"] }
+
+[dev-dependencies]
+futures = "0.3"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
\ No newline at end of file
diff --git a/tests/async.rs b/tests/async.rs
new file mode 100644
index 0000000..3d1cd5e
--- /dev/null
+++ b/tests/async.rs
@@ -0,0 +1,102 @@
+use std::{
+ future::Future,
+ pin::Pin,
+ sync::{
+ Arc,
+ atomic::{AtomicBool, AtomicUsize, Ordering},
+ },
+ task::{Context, Poll},
+ time::Duration,
+};
+
+use axpoll::PollSet;
+use tokio::time;
+
+struct WaitFuture {
+ ps: Arc,
+ ready: Arc,
+}
+
+impl Future for WaitFuture {
+ type Output = ();
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
+ if self.ready.load(Ordering::SeqCst) {
+ Poll::Ready(())
+ } else {
+ self.ps.register(cx.waker());
+ Poll::Pending
+ }
+ }
+}
+
+impl WaitFuture {
+ fn new(ps: Arc, ready: Arc) -> Self {
+ Self { ps, ready }
+ }
+}
+
+struct Counter(AtomicUsize);
+
+impl Counter {
+ fn new() -> Arc {
+ Arc::new(Self(AtomicUsize::new(0)))
+ }
+
+ fn count(&self) -> usize {
+ self.0.load(Ordering::SeqCst)
+ }
+
+ fn add(&self) {
+ self.0.fetch_add(1, Ordering::SeqCst);
+ }
+}
+
+#[tokio::test]
+async fn async_wake_single() {
+ let ps = Arc::new(PollSet::new());
+ let ready = Arc::new(AtomicBool::new(false));
+
+ let f = WaitFuture::new(ps.clone(), ready.clone());
+
+ let handle = tokio::spawn(async move {
+ ready.clone().store(true, Ordering::SeqCst);
+ ps.clone().wake();
+ });
+
+ f.await;
+ handle.await.unwrap();
+}
+
+#[tokio::test]
+async fn async_wake_many() {
+ let ps = Arc::new(PollSet::new());
+ let counter = Counter::new();
+
+ let mut flags = Vec::new();
+ let mut handles = Vec::new();
+
+ for _ in 0..65 {
+ let flag = Arc::new(AtomicBool::new(false));
+ let f = WaitFuture::new(ps.clone(), flag.clone());
+ let counter = counter.clone();
+ let h = tokio::spawn(async move {
+ f.await;
+ counter.add();
+ });
+ flags.push(flag);
+ handles.push(h);
+ }
+
+ time::sleep(Duration::from_millis(20)).await;
+
+ for f in &flags {
+ f.store(true, Ordering::SeqCst);
+ }
+ ps.wake();
+ for h in handles {
+ h.await.unwrap();
+ }
+
+ assert_eq!(counter.count(), 65);
+}
diff --git a/tests/tests.rs b/tests/tests.rs
new file mode 100644
index 0000000..57b316a
--- /dev/null
+++ b/tests/tests.rs
@@ -0,0 +1,92 @@
+use std::{
+ sync::{
+ Arc,
+ atomic::{AtomicUsize, Ordering},
+ },
+ task::{Context, Wake, Waker},
+};
+
+use axpoll::PollSet;
+
+struct Counter(AtomicUsize);
+
+impl Counter {
+ fn new() -> Arc {
+ Arc::new(Self(AtomicUsize::new(0)))
+ }
+
+ fn count(&self) -> usize {
+ self.0.load(Ordering::SeqCst)
+ }
+
+ fn add(&self) {
+ self.0.fetch_add(1, Ordering::SeqCst);
+ }
+}
+
+impl Wake for Counter {
+ fn wake(self: Arc) {
+ self.add();
+ }
+
+ fn wake_by_ref(self: &Arc) {
+ self.add();
+ }
+}
+
+#[test]
+fn register_and_wake() {
+ let ps = PollSet::new();
+ let counter = Counter::new();
+ let w = Waker::from(counter.clone());
+ ps.register(&w);
+ assert_eq!(ps.wake(), 1);
+ assert_eq!(counter.count(), 1);
+}
+
+#[test]
+fn empty_return() {
+ let ps = PollSet::new();
+ assert_eq!(ps.wake(), 0);
+}
+
+#[test]
+fn full_capacity() {
+ let ps = PollSet::new();
+ let counter = Counter::new();
+ for _ in 0..64 {
+ let w = Waker::from(counter.clone());
+ let cx = Context::from_waker(&w);
+ ps.register(cx.waker());
+ }
+ let woke = ps.wake();
+ assert_eq!(woke, 64);
+ assert_eq!(counter.count(), 64);
+}
+
+#[test]
+fn overwrite() {
+ let ps = PollSet::new();
+ let counters = (0..65).map(|_| Counter::new()).collect::>();
+ for c in &counters {
+ let w = Waker::from(c.clone());
+ let cx = Context::from_waker(&w);
+ ps.register(cx.waker());
+ }
+ assert_eq!(ps.wake(), 64);
+ let total: usize = counters.iter().map(|c| c.count()).sum();
+ assert_eq!(total, 65);
+}
+
+#[test]
+fn drop_wakes() {
+ let ps = PollSet::new();
+ let counters = Counter::new();
+ for _ in 0..10 {
+ let w = Waker::from(counters.clone());
+ let cx = Context::from_waker(&w);
+ ps.register(cx.waker());
+ }
+ drop(ps);
+ assert_eq!(counters.count(), 10);
+}