diff --git a/Cargo.toml b/Cargo.toml index 06eccad..26099a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,9 +19,13 @@ fork-like interface. [badges] travis-ci = { repository = "AltSysrq/rusty-fork" } +[workspace] +members = ["rusty-fork-macro"] + [dependencies] fnv = "1.0" quick-error = "1.2" +rusty-fork-macro = { path = "rusty-fork-macro", optional = true } tempfile = "3.0" wait-timeout = { version = "0.2", optional = true } @@ -29,4 +33,5 @@ wait-timeout = { version = "0.2", optional = true } [features] default = [ "timeout" ] +macro = [ "rusty-fork-macro" ] timeout = [ "wait-timeout" ] diff --git a/README.md b/README.md index fc06ee1..dc7a935 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ the test and forcibly terminate it and produce a normal test failure. as the current working directory, can do so without interfering with other tests. -This crate itself provides two things: +This crate itself provides three things: - The [`rusty_fork_test!`](macro.rusty_fork_test.html) macro, which is a simple way to wrap standard Rust tests to be run in subprocesses with @@ -29,6 +29,10 @@ optional timeouts. - The [`fork`](fn.fork.html) function which can be used as a building block to make other types of process isolation strategies. +- The `#[fork_test]` proc macro, which works exactly like `rusty_fork_test!`, + but with the additionaly feature of supporting `async` functions. It is gated + behind the `macro` crate feature flag. + ## Quick Start If you just want to run normal Rust tests in isolated processes, getting diff --git a/rusty-fork-macro/Cargo.toml b/rusty-fork-macro/Cargo.toml new file mode 100644 index 0000000..56f3d12 --- /dev/null +++ b/rusty-fork-macro/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rusty-fork-macro" +version = "0.1.0" +authors = ["Jason Lingle"] +license = "MIT OR Apache-2.0" +readme = "../README.md" +repository = "https://github.com/altsysrq/rusty-fork" +documentation = "https://docs.rs/rusty-fork-macro" +keywords = ["testing", "process", "fork"] +categories = ["development-tools::testing"] +edition = "2018" + +description = """ +Proc-macro library for rusty-fork. +""" + +[lib] +proc-macro = true + +[dependencies] +quote = "1" +syn = { version = "1", features = ["full"] } + +[dev-dependencies] +rusty-fork = { path = "../", features = ["macro", "timeout"] } +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/rusty-fork-macro/src/lib.rs b/rusty-fork-macro/src/lib.rs new file mode 100644 index 0000000..3f1533b --- /dev/null +++ b/rusty-fork-macro/src/lib.rs @@ -0,0 +1,230 @@ +//- +// Copyright 2018 +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![deny(missing_docs, unsafe_code)] + +//! Proc-macro crate of `rusty-fork`. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::{AttributeArgs, Error, ItemFn, Lit, Meta, NestedMeta}; + +/// Run Rust tests in subprocesses. +/// +/// The basic usage is to simply put this macro around your test functions. +/// +/// ``` +/// # /* +/// #[cfg(test)] +/// # */ +/// mod test { +/// use rusty_fork::fork_test; +/// +/// # /* +/// #[fork_test] +/// # */ +/// # pub +/// fn my_test() { +/// assert_eq!(2, 1 + 1); +/// } +/// +/// // more tests... +/// } +/// # +/// # fn main() { test::my_test(); } +/// ``` +/// +/// Each test will be run in its own process. If the subprocess exits +/// unsuccessfully for any reason, including due to signals, the test fails. +/// +/// It is also possible to specify a timeout which is applied to all tests in +/// the block, like so: +/// +/// ``` +/// use rusty_fork::fork_test; +/// +/// # /* +/// #[fork_test(timeout_ms = 1000)] +/// # */ +/// fn my_test() { +/// do_some_expensive_computation(); +/// } +/// # fn do_some_expensive_computation() { } +/// # fn main() { my_test(); } +/// ``` +/// +/// If any individual test takes more than the given timeout, the child is +/// terminated and the test panics. +/// +/// Using the timeout feature requires the `timeout` feature for this crate to +/// be enabled (which it is by default). +/// +/// ``` +/// use rusty_fork::fork_test; +/// +/// # /* +/// #[fork_test(crate = rusty_fork)] +/// # */ +/// fn my_test() { +/// assert_eq!(2, 1 + 1); +/// } +/// # fn main() { my_test(); } +/// ``` +/// +/// Sometimes the crate dependency might be renamed, in cases like this use the `crate` attribute +/// to pass the new name to rusty-fork. +#[proc_macro_attribute] +pub fn fork_test(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as AttributeArgs); + + // defaults + let mut crate_name = quote::quote! { rusty_fork }; + let mut timeout = quote::quote! {}; + + // may be changed by the user + for arg in args { + if let NestedMeta::Meta(Meta::NameValue(name_value)) = arg { + if let Some(ident) = name_value.path.get_ident() { + match ident.to_string().as_str() { + "timeout_ms" => { + if let Lit::Int(int) = name_value.lit { + timeout = quote::quote! { #![rusty_fork(timeout_ms = #int)] } + } + } + "crate" => { + if let Lit::Str(str) = name_value.lit { + crate_name = str.to_token_stream(); + } + } + // we don't support using invalid attributes + attribute => { + return Error::new( + ident.span(), + format!( + "`{}` is not a valid attribute for `#[fork_test]`", + attribute + ), + ) + .to_compile_error() + .into() + } + } + } + } + } + + let item = syn::parse_macro_input!(item as ItemFn); + + let fn_attrs = item.attrs; + let fn_vis = item.vis; + let fn_sig = item.sig; + let fn_body = item.block; + + // the default is that we add the `#[test]` for the use + let mut test = quote::quote! { #[test] }; + + // we should still support a use case where the user adds it himself + for attr in &fn_attrs { + if let Some(ident) = attr.path.get_ident() { + if ident == "test" { + test = quote::quote! {}; + } + } + } + + // we don't support async functions, whatever library the user uses to support this, should + // process first + if let Some(asyncness) = fn_sig.asyncness { + return Error::new( + asyncness.span, + "put `#[fork_test]` after the macro that enables `async` support", + ) + .to_compile_error() + .into(); + } + + (quote::quote! { + ::#crate_name::rusty_fork_test! { + #timeout + + #test + #(#fn_attrs)* + #fn_vis #fn_sig #fn_body + } + }) + .into() +} + +#[cfg(test)] +mod test { + use rusty_fork::fork_test; + use std::io::Result; + + #[fork_test] + fn trivials() {} + + #[fork_test] + #[should_panic] + fn panicking_child() { + panic!("just testing a panic, nothing to see here"); + } + + #[fork_test] + #[should_panic] + fn aborting_child() { + ::std::process::abort(); + } + + #[fork_test] + fn trivial_result() -> Result<()> { + Ok(()) + } + + #[fork_test] + #[should_panic] + fn panicking_child_result() -> Result<()> { + panic!("just testing a panic, nothing to see here"); + } + + #[fork_test] + #[should_panic] + fn aborting_child_result() -> Result<()> { + ::std::process::abort(); + } + + #[fork_test(timeout_ms = 1000)] + fn timeout_passes() {} + + #[fork_test(timeout_ms = 1000)] + #[should_panic] + fn timeout_fails() { + println!("hello from child"); + ::std::thread::sleep(::std::time::Duration::from_millis(10000)); + println!("goodbye from child"); + } + + #[tokio::test] + #[fork_test] + async fn async_test() { + tokio::task::spawn(async { + println!("hello from child"); + }) + .await + .unwrap(); + } + + #[tokio::test] + #[fork_test] + async fn async_return_test() -> std::result::Result<(), tokio::task::JoinError> { + tokio::task::spawn(async { + println!("hello from child"); + }) + .await + } +} diff --git a/src/fork_test.rs b/src/fork_test.rs index a5531d1..5a5d0f9 100644 --- a/src/fork_test.rs +++ b/src/fork_test.rs @@ -72,13 +72,52 @@ macro_rules! rusty_fork_test { (#![rusty_fork(timeout_ms = $timeout:expr)] $( $(#[$meta:meta])* - fn $test_name:ident() $body:block + fn $test_name:ident()$( -> $ret:ty)? $body:block )*) => { $( + $crate::rusty_fork_test_ret!($timeout, [$($meta)*], $test_name, $body$(, $ret)?); + )* }; + + ($( + $(#[$meta:meta])* + fn $test_name:ident()$( -> $ret:ty)? $body:block + )*) => { + $crate::rusty_fork_test! { + #![rusty_fork(timeout_ms = 0)] + + $($(#[$meta])* fn $test_name()$( -> $ret)? $body)* + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! rusty_fork_test_ret { + ($timeout:expr, [$($meta:meta)*], $test_name:ident, $body:block) => { + $crate::rusty_fork_test_inner!($timeout, [$($meta)*], $test_name, fn body_fn() $body); + }; + + ($timeout:expr, [$($meta:meta)*], $test_name:ident, $body:block, $ret:ty) => { + $crate::rusty_fork_test_inner!( + $timeout, + [$($meta)*], + $test_name, + fn body_fn() { + fn body_fn() -> $ret $body + body_fn().unwrap(); + } + ); + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! rusty_fork_test_inner { + ($timeout:expr, [$($meta:meta)*], $test_name:ident, $body_fn:stmt) => { $(#[$meta])* fn $test_name() { // Eagerly convert everything to function pointers so that all // tests use the same instantiation of `fork`. - fn body_fn() $body + $body_fn let body: fn () = body_fn; fn supervise_fn(child: &mut $crate::ChildWrapper, @@ -95,17 +134,6 @@ macro_rules! rusty_fork_test { $crate::fork_test::no_configure_child, supervise, body).expect("forking test failed") } - )* }; - - ($( - $(#[$meta:meta])* - fn $test_name:ident() $body:block - )*) => { - rusty_fork_test! { - #![rusty_fork(timeout_ms = 0)] - - $($(#[$meta])* fn $test_name() $body)* - } }; } @@ -170,6 +198,8 @@ fn wait_timeout(_: &mut ChildWrapper, _: u64) { #[cfg(test)] mod test { + use std::io::Result; + rusty_fork_test! { #[test] fn trivial() { } @@ -185,6 +215,23 @@ mod test { fn aborting_child() { ::std::process::abort(); } + + #[test] + fn trivial_result() -> Result<()> { + Ok(()) + } + + #[test] + #[should_panic] + fn panicking_child_result() -> Result<()> { + panic!("just testing a panic, nothing to see here"); + } + + #[test] + #[should_panic] + fn aborting_child_result() -> Result<()> { + ::std::process::abort(); + } } rusty_fork_test! { diff --git a/src/lib.rs b/src/lib.rs index d1fe900..f0abc0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ //! as the current working directory, can do so without interfering with other //! tests. //! -//! This crate itself provides two things: +//! This crate itself provides three things: //! //! - The [`rusty_fork_test!`](macro.rusty_fork_test.html) macro, which is a //! simple way to wrap standard Rust tests to be run in subprocesses with @@ -35,6 +35,10 @@ //! - The [`fork`](fn.fork.html) function which can be used as a building block //! to make other types of process isolation strategies. //! +//! - The [`#[fork_test]`](crate::fork_test) proc macro, which works exactly like +//! `rusty_fork_test!`, but with the additionaly feature of supporting `async` functions. It is +//1 gated behind the `macro` crate feature flag. +//! //! ## Quick Start //! //! If you just want to run normal Rust tests in isolated processes, getting @@ -126,3 +130,5 @@ pub use crate::sugar::RustyForkId; pub use crate::error::{Error, Result}; pub use crate::fork::fork; pub use crate::child_wrapper::{ChildWrapper, ExitStatusWrapper}; +#[cfg(feature = "macro")] +pub use rusty_fork_macro::fork_test;