diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0219a83..f64d785 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,9 @@ jobs: - beta - nightly - 1.39.0 + features: + - default + - file_locks steps: - uses: actions/checkout@v2.3.4 - uses: actions-rs/toolchain@v1.0.7 @@ -33,17 +36,42 @@ jobs: with: command: fmt args: -- --check + if: ${{ matrix.features == 'default' }} - name: Clippy uses: actions-rs/cargo@v1.0.3 env: RUSTFLAGS: -Dwarnings with: command: clippy + if: ${{ matrix.features == 'default' }} continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' }} - name: Build and test uses: actions-rs/cargo@v1.0.3 with: command: test + args: --features ${{ matrix.features }} + + multi-os-testing: + name: Test suite + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions-rs/toolchain@v1.0.7 + with: + profile: minimal + toolchain: stable + override: true + - uses: Swatinem/rust-cache@v1 + - name: Build and test + uses: actions-rs/cargo@v1.0.3 + with: + command: test + args: --all-features minimal-versions: name: minimal versions check diff --git a/Cargo.lock b/Cargo.lock index cc98562..4a40d90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fslock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31bbcaaf785e09a604b93b15b9ccb41cbf4716fa04c788748d157cb3134d717b" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -499,6 +509,7 @@ dependencies = [ name = "serial_test" version = "0.5.1" dependencies = [ + "fslock", "lazy_static", "parking_lot", "serial_test_derive", diff --git a/serial_test/Cargo.toml b/serial_test/Cargo.toml index 346f47e..e217772 100644 --- a/serial_test/Cargo.toml +++ b/serial_test/Cargo.toml @@ -14,3 +14,11 @@ keywords = ["sequential"] lazy_static = "1.2" parking_lot = ">= 0.10, < 0.12" serial_test_derive = { version = "~0.5.1", path = "../serial_test_derive" } +fslock = {version = "0.2", optional = true} + +[features] +default = [] +file_locks = ["fslock"] + +[package.metadata.docs.rs] +all-features = true \ No newline at end of file diff --git a/serial_test/src/code_lock.rs b/serial_test/src/code_lock.rs new file mode 100644 index 0000000..a09b2f8 --- /dev/null +++ b/serial_test/src/code_lock.rs @@ -0,0 +1,72 @@ +use lazy_static::lazy_static; +use parking_lot::ReentrantMutex; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; + +lazy_static! { + static ref LOCKS: Arc>>> = + Arc::new(RwLock::new(HashMap::new())); +} + +fn check_new_key(name: &str) { + // Check if a new key is needed. Just need a read lock, which can be done in sync with everyone else + let new_key = { + let unlock = LOCKS.read().unwrap(); + !unlock.deref().contains_key(name) + }; + if new_key { + // This is the rare path, which avoids the multi-writer situation mostly + LOCKS + .write() + .unwrap() + .deref_mut() + .insert(name.to_string(), ReentrantMutex::new(())); + } +} + +#[doc(hidden)] +pub fn local_serial_core_with_return( + name: &str, + function: fn() -> Result<(), E>, +) -> Result<(), E> { + check_new_key(name); + + let unlock = LOCKS.read().unwrap(); + // _guard needs to be named to avoid being instant dropped + let _guard = unlock.deref()[name].lock(); + function() +} + +#[doc(hidden)] +pub fn local_serial_core(name: &str, function: fn()) { + check_new_key(name); + + let unlock = LOCKS.read().unwrap(); + // _guard needs to be named to avoid being instant dropped + let _guard = unlock.deref()[name].lock(); + function(); +} + +#[doc(hidden)] +pub async fn local_async_serial_core_with_return( + name: &str, + fut: impl std::future::Future>, +) -> Result<(), E> { + check_new_key(name); + + let unlock = LOCKS.read().unwrap(); + // _guard needs to be named to avoid being instant dropped + let _guard = unlock.deref()[name].lock(); + fut.await +} + +#[doc(hidden)] +pub async fn local_async_serial_core(name: &str, fut: impl std::future::Future) { + check_new_key(name); + + let unlock = LOCKS.read().unwrap(); + // _guard needs to be named to avoid being instant dropped + let _guard = unlock.deref()[name].lock(); + fut.await; +} diff --git a/serial_test/src/file_lock.rs b/serial_test/src/file_lock.rs new file mode 100644 index 0000000..20cd99e --- /dev/null +++ b/serial_test/src/file_lock.rs @@ -0,0 +1,86 @@ +use fslock::LockFile; +use std::{env, fs, path::Path}; + +struct Lock { + lockfile: LockFile, +} + +impl Lock { + fn unlock(self: &mut Lock) { + self.lockfile.unlock().unwrap(); + println!("Unlock"); + } +} + +fn do_lock(path: &str) -> Lock { + if !Path::new(path).exists() { + fs::write(path, "").unwrap_or_else(|_| panic!("Lock file path was {:?}", path)) + } + let mut lockfile = LockFile::open(path).unwrap(); + println!("Waiting on {:?}", path); + lockfile.lock().unwrap(); + println!("Locked for {:?}", path); + Lock { lockfile } +} + +fn path_for_name(name: &str) -> String { + let mut pathbuf = env::temp_dir(); + pathbuf.push(format!("serial-test-{}", name)); + pathbuf.into_os_string().into_string().unwrap() +} + +fn make_lock_for_name_and_path(name: &str, path: Option<&str>) -> Lock { + if let Some(opt_path) = path { + do_lock(opt_path) + } else { + let default_path = path_for_name(name); + do_lock(&default_path) + } +} + +#[doc(hidden)] +pub fn fs_serial_core(name: &str, path: Option<&str>, function: fn()) { + let mut lock = make_lock_for_name_and_path(name, path); + function(); + lock.unlock(); +} + +#[doc(hidden)] +pub fn fs_serial_core_with_return( + name: &str, + path: Option<&str>, + function: fn() -> Result<(), E>, +) -> Result<(), E> { + let mut lock = make_lock_for_name_and_path(name, path); + let ret = function(); + lock.unlock(); + ret +} + +#[doc(hidden)] +pub async fn fs_async_serial_core_with_return( + name: &str, + path: Option<&str>, + fut: impl std::future::Future>, +) -> Result<(), E> { + let mut lock = make_lock_for_name_and_path(name, path); + let ret = fut.await; + lock.unlock(); + ret +} + +#[doc(hidden)] +pub async fn fs_async_serial_core( + name: &str, + path: Option<&str>, + fut: impl std::future::Future, +) { + let mut lock = make_lock_for_name_and_path(name, path); + fut.await; + lock.unlock(); +} + +#[test] +fn test_serial() { + fs_serial_core("test", None, || {}); +} diff --git a/serial_test/src/lib.rs b/serial_test/src/lib.rs index 91adcbc..b3b6f7c 100644 --- a/serial_test/src/lib.rs +++ b/serial_test/src/lib.rs @@ -1,5 +1,7 @@ +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + //! # serial_test -//! `serial_test` allows for the creation of serialised Rust tests using the [serial](attr.serial.html) attribute +//! `serial_test` allows for the creation of serialised Rust tests using the [serial](macro@serial) attribute //! e.g. //! ```` //! #[test] @@ -14,79 +16,39 @@ //! // Do things //! } //! ```` -//! Multiple tests with the [serial](attr.serial.html) attribute are guaranteed to be executed in serial. Ordering +//! Multiple tests with the [serial](macro@serial) attribute are guaranteed to be executed in serial. Ordering //! of the tests is not guaranteed however. +//! +//! For cases like doctests and integration tests where the tests are run as separate processes, we also support +//! [file_serial](macro@file_serial), with similar properties but based off file locking. 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. +//! ```` +//! #[test] +//! #[file_serial] +//! fn test_serial_three() { +//! // Do things +//! } +//! ```` -use lazy_static::lazy_static; -use parking_lot::ReentrantMutex; -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, RwLock}; - -lazy_static! { - static ref LOCKS: Arc>>> = - Arc::new(RwLock::new(HashMap::new())); -} - -fn check_new_key(name: &str) { - // Check if a new key is needed. Just need a read lock, which can be done in sync with everyone else - let new_key = { - let unlock = LOCKS.read().unwrap(); - !unlock.deref().contains_key(name) - }; - if new_key { - // This is the rare path, which avoids the multi-writer situation mostly - LOCKS - .write() - .unwrap() - .deref_mut() - .insert(name.to_string(), ReentrantMutex::new(())); - } -} - -#[doc(hidden)] -pub fn serial_core_with_return(name: &str, function: fn() -> Result<(), E>) -> Result<(), E> { - check_new_key(name); - - let unlock = LOCKS.read().unwrap(); - // _guard needs to be named to avoid being instant dropped - let _guard = unlock.deref()[name].lock(); - function() -} - -#[doc(hidden)] -pub fn serial_core(name: &str, function: fn()) { - check_new_key(name); - - let unlock = LOCKS.read().unwrap(); - // _guard needs to be named to avoid being instant dropped - let _guard = unlock.deref()[name].lock(); - function(); -} - -#[doc(hidden)] -pub async fn async_serial_core_with_return( - name: &str, - fut: impl std::future::Future>, -) -> Result<(), E> { - check_new_key(name); - - let unlock = LOCKS.read().unwrap(); - // _guard needs to be named to avoid being instant dropped - let _guard = unlock.deref()[name].lock(); - fut.await -} +mod code_lock; +#[cfg(feature = "file_locks")] +mod file_lock; -#[doc(hidden)] -pub async fn async_serial_core(name: &str, fut: impl std::future::Future) { - check_new_key(name); +pub use code_lock::{ + local_async_serial_core, local_async_serial_core_with_return, local_serial_core, + local_serial_core_with_return, +}; - let unlock = LOCKS.read().unwrap(); - // _guard needs to be named to avoid being instant dropped - let _guard = unlock.deref()[name].lock(); - fut.await -} +#[cfg(feature = "file_locks")] +pub use file_lock::{ + fs_async_serial_core, fs_async_serial_core_with_return, fs_serial_core, + fs_serial_core_with_return, +}; -// Re-export #[serial]. +// Re-export #[serial/file_serial]. #[allow(unused_imports)] pub use serial_test_derive::serial; + +#[cfg(feature = "file_locks")] +pub use serial_test_derive::file_serial; diff --git a/serial_test/tests/tests.rs b/serial_test/tests/tests.rs index 01d9785..add8b10 100644 --- a/serial_test/tests/tests.rs +++ b/serial_test/tests/tests.rs @@ -1,8 +1,8 @@ -use serial_test::serial_core; +use serial_test::local_serial_core; #[test] fn test_empty_serial_call() { - serial_core("beta", || { + local_serial_core("beta", || { println!("Bar"); }); } diff --git a/serial_test_derive/src/lib.rs b/serial_test_derive/src/lib.rs index c6ad477..f3a2076 100644 --- a/serial_test_derive/src/lib.rs +++ b/serial_test_derive/src/lib.rs @@ -6,7 +6,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use proc_macro2::TokenTree; use proc_macro_error::{abort_call_site, proc_macro_error}; -use quote::quote; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use std::ops::Deref; /// Allows for the creation of serialised Rust tests @@ -23,9 +23,9 @@ use std::ops::Deref; /// // Do things /// } /// ```` -/// Multiple tests with the [serial](attr.serial.html) attribute are guaranteed to be executed in serial. Ordering +/// 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 want different subsets of tests to be serialised with each -/// other, but not depend on other subsets, you can add an argument to [serial](attr.serial.html), and all calls +/// 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] @@ -54,30 +54,154 @@ use std::ops::Deref; /// ```` /// `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) is supported #[proc_macro_attribute] #[proc_macro_error] pub fn serial(attr: TokenStream, input: TokenStream) -> TokenStream { - serial_core(attr.into(), input.into()).into() + local_serial_core(attr.into(), input.into()).into() } -fn serial_core( +/// 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] +#[proc_macro_error] +pub fn file_serial(attr: TokenStream, input: TokenStream) -> TokenStream { + fs_serial_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 }, + }); + } +} + +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) => { + raw_args.push(id.to_string()); + } + 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(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 +} + +fn local_serial_core( attr: proc_macro2::TokenStream, input: proc_macro2::TokenStream, ) -> proc_macro2::TokenStream { - let attrs = attr.into_iter().collect::>(); - let key = match attrs.len() { + let mut raw_args = get_raw_args(attr); + let key = match raw_args.len() { 0 => "".to_string(), + 1 => raw_args.pop().unwrap(), + n => { + panic!( + "Expected either 0 or 1 arguments, got {}: {:?}", + n, raw_args + ); + } + }; + serial_setup(input, vec![Box::new(key)], "local") +} + +fn fs_serial_core( + attr: proc_macro2::TokenStream, + input: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + let none_ident = Box::new(format_ident!("None")); + let mut args: Vec> = Vec::new(); + let mut raw_args = get_raw_args(attr); + match raw_args.len() { + 0 => { + args.push(Box::new("".to_string())); + args.push(none_ident); + } 1 => { - if let TokenTree::Ident(id) = &attrs[0] { - id.to_string() - } else { - panic!("Expected a single name as argument, got {:?}", attrs); - } + args.push(Box::new(raw_args.pop().unwrap())); + args.push(none_ident); + } + 2 => { + let key = raw_args.remove(0); + let path = raw_args.remove(0); + args.push(Box::new(key)); + args.push(Box::new(QuoteOption(Some(path)))); } n => { - panic!("Expected either 0 or 1 arguments, got {}: {:?}", n, attrs); + panic!("Expected 0-2 arguments, got {}: {:?}", n, raw_args); } - }; + } + serial_setup(input, args, "fs") +} + +fn serial_setup( + input: proc_macro2::TokenStream, + args: Vec>, + prefix: &str, +) -> proc_macro2::TokenStream +where + T: quote::ToTokens + ?Sized, +{ let ast: syn::ItemFn = syn::parse2(input).unwrap(); let asyncness = ast.sig.asyncness; let name = ast.sig.ident; @@ -110,114 +234,178 @@ fn serial_core( .collect(); if let Some(ret) = return_type { match asyncness { - Some(_) => quote! { - #(#attrs) - * - async fn #name () -> #ret { - serial_test::async_serial_core_with_return(#key, || async #block ).await; + Some(_) => { + let fnname = format_ident!("{}_async_serial_core_with_return", prefix); + quote! { + #(#attrs) + * + async fn #name () -> #ret { + serial_test::#fnname(#(#args ),*, || async #block ).await; + } } - }, - None => quote! { - #(#attrs) - * - fn #name () -> #ret { - serial_test::serial_core_with_return(#key, || #block ) + } + None => { + let fnname = format_ident!("{}_serial_core_with_return", prefix); + quote! { + #(#attrs) + * + fn #name () -> #ret { + serial_test::#fnname(#(#args ),*, || #block ) + } } - }, + } } } else { match asyncness { - Some(_) => quote! { - #(#attrs) - * - async fn #name () { - serial_test::async_serial_core(#key, || async #block ).await; + Some(_) => { + let fnname = format_ident!("{}_async_serial_core", prefix); + quote! { + #(#attrs) + * + async fn #name () { + serial_test::#fnname(#(#args ),*, || async #block ).await; + } } - }, - None => quote! { - #(#attrs) - * - fn #name () { - serial_test::serial_core(#key, || #block ); + } + None => { + let fnname = format_ident!("{}_serial_core", prefix); + quote! { + #(#attrs) + * + fn #name () { + serial_test::#fnname(#(#args ),*, || #block ); + } } - }, + } } } } -#[test] -fn test_serial() { - let attrs = proc_macro2::TokenStream::new(); - let input = quote! { - #[test] - fn foo() {} - }; - let stream = serial_core(attrs.into(), input); - let compare = quote! { - #[test] - fn foo () { - serial_test::serial_core("", || {} ); - } - }; - assert_eq!(format!("{}", compare), format!("{}", stream)); -} +#[cfg(test)] +mod tests { + use proc_macro2::{Literal, Punct, Spacing}; -#[test] -fn test_stripped_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 = serial_core(attrs.into(), input); - let compare = quote! { - #[test] - #[something_else] - fn foo () { - serial_test::serial_core("", || {} ); - } - }; - assert_eq!(format!("{}", compare), format!("{}", stream)); -} + use super::{format_ident, fs_serial_core, local_serial_core, quote, TokenTree}; + use std::iter::FromIterator; -#[test] -fn test_serial_async() { - let attrs = proc_macro2::TokenStream::new(); - let input = quote! { - async fn foo() {} - }; - let stream = serial_core(attrs.into(), input); - let compare = quote! { - async fn foo () { - serial_test::async_serial_core("", || async {} ).await; - } - }; - assert_eq!(format!("{}", compare), format!("{}", stream)); -} + #[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_async_return() { - let attrs = proc_macro2::TokenStream::new(); - let input = quote! { - async fn foo() -> Result<(), ()> { Ok(()) } - }; - let stream = serial_core(attrs.into(), input); - let compare = quote! { - async fn foo () -> Result<(), ()> { - serial_test::async_serial_core_with_return("", || async { Ok(()) } ).await; - } - }; - assert_eq!(format!("{}", compare), format!("{}", stream)); -} + #[test] + fn test_stripped_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] + #[something_else] + fn foo () { + serial_test::local_serial_core("", || {} ); + } + }; + assert_eq!(format!("{}", compare), format!("{}", stream)); + } + + #[test] + 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 () { + serial_test::local_async_serial_core("", || async {} ).await; + } + }; + assert_eq!(format!("{}", compare), format!("{}", stream)); + } + + #[test] + 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 () -> Result<(), ()> { + serial_test::local_async_serial_core_with_return("", || async { Ok(()) } ).await; + } + }; + assert_eq!(format!("{}", compare), format!("{}", stream)); + } + + // 1.54 needed for https://github.com/rust-lang/rust/commit/9daf546b77dbeab7754a80d7336cd8d00c6746e4 change in note message + #[rustversion::since(1.54)] + #[test] + fn test_serial_async_before_wrapper() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/broken/test_serial_async_before_wrapper.rs"); + } + + #[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)); + } -// 1.54 needed for https://github.com/rust-lang/rust/commit/9daf546b77dbeab7754a80d7336cd8d00c6746e4 change in note message -#[rustversion::since(1.54)] -#[test] -fn test_serial_async_before_wrapper() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/broken/test_serial_async_before_wrapper.rs"); + #[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)); + } } diff --git a/serial_test_test/Cargo.toml b/serial_test_test/Cargo.toml index 4b3081c..324a7e0 100644 --- a/serial_test_test/Cargo.toml +++ b/serial_test_test/Cargo.toml @@ -6,12 +6,18 @@ version = "0.5.1" authors = ["Tom Parker-Shemilt "] edition = "2018" -[dev-dependencies] +[dependencies] serial_test = { path="../serial_test" } lazy_static = "^1.2" env_logger = ">= 0.7, <0.9" + +[dev-dependencies] tokio = { version = "0.2", features = ["macros", "rt-threaded"] } # Can't upgrade or we break 1.39 actix-rt = { version = "1.0", default_features = false } # futures 0.3.15 breaks 1.39 -futures-util = {version = ">=0.3, <0.3.15", default_features = false } \ No newline at end of file +futures-util = {version = ">=0.3, <0.3.15", default_features = false } + +[features] +default = [] +file_locks = ["serial_test/file_locks"] \ No newline at end of file diff --git a/serial_test_test/src/lib.rs b/serial_test_test/src/lib.rs index 5e25c89..b63e9a4 100644 --- a/serial_test_test/src/lib.rs +++ b/serial_test_test/src/lib.rs @@ -1,19 +1,92 @@ +//! Not inside the cfg(test) block because of +//! ``` +//! #[macro_use] extern crate serial_test; +//! extern crate serial_test_test; +//! use serial_test_test::{fs_test_fn}; +//! #[cfg(feature = "file_locks")] +//! #[serial_test::file_serial] +//! fn main() { +//! fs_test_fn(1); +//! } +//! #[cfg(not(feature = "file_locks"))] +//! fn main() {} +//! ``` +//! ``` +//! #[macro_use] extern crate serial_test; +//! extern crate serial_test_test; +//! use serial_test_test::{fs_test_fn}; +//! #[cfg(feature = "file_locks")] +//! #[serial_test::file_serial] +//! fn main() { +//! fs_test_fn(2); +//! } +//! #[cfg(not(feature = "file_locks"))] +//! fn main() {} +//! ``` +//! ``` +//! #[macro_use] extern crate serial_test; +//! extern crate serial_test_test; +//! use serial_test_test::{fs_test_fn}; +//! #[cfg(feature = "file_locks")] +//! #[serial_test::file_serial] +//! fn main() { +//! fs_test_fn(3); +//! } +//! #[cfg(not(feature = "file_locks"))] +//! fn main() {} +//! ``` + +use lazy_static::lazy_static; +use std::convert::TryInto; +use std::env; +use std::fs; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread; +use std::time::Duration; + +lazy_static! { + static ref LOCK: Arc = Arc::new(AtomicUsize::new(0)); +} + +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +pub fn test_fn(count: usize) { + init(); + println!("Start {}", count); + LOCK.store(count, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000 * (count as u64))); + println!("End {}", count); + assert_eq!(LOCK.load(Ordering::Relaxed), count); +} + +pub fn fs_test_fn(count: usize) { + init(); + println!("Start {}", count); + let mut pathbuf = env::temp_dir(); + pathbuf.push("serial-test-test"); + fs::write(pathbuf.as_path(), count.to_ne_bytes()).unwrap(); + thread::sleep(Duration::from_millis(1000 * (count as u64))); + println!("End {}", count); + + let loaded = fs::read(pathbuf.as_path()) + .map(|bytes| usize::from_ne_bytes(bytes.as_slice().try_into().unwrap())) + .unwrap(); + assert_eq!(loaded, count); +} + #[cfg(test)] mod tests { - use lazy_static::lazy_static; + use super::{init, test_fn}; use serial_test::serial; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - use std::thread; - use std::time::Duration; - - lazy_static! { - static ref LOCK: Arc = Arc::new(AtomicUsize::new(0)); - } - fn init() { - let _ = env_logger::builder().is_test(true).try_init(); - } + #[cfg(feature = "file_locks")] + use super::fs_test_fn; + #[cfg(feature = "file_locks")] + use serial_test::file_serial; #[test] #[serial] @@ -24,34 +97,19 @@ mod tests { #[test] #[serial(alpha)] fn test_serial_1() { - init(); - println!("Start 1"); - LOCK.store(1, Ordering::Relaxed); - thread::sleep(Duration::from_millis(100)); - println!("End 1"); - assert_eq!(LOCK.load(Ordering::Relaxed), 1); + test_fn(1) } #[test] #[serial(alpha)] fn test_serial_2() { - init(); - println!("Start 2"); - LOCK.store(2, Ordering::Relaxed); - thread::sleep(Duration::from_millis(200)); - println!("End 2"); - assert_eq!(LOCK.load(Ordering::Relaxed), 2); + test_fn(2) } #[test] #[serial(alpha)] fn test_serial_3() { - init(); - println!("Start 3"); - LOCK.store(3, Ordering::Relaxed); - thread::sleep(Duration::from_millis(300)); - println!("End 3"); - assert_eq!(LOCK.load(Ordering::Relaxed), 3); + test_fn(3) } #[test] @@ -102,4 +160,36 @@ mod tests { init(); Ok(()) } + + #[cfg(feature = "file_locks")] + #[test] + #[file_serial] + fn test_file_1() { + fs_test_fn(1); + } + + #[cfg(feature = "file_locks")] + #[test] + #[file_serial] + fn test_file_2() { + fs_test_fn(2); + } + + #[cfg(feature = "file_locks")] + #[test] + #[file_serial] + fn test_file_3() { + fs_test_fn(3); + } + + #[cfg(all(feature = "file_locks", not(windows)))] + #[test] + #[file_serial(test, "/tmp/test")] + fn test_file_with_path() {} + + #[test] + #[serial(test_key)] + fn test_with_key() { + init(); + } }