Skip to content

Commit f21deac

Browse files
author
Andrew J Westlake
committed
Added ability to split test_main! macro into components allowing tests to be compile checked in both libtests and doctests
1 parent 21eb9c8 commit f21deac

File tree

3 files changed

+187
-14
lines changed

3 files changed

+187
-14
lines changed

pyo3-asyncio-macros/src/lib.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,7 @@ impl Parse for TestMainArgs {
249249

250250
#[cfg(not(test))]
251251
#[proc_macro]
252-
pub fn test_main(args: TokenStream) -> TokenStream {
253-
let TestMainArgs { attrs, suite_name } = syn::parse_macro_input!(args as TestMainArgs);
254-
252+
pub fn test_structs(_args: TokenStream) -> TokenStream {
255253
let result = quote! {
256254
#[derive(Clone)]
257255
pub(crate) struct Test {
@@ -270,16 +268,37 @@ pub fn test_main(args: TokenStream) -> TokenStream {
270268
}
271269

272270
inventory::collect!(Test);
271+
};
272+
result.into()
273+
}
273274

274-
#(#attrs)*
275-
async fn main() -> pyo3::PyResult<()> {
276-
let args = pyo3_asyncio::testing::parse_args(#suite_name);
275+
#[cfg(not(test))]
276+
#[proc_macro]
277+
pub fn test_main_body(args: TokenStream) -> TokenStream {
278+
let suite_name = syn::parse_macro_input!(args as syn::LitStr);
279+
280+
let result = quote! {
281+
let args = pyo3_asyncio::testing::parse_args(#suite_name);
277282

278-
pyo3_asyncio::testing::test_harness(
279-
inventory::iter::<Test>().map(|test| test.clone()).collect(), args
280-
)
281-
.await?;
283+
pyo3_asyncio::testing::test_harness(
284+
inventory::iter::<Test>().map(|test| test.clone()).collect(), args
285+
)
286+
.await?;
287+
};
288+
result.into()
289+
}
290+
291+
#[cfg(not(test))]
292+
#[proc_macro]
293+
pub fn test_main(args: TokenStream) -> TokenStream {
294+
let TestMainArgs { attrs, suite_name } = syn::parse_macro_input!(args as TestMainArgs);
295+
296+
let result = quote! {
297+
pyo3_asyncio::testing::test_structs!();
282298

299+
#(#attrs)*
300+
async fn main() -> pyo3::PyResult<()> {
301+
pyo3_asyncio::testing::test_main_body!(#suite_name);
283302
Ok(())
284303
}
285304
};

src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,3 +395,13 @@ fn dump_err(py: Python<'_>) -> impl FnOnce(PyErr) + '_ {
395395
e.print_and_set_sys_last_vars(py);
396396
}
397397
}
398+
399+
/// Alias crate as pyo3_asyncio for test_structs! macro expansion
400+
#[cfg(test)]
401+
#[cfg(feature = "testing")]
402+
use crate as pyo3_asyncio;
403+
404+
// Expand test structs in crate root to allow lib tests to be compile-checked (but not ran)
405+
#[cfg(test)]
406+
#[cfg(feature = "testing")]
407+
testing::test_structs!();

src/testing.rs

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
//! ### Adding Tests to the PyO3 Asyncio Test Harness
5858
//!
5959
//! For `async-std` use the [`pyo3_asyncio::async_std::test`](crate::async_std::test) attribute:
60-
//! ```ignore
60+
//! ```no_run
6161
//! use std::{time::Duration, thread};
6262
//!
6363
//! use pyo3::prelude::*;
@@ -74,11 +74,23 @@
7474
//! Ok(())
7575
//! }
7676
//!
77-
//! pyo3_asyncio::testing::test_main!(#[pyo3_asyncio::async_std::main], "Example Test Suite");
77+
//! // ...
78+
//! #
79+
//! # // Doctests don't detect main fn when using the test_main!() macro, so we expand it into the
80+
//! # // components of that macro instead.
81+
//! #
82+
//! # pyo3_asyncio::testing::test_structs!();
83+
//! #
84+
//! # #[pyo3_asyncio::async_std::main]
85+
//! # async fn main() -> PyResult<()> {
86+
//! # pyo3_asyncio::testing::test_main_body!("Example Test Suite");
87+
//! #
88+
//! # Ok(())
89+
//! # }
7890
//! ```
7991
//!
8092
//! For `tokio` use the [`pyo3_asyncio::tokio::test`](crate::tokio::test) attribute:
81-
//! ```ignore
93+
//! ```no_run
8294
//! use std::{time::Duration, thread};
8395
//!
8496
//! use pyo3::prelude::*;
@@ -95,7 +107,102 @@
95107
//! Ok(())
96108
//! }
97109
//!
98-
//! pyo3_asyncio::testing::test_main!(#[pyo3_asyncio::tokio::main], "Example Test Suite");
110+
//! // ...
111+
//! #
112+
//! # // Doctests don't detect main fn when using the test_main!() macro, so we expand it into the
113+
//! # // components of that macro instead.
114+
//! #
115+
//! # pyo3_asyncio::testing::test_structs!();
116+
//! #
117+
//! # #[pyo3_asyncio::tokio::main]
118+
//! # async fn main() -> PyResult<()> {
119+
//! # pyo3_asyncio::testing::test_main_body!("Example Test Suite");
120+
//! #
121+
//! # Ok(())
122+
//! # }
123+
//! ```
124+
//!
125+
//! ### Caveats
126+
//!
127+
//! The `test_main!()` macro _must_ be placed in the crate root. The `inventory` crate places
128+
//! restrictions on the structures used by the `#[test]` attributes that force us to create a custom
129+
//! `Test` structure in the crate root. If `test_main!()` is not expanded in the crate root, then
130+
//! the resolution of `crate::Test` will fail and the test will not compile.
131+
//!
132+
//! #### Lib Tests and Doc Tests
133+
//!
134+
//! Unfortunately, these utilities will only run in integration tests. Running lib tests are out of
135+
//! the question since we need control over the main function.
136+
//!
137+
//! You can however perform compilation checks for lib tests and doc tests. This is more handy for
138+
//! doc tests than it is for lib tests, but it's there if you want it.
139+
//!
140+
//! #### Expanding `test_main!()` for Doc Tests
141+
//!
142+
//! For some reason, doc tests don't interpret the `test_main!()` macro as providing `fn main()` and
143+
//! will wrap the test body in another `fn main()`. For technical reasons listed in the Caveats
144+
//! section above, this is problematic because the `Test` structure will not be inside the crate
145+
//! root anymore.
146+
//!
147+
//! To get around this, we can instead expand `test_main!()` into its components for the doc test:
148+
//! * [`test_structs!()`](crate::testing::test_structs)
149+
//! * [`test_main_body!(suite_name: &'static str)`](crate::testing::test_main_body)
150+
//!
151+
//! The following `test_main!()` macro:
152+
//!
153+
//! ```no_run
154+
//! pyo3_asyncio::testing::test_main!(#[pyo3_asyncio::async_std::main], "Example Test Suite");
155+
//! ```
156+
//!
157+
//! Is equivalent to this expansion:
158+
//!
159+
//! ```no_run
160+
//! use pyo3::prelude::*;
161+
//!
162+
//! pyo3_asyncio::testing::test_structs!();
163+
//!
164+
//! #[pyo3_asyncio::async_std::main]
165+
//! async fn main() -> PyResult<()> {
166+
//! pyo3_asyncio::testing::test_main_body!("Example Test Suite");
167+
//! Ok(())
168+
//! }
169+
//! ```
170+
//!
171+
//! #### Allowing Compilation Checks in Lib Tests
172+
//!
173+
//! In order to allow the `#[test]` attributes to expand, we need to expand the `test_structs!()`
174+
//! macro in the crate root. After that, `pyo3-asyncio` tests can be defined anywhere. Again, these
175+
//! will not run, but they will be compile-checked during testing.
176+
//!
177+
//! `my-crate/src/lib.rs`
178+
//! ```no_run
179+
//! #[cfg(test)]
180+
//! pyo3_asyncio::testing::test_structs!();
181+
//!
182+
//! #[cfg(test)]
183+
//! mod tests {
184+
//! use pyo3::prelude::*;
185+
//!
186+
//! use crate as pyo3_asyncio;
187+
//!
188+
//! #[pyo3_asyncio::async_std::test]
189+
//! async fn test_async_std_async_test_compiles() -> PyResult<()> {
190+
//! Ok(())
191+
//! }
192+
//! #[pyo3_asyncio::async_std::test]
193+
//! fn test_async_std_sync_test_compiles() -> PyResult<()> {
194+
//! Ok(())
195+
//! }
196+
//!
197+
//! #[pyo3_asyncio::tokio::test]
198+
//! async fn test_tokio_async_test_compiles() -> PyResult<()> {
199+
//! Ok(())
200+
//! }
201+
//! #[pyo3_asyncio::tokio::test]
202+
//! fn test_tokio_sync_test_compiles() -> PyResult<()> {
203+
//! Ok(())
204+
//! }
205+
//! }
99206
//! ```
100207
101208
use std::{future::Future, pin::Pin};
@@ -108,6 +215,14 @@ use pyo3::prelude::*;
108215
#[cfg(feature = "attributes")]
109216
pub use pyo3_asyncio_macros::test_main;
110217

218+
/// <span class="module-item stab portability" style="display: inline; border-radius: 3px; padding: 2px; font-size: 80%; line-height: 1.2;"><code>attributes</code></span>
219+
#[cfg(feature = "attributes")]
220+
pub use pyo3_asyncio_macros::test_structs;
221+
222+
/// <span class="module-item stab portability" style="display: inline; border-radius: 3px; padding: 2px; font-size: 80%; line-height: 1.2;"><code>attributes</code></span>
223+
#[cfg(feature = "attributes")]
224+
pub use pyo3_asyncio_macros::test_main_body;
225+
111226
/// Args that should be provided to the test program
112227
///
113228
/// These args are meant to mirror the default test harness's args.
@@ -202,3 +317,32 @@ pub async fn test_harness(tests: Vec<impl Test + 'static>, args: Args) -> PyResu
202317

203318
Ok(())
204319
}
320+
321+
#[cfg(test)]
322+
mod tests {
323+
use pyo3::prelude::*;
324+
325+
use crate as pyo3_asyncio;
326+
327+
#[cfg(feature = "async-std-runtime")]
328+
#[pyo3_asyncio::async_std::test]
329+
async fn test_async_std_async_test_compiles() -> PyResult<()> {
330+
Ok(())
331+
}
332+
#[cfg(feature = "async-std-runtime")]
333+
#[pyo3_asyncio::async_std::test]
334+
fn test_async_std_sync_test_compiles() -> PyResult<()> {
335+
Ok(())
336+
}
337+
338+
#[cfg(feature = "tokio-runtime")]
339+
#[pyo3_asyncio::tokio::test]
340+
async fn test_tokio_async_test_compiles() -> PyResult<()> {
341+
Ok(())
342+
}
343+
#[cfg(feature = "tokio-runtime")]
344+
#[pyo3_asyncio::tokio::test]
345+
fn test_tokio_sync_test_compiles() -> PyResult<()> {
346+
Ok(())
347+
}
348+
}

0 commit comments

Comments
 (0)