From 021d9c447d5e1d4ddf2dd72a8b2ff043194d314a Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Mon, 22 Sep 2025 21:21:36 -0400 Subject: [PATCH] test: clean up async boundary test --- crates/cli/src/tests.rs | 2 +- crates/cli/src/tests/async_boundary_test.rs | 150 +++++++++++ crates/cli/src/tests/async_context_test.rs | 278 -------------------- 3 files changed, 151 insertions(+), 279 deletions(-) create mode 100644 crates/cli/src/tests/async_boundary_test.rs delete mode 100644 crates/cli/src/tests/async_context_test.rs diff --git a/crates/cli/src/tests.rs b/crates/cli/src/tests.rs index cc4d336d..9fc1c71b 100644 --- a/crates/cli/src/tests.rs +++ b/crates/cli/src/tests.rs @@ -1,4 +1,4 @@ -mod async_context_test; +mod async_boundary_test; mod corpus_test; mod detect_language; mod helpers; diff --git a/crates/cli/src/tests/async_boundary_test.rs b/crates/cli/src/tests/async_boundary_test.rs new file mode 100644 index 00000000..254ed931 --- /dev/null +++ b/crates/cli/src/tests/async_boundary_test.rs @@ -0,0 +1,150 @@ +use std::{ + future::Future, + pin::Pin, + ptr, + task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, +}; + +use tree_sitter::Parser; + +use super::helpers::fixtures::get_language; + +#[test] +fn test_node_across_async_boundaries() { + let mut parser = Parser::new(); + let language = get_language("bash"); + parser.set_language(&language).unwrap(); + let tree = parser.parse("#", None).unwrap(); + let root = tree.root_node(); + + let (result, yields) = simple_async_executor(async { + let root_ref = &root; + + // Test node captured by value + let fut_by_value = async { + yield_once().await; + root.child(0).unwrap().kind() + }; + + // Test node captured by reference + let fut_by_ref = async { + yield_once().await; + root_ref.child(0).unwrap().kind() + }; + + let result1 = fut_by_value.await; + let result2 = fut_by_ref.await; + + assert_eq!(result1, result2); + result1 + }); + + assert_eq!(result, "comment"); + assert_eq!(yields, 2); +} + +#[test] +fn test_cursor_across_async_boundaries() { + let mut parser = Parser::new(); + let language = get_language("c"); + parser.set_language(&language).unwrap(); + let tree = parser.parse("#", None).unwrap(); + let mut cursor = tree.walk(); + + let ((), yields) = simple_async_executor(async { + cursor.goto_first_child(); + + // Test cursor usage across yield point + yield_once().await; + cursor.goto_first_child(); + + // Test cursor in async block + let cursor_ref = &mut cursor; + let fut = async { + yield_once().await; + cursor_ref.goto_first_child(); + }; + fut.await; + }); + + assert_eq!(yields, 2); +} + +#[test] +fn test_node_and_cursor_together() { + let mut parser = Parser::new(); + let language = get_language("javascript"); + parser.set_language(&language).unwrap(); + let tree = parser.parse("#", None).unwrap(); + let root = tree.root_node(); + let mut cursor = tree.walk(); + + let ((), yields) = simple_async_executor(async { + cursor.goto_first_child(); + + let fut = async { + yield_once().await; + let _ = root.to_sexp(); + cursor.goto_first_child(); + }; + + yield_once().await; + fut.await; + }); + + assert_eq!(yields, 2); +} + +fn simple_async_executor(future: F) -> (F::Output, u32) +where + F: Future, +{ + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut yields = 0; + let mut future = Box::pin(future); + + loop { + match future.as_mut().poll(&mut cx) { + Poll::Ready(result) => return (result, yields), + Poll::Pending => yields += 1, + } + } +} + +async fn yield_once() { + struct YieldOnce { + yielded: bool, + } + + impl Future for YieldOnce { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + if self.yielded { + Poll::Ready(()) + } else { + self.yielded = true; + cx.waker().wake_by_ref(); + Poll::Pending + } + } + } + + YieldOnce { yielded: false }.await; +} + +const fn noop_waker() -> Waker { + const VTABLE: RawWakerVTable = RawWakerVTable::new( + // Cloning just returns a new no-op raw waker + |_| RAW, + // `wake` does nothing + |_| {}, + // `wake_by_ref` does nothing + |_| {}, + // Dropping does nothing as we don't allocate anything + |_| {}, + ); + const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE); + unsafe { Waker::from_raw(RAW) } +} diff --git a/crates/cli/src/tests/async_context_test.rs b/crates/cli/src/tests/async_context_test.rs deleted file mode 100644 index 48849bd2..00000000 --- a/crates/cli/src/tests/async_context_test.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::{ - future::Future, - pin::{pin, Pin}, - ptr, - task::{self, Context, Poll, RawWaker, RawWakerVTable, Waker}, -}; - -use tree_sitter::Parser; - -use super::helpers::fixtures::get_language; - -#[test] -fn test_node_in_fut() { - let (ret, pended) = tokio_like_spawn(async { - let mut parser = Parser::new(); - let language = get_language("bash"); - parser.set_language(&language).unwrap(); - - let tree = parser.parse("#", None).unwrap(); - - let root = tree.root_node(); - let root_ref = &root; - - let fut_val_fn = || async { - yield_now().await; - root.child(0).unwrap().kind() - }; - - yield_now().await; - - let fut_ref_fn = || async { - yield_now().await; - root_ref.child(0).unwrap().kind() - }; - - let f1 = fut_val_fn().await; - let f2 = fut_ref_fn().await; - assert_eq!(f1, f2); - - let fut_val = async { - yield_now().await; - root.child(0).unwrap().kind() - }; - - let fut_ref = async { - yield_now().await; - root_ref.child(0).unwrap().kind() - }; - - let f1 = fut_val.await; - let f2 = fut_ref.await; - assert_eq!(f1, f2); - - f1 - }) - .join(); - assert_eq!(ret, "comment"); - assert_eq!(pended, 5); -} - -#[test] -fn test_node_and_cursor_ref_in_fut() { - let ((), pended) = tokio_like_spawn(async { - let mut parser = Parser::new(); - let language = get_language("c"); - parser.set_language(&language).unwrap(); - - let tree = parser.parse("#", None).unwrap(); - - let root = tree.root_node(); - let root_ref = &root; - - let mut cursor = tree.walk(); - let cursor_ref = &mut cursor; - - cursor_ref.goto_first_child(); - - let fut_val = async { - yield_now().await; - let _ = root.to_sexp(); - }; - - yield_now().await; - - let fut_ref = async { - yield_now().await; - let _ = root_ref.to_sexp(); - cursor_ref.goto_first_child(); - }; - - fut_val.await; - fut_ref.await; - - cursor_ref.goto_first_child(); - }) - .join(); - assert_eq!(pended, 3); -} - -#[test] -fn test_node_and_cursor_ref_in_fut_with_fut_fabrics() { - let ((), pended) = tokio_like_spawn(async { - let mut parser = Parser::new(); - let language = get_language("javascript"); - parser.set_language(&language).unwrap(); - - let tree = parser.parse("#", None).unwrap(); - - let root = tree.root_node(); - let root_ref = &root; - - let mut cursor = tree.walk(); - let cursor_ref = &mut cursor; - - cursor_ref.goto_first_child(); - - let fut_val = || async { - yield_now().await; - let _ = root.to_sexp(); - }; - - yield_now().await; - - let fut_ref = || async move { - yield_now().await; - let _ = root_ref.to_sexp(); - cursor_ref.goto_first_child(); - }; - - fut_val().await; - fut_val().await; - fut_ref().await; - }) - .join(); - assert_eq!(pended, 4); -} - -#[test] -fn test_node_and_cursor_ref_in_fut_with_inner_spawns() { - let (ret, pended) = tokio_like_spawn(async { - let mut parser = Parser::new(); - let language = get_language("rust"); - parser.set_language(&language).unwrap(); - - let tree = parser.parse("#", None).unwrap(); - - let mut cursor = tree.walk(); - let cursor_ref = &mut cursor; - - cursor_ref.goto_first_child(); - - let fut_val = || { - let tree = tree.clone(); - async move { - let root = tree.root_node(); - let mut cursor = tree.walk(); - let cursor_ref = &mut cursor; - yield_now().await; - let _ = root.to_sexp(); - cursor_ref.goto_first_child(); - } - }; - - yield_now().await; - - let fut_ref = || { - let tree = tree.clone(); - async move { - let root = tree.root_node(); - let root_ref = &root; - let mut cursor = tree.walk(); - let cursor_ref = &mut cursor; - yield_now().await; - let _ = root_ref.to_sexp(); - cursor_ref.goto_first_child(); - } - }; - - let ((), p1) = tokio_like_spawn(fut_val()).await.unwrap(); - let ((), p2) = tokio_like_spawn(fut_ref()).await.unwrap(); - - cursor_ref.goto_first_child(); - - fut_val().await; - fut_val().await; - fut_ref().await; - - cursor_ref.goto_first_child(); - - p1 + p2 - }) - .join(); - assert_eq!(pended, 4); - assert_eq!(ret, 2); -} - -fn tokio_like_spawn(future: T) -> JoinHandle<(T::Output, usize)> -where - T: Future + Send + 'static, - T::Output: Send + 'static, -{ - // No runtime, just noop waker - - let waker = noop_waker(); - let mut cx = task::Context::from_waker(&waker); - - let mut pending = 0; - let mut future = pin!(future); - let ret = loop { - match future.as_mut().poll(&mut cx) { - Poll::Pending => pending += 1, - Poll::Ready(r) => { - break r; - } - } - }; - JoinHandle::new((ret, pending)) -} - -async fn yield_now() { - struct SimpleYieldNow { - yielded: bool, - } - - impl Future for SimpleYieldNow { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { - cx.waker().wake_by_ref(); - if self.yielded { - return Poll::Ready(()); - } - self.yielded = true; - Poll::Pending - } - } - - SimpleYieldNow { yielded: false }.await; -} - -pub const fn noop_waker() -> Waker { - const VTABLE: RawWakerVTable = RawWakerVTable::new( - // Cloning just returns a new no-op raw waker - |_| RAW, - // `wake` does nothing - |_| {}, - // `wake_by_ref` does nothing - |_| {}, - // Dropping does nothing as we don't allocate anything - |_| {}, - ); - const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE); - unsafe { Waker::from_raw(RAW) } -} - -struct JoinHandle { - data: Option, -} - -impl JoinHandle { - #[must_use] - const fn new(data: T) -> Self { - Self { data: Some(data) } - } - - const fn join(&mut self) -> T { - self.data.take().unwrap() - } -} - -impl Future for JoinHandle { - type Output = std::result::Result; - - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - let data = self.get_mut().data.take().unwrap(); - Poll::Ready(Ok(data)) - } -}