serial_test_derive-2.0.0/.cargo_vcs_info.json0000644000000001600000000000100147040ustar { "git": { "sha1": "7e3a5ca6f33c5da1d49e36e436025b3d2e91e59e" }, "path_in_vcs": "serial_test_derive" }serial_test_derive-2.0.0/Cargo.toml0000644000000017550000000000100127150ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "serial_test_derive" version = "2.0.0" authors = ["Tom Parker-Shemilt "] description = "Helper crate for serial_test" readme = "README.md" categories = ["development-tools::testing"] license = "MIT" repository = "https://github.com/palfrey/serial_test/" [lib] proc-macro = true [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.syn] version = "2" features = ["full"] [dev-dependencies.env_logger] version = "0.10" [features] async = [] serial_test_derive-2.0.0/Cargo.toml.orig000064400000000000000000000007441046102023000163730ustar 00000000000000[package] name = "serial_test_derive" description = "Helper crate for serial_test" license = "MIT" version = "2.0.0" authors = ["Tom Parker-Shemilt "] edition = "2018" readme = "README.md" repository = "https://github.com/palfrey/serial_test/" categories = ["development-tools::testing"] [lib] proc-macro = true [dependencies] quote = "1.0" syn = { version="2", features=["full"] } proc-macro2 = "1.0" [dev-dependencies] env_logger = "0.10" [features] async = []serial_test_derive-2.0.0/LICENSE000064400000000000000000000020451046102023000145050ustar 00000000000000Copyright (c) 2018 Tom Parker-Shemilt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.serial_test_derive-2.0.0/README.md000064400000000000000000000042531046102023000147620ustar 00000000000000# serial_test [![Version](https://img.shields.io/crates/v/serial_test.svg)](https://crates.io/crates/serial_test) [![Downloads](https://img.shields.io/crates/d/serial_test)](https://crates.io/crates/serial_test) [![Docs](https://docs.rs/serial_test/badge.svg)](https://docs.rs/serial_test/) [![MIT license](https://img.shields.io/crates/l/serial_test.svg)](./LICENSE) [![Build Status](https://github.com/palfrey/serial_test/workflows/Continuous%20integration/badge.svg?branch=main)](https://github.com/palfrey/serial_test/actions) [![MSRV: 1.68.2](https://flat.badgen.net/badge/MSRV/1.68.2/purple)](https://blog.rust-lang.org/2023/03/28/Rust-1.68.2.html) `serial_test` allows for the creation of serialised Rust tests using the `serial` attribute e.g. ```rust #[test] #[serial] fn test_serial_one() { // Do things } #[test] #[serial] fn test_serial_another() { // Do things } #[tokio::test] #[serial] async fn test_serial_another() { // Do things asynchronously } ``` Multiple tests with the `serial` attribute are guaranteed to be executed in serial. Ordering of the tests is not guaranteed however. Other tests with the `parallel` attribute may run at the same time as each other, but not at the same time as a test with `serial`. Tests with neither attribute may run at any time and no guarantees are made about their timing! For cases like doctests and integration tests where the tests are run as separate processes, we also support `file_serial`, with similar properties but based off file locking. Note that there are no guarantees about one test with `serial` and another with `file_serial` as they lock using different methods, and `parallel` doesn't support `file_serial` yet (patches welcomed!). ## Usage The minimum supported Rust version here is 1.68.2. Note this is minimum _supported_, as it may well compile with lower versions, but they're not supported at all. Upgrades to this will require at a major version bump. 1.x supports 1.51 if you need a lower version than that. Add to your Cargo.toml ```toml [dev-dependencies] serial_test = "*" ``` plus `use serial_test::serial;` in your imports section. You can then either add `#[serial]` or `#[serial(some_text)]` to tests as required. serial_test_derive-2.0.0/src/lib.rs000064400000000000000000000407011046102023000154040ustar 00000000000000//! # serial_test_derive //! Helper crate for [serial_test](../serial_test/index.html) #![cfg_attr(docsrs, feature(doc_cfg))] #![deny(missing_docs)] extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::TokenTree; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use std::ops::Deref; /// Allows for the creation of serialised Rust tests /// ```` /// #[test] /// #[serial] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[serial] /// fn test_serial_another() { /// // Do things /// } /// ```` /// Multiple tests with the [serial](macro@serial) attribute are guaranteed to be executed in serial. Ordering /// of the tests is not guaranteed however. If you have other tests that can be run in parallel, but would clash /// if run at the same time as the [serial](macro@serial) tests, you can use the [parallel](macro@parallel) attribute. /// /// If you want different subsets of tests to be serialised with each /// other, but not depend on other subsets, you can add an argument to [serial](macro@serial), and all calls /// with identical arguments will be called in serial. e.g. /// /// ```` /// #[test] /// #[serial(something)] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[serial(something)] /// fn test_serial_another() { /// // Do things /// } /// /// #[test] /// #[serial(other)] /// fn test_serial_third() { /// // Do things /// } /// /// #[test] /// #[serial(other)] /// fn test_serial_fourth() { /// // Do things /// } /// ```` /// `test_serial_one` and `test_serial_another` will be executed in serial, as will `test_serial_third` and `test_serial_fourth` /// but neither sequence will be blocked by the other /// /// Nested serialised tests (i.e. a [serial](macro@serial) tagged test calling another) are supported #[proc_macro_attribute] pub fn serial(attr: TokenStream, input: TokenStream) -> TokenStream { local_serial_core(attr.into(), input.into()).into() } /// Allows for the creation of parallel Rust tests that won't clash with serial tests /// ```` /// #[test] /// #[serial] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[parallel] /// fn test_parallel_one() { /// // Do things /// } /// /// #[test] /// #[parallel] /// fn test_parallel_two() { /// // Do things /// } /// ```` /// Multiple tests with the [parallel](macro@parallel) attribute may run in parallel, but not at the /// same time as [serial](macro@serial) tests. e.g. in the example code above, `test_parallel_one` /// and `test_parallel_two` may run at the same time, but `test_serial_one` is guaranteed not to run /// at the same time as either of them. [parallel](macro@parallel) also takes key arguments for groups /// of tests as per [serial](macro@serial). /// /// Note that this has zero effect on [file_serial](macro@file_serial) tests, as that uses a different /// serialisation mechanism. For that, you want [file_parallel](macro@file_parallel). #[proc_macro_attribute] pub fn parallel(attr: TokenStream, input: TokenStream) -> TokenStream { local_parallel_core(attr.into(), input.into()).into() } /// Allows for the creation of file-serialised Rust tests /// ```` /// #[test] /// #[file_serial] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[file_serial] /// fn test_serial_another() { /// // Do things /// } /// ```` /// /// Multiple tests with the [file_serial](macro@file_serial) attribute are guaranteed to run in serial, as per the [serial](macro@serial) /// attribute. Note that there are no guarantees about one test with [serial](macro@serial) and another with [file_serial](macro@file_serial) /// as they lock using different methods, and [file_serial](macro@file_serial) does not support nested serialised tests, but otherwise acts /// like [serial](macro@serial). /// /// It also supports an optional `path` arg e.g /// ```` /// #[test] /// #[file_serial(key, "/tmp/foo")] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[file_serial(key, "/tmp/foo")] /// fn test_serial_another() { /// // Do things /// } /// ```` /// Note that in this case you need to specify the `name` arg as well (as per [serial](macro@serial)). The path defaults to a reasonable temp directory for the OS if not specified. #[proc_macro_attribute] #[cfg_attr(docsrs, doc(cfg(feature = "file_locks")))] pub fn file_serial(attr: TokenStream, input: TokenStream) -> TokenStream { fs_serial_core(attr.into(), input.into()).into() } /// Allows for the creation of file-serialised parallel Rust tests that won't clash with file-serialised serial tests /// ```` /// #[test] /// #[file_serial] /// fn test_serial_one() { /// // Do things /// } /// /// #[test] /// #[file_parallel] /// fn test_parallel_one() { /// // Do things /// } /// /// #[test] /// #[file_parallel] /// fn test_parallel_two() { /// // Do things /// } /// ```` /// Effectively, this should behave like [parallel](macro@parallel) but for [file_serial](macro@file_serial). /// Note that as per [file_serial](macro@file_serial) this doesn't do anything for [serial](macro@serial)/[parallel](macro@parallel) tests. /// /// It also supports an optional `path` arg e.g /// ```` /// #[test] /// #[file_parallel(key, "/tmp/foo")] /// fn test_parallel_one() { /// // Do things /// } /// /// #[test] /// #[file_parallel(key, "/tmp/foo")] /// fn test_parallel_another() { /// // Do things /// } /// ```` /// Note that in this case you need to specify the `name` arg as well (as per [parallel](macro@parallel)). The path defaults to a reasonable temp directory for the OS if not specified. #[proc_macro_attribute] #[cfg_attr(docsrs, doc(cfg(feature = "file_locks")))] pub fn file_parallel(attr: TokenStream, input: TokenStream) -> TokenStream { fs_parallel_core(attr.into(), input.into()).into() } // Based off of https://github.com/dtolnay/quote/issues/20#issuecomment-437341743 struct QuoteOption(Option); impl ToTokens for QuoteOption { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { tokens.append_all(match self.0 { Some(ref t) => quote! { ::std::option::Option::Some(#t) }, None => quote! { ::std::option::Option::None }, }); } } enum Arg { Name(String), } fn get_raw_args(attr: proc_macro2::TokenStream) -> Vec { let mut attrs = attr.into_iter().collect::>(); let mut raw_args: Vec = Vec::new(); while !attrs.is_empty() { match attrs.remove(0) { TokenTree::Ident(id) => { let name = id.to_string(); raw_args.push(Arg::Name(name)); } TokenTree::Literal(literal) => { let string_literal = literal.to_string(); if !string_literal.starts_with('\"') || !string_literal.ends_with('\"') { panic!("Expected a string literal, got '{}'", string_literal); } // Hacky way of getting a string without the enclosing quotes raw_args.push(Arg::Name( string_literal[1..string_literal.len() - 1].to_string(), )); } x => { panic!("Expected either strings or literals as args, not {}", x); } } if !attrs.is_empty() { match attrs.remove(0) { TokenTree::Punct(p) if p.as_char() == ',' => {} x => { panic!("Expected , between args, not {}", x); } } } } raw_args } #[derive(Default, Debug)] struct Config { name: String, path: Option, } fn get_core_key(attr: proc_macro2::TokenStream) -> Config { let raw_args = get_raw_args(attr); let mut c = Config::default(); let mut name_found = false; for a in raw_args { match a { Arg::Name(name) if !name_found => { c.name = name; name_found = true; } Arg::Name(name) => { c.path = Some(name); } } } c } fn local_serial_core( attr: proc_macro2::TokenStream, input: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let config = get_core_key(attr); let args: Vec> = vec![Box::new(config.name)]; serial_setup(input, args, "local") } fn local_parallel_core( attr: proc_macro2::TokenStream, input: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let config = get_core_key(attr); let args: Vec> = vec![Box::new(config.name)]; parallel_setup(input, args, "local") } fn fs_args(attr: proc_macro2::TokenStream) -> Vec> { let config = get_core_key(attr); vec![ Box::new(config.name), if let Some(path) = config.path { Box::new(QuoteOption(Some(path))) } else { Box::new(format_ident!("None")) }, ] } fn fs_serial_core( attr: proc_macro2::TokenStream, input: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let args = fs_args(attr); serial_setup(input, args, "fs") } fn fs_parallel_core( attr: proc_macro2::TokenStream, input: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { let args = fs_args(attr); parallel_setup(input, args, "fs") } fn core_setup( input: proc_macro2::TokenStream, args: Vec>, prefix: &str, kind: &str, ) -> proc_macro2::TokenStream where T: quote::ToTokens + ?Sized, { let ast: syn::ItemFn = syn::parse2(input).unwrap(); let asyncness = ast.sig.asyncness; if asyncness.is_some() && cfg!(not(feature = "async")) { panic!("async testing attempted with async feature disabled in serial_test!"); } let vis = ast.vis; let name = ast.sig.ident; let return_type = match ast.sig.output { syn::ReturnType::Default => None, syn::ReturnType::Type(_rarrow, ref box_type) => Some(box_type.deref()), }; let block = ast.block; let attrs: Vec = ast.attrs.into_iter().collect(); if let Some(ret) = return_type { match asyncness { Some(_) => { let fnname = format_ident!("{}_async_{}_core_with_return", prefix, kind); let temp_fn = format_ident!("_{}_internal", name); quote! { async fn #temp_fn () -> #ret #block #(#attrs) * #vis async fn #name () -> #ret { serial_test::#fnname(#(#args ),*, #temp_fn()).await } } } None => { let fnname = format_ident!("{}_{}_core_with_return", prefix, kind); quote! { #(#attrs) * #vis fn #name () -> #ret { serial_test::#fnname(#(#args ),*, || #block ) } } } } } else { match asyncness { Some(_) => { let fnname = format_ident!("{}_async_{}_core", prefix, kind); let temp_fn = format_ident!("_{}_internal", name); quote! { async fn #temp_fn () #block #(#attrs) * #vis async fn #name () { serial_test::#fnname(#(#args ),*, #temp_fn()).await; } } } None => { let fnname = format_ident!("{}_{}_core", prefix, kind); quote! { #(#attrs) * #vis fn #name () { serial_test::#fnname(#(#args ),*, || #block ); } } } } } } fn serial_setup( input: proc_macro2::TokenStream, args: Vec>, prefix: &str, ) -> proc_macro2::TokenStream where T: quote::ToTokens + ?Sized, { core_setup(input, args, prefix, "serial") } fn parallel_setup( input: proc_macro2::TokenStream, args: Vec>, prefix: &str, ) -> proc_macro2::TokenStream where T: quote::ToTokens + ?Sized, { core_setup(input, args, prefix, "parallel") } #[cfg(test)] mod tests { use super::{fs_serial_core, local_serial_core}; use proc_macro2::{Literal, Punct, Spacing, TokenTree}; use quote::{format_ident, quote}; use std::iter::FromIterator; #[test] fn test_serial() { let attrs = proc_macro2::TokenStream::new(); let input = quote! { #[test] fn foo() {} }; let stream = local_serial_core(attrs.into(), input); let compare = quote! { #[test] fn foo () { serial_test::local_serial_core("", || {} ); } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] fn test_serial_with_pub() { let attrs = proc_macro2::TokenStream::new(); let input = quote! { #[test] pub fn foo() {} }; let stream = local_serial_core(attrs.into(), input); let compare = quote! { #[test] pub fn foo () { serial_test::local_serial_core("", || {} ); } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] fn test_other_attributes() { let _ = env_logger::builder().is_test(true).try_init(); let attrs = proc_macro2::TokenStream::new(); let input = quote! { #[test] #[ignore] #[should_panic(expected = "Testing panic")] #[something_else] fn foo() {} }; let stream = local_serial_core(attrs.into(), input); let compare = quote! { #[test] #[ignore] #[should_panic(expected = "Testing panic")] #[something_else] fn foo () { serial_test::local_serial_core("", || {} ); } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] #[cfg(feature = "async")] fn test_serial_async() { let attrs = proc_macro2::TokenStream::new(); let input = quote! { async fn foo() {} }; let stream = local_serial_core(attrs.into(), input); let compare = quote! { async fn _foo_internal () { } async fn foo () { serial_test::local_async_serial_core("", _foo_internal() ).await; } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] #[cfg(feature = "async")] fn test_serial_async_return() { let attrs = proc_macro2::TokenStream::new(); let input = quote! { async fn foo() -> Result<(), ()> { Ok(()) } }; let stream = local_serial_core(attrs.into(), input); let compare = quote! { async fn _foo_internal () -> Result<(), ()> { Ok(()) } async fn foo () -> Result<(), ()> { serial_test::local_async_serial_core_with_return("", _foo_internal() ).await } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] fn test_file_serial() { let attrs = vec![TokenTree::Ident(format_ident!("foo"))]; let input = quote! { #[test] fn foo() {} }; let stream = fs_serial_core( proc_macro2::TokenStream::from_iter(attrs.into_iter()), input, ); let compare = quote! { #[test] fn foo () { serial_test::fs_serial_core("foo", None, || {} ); } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } #[test] fn test_file_serial_with_path() { let attrs = vec![ TokenTree::Ident(format_ident!("foo")), TokenTree::Punct(Punct::new(',', Spacing::Alone)), TokenTree::Literal(Literal::string("bar_path")), ]; let input = quote! { #[test] fn foo() {} }; let stream = fs_serial_core( proc_macro2::TokenStream::from_iter(attrs.into_iter()), input, ); let compare = quote! { #[test] fn foo () { serial_test::fs_serial_core("foo", ::std::option::Option::Some("bar_path"), || {} ); } }; assert_eq!(format!("{}", compare), format!("{}", stream)); } }