Skip to content

Commit

Permalink
Add #test_with::lock
Browse files Browse the repository at this point in the history
  • Loading branch information
yanganto committed Jul 11, 2024
1 parent 0b87e9f commit b06ad40
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 283 deletions.
473 changes: 240 additions & 233 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "test-with"
version = "0.12.6"
version = "0.13.0"
authors = ["Antonio Yang <[email protected]>"]
edition = "2021"
license = "MIT"
description = "A lib help you run test with condition"
repository = "https://github.com/yanganto/test-with"
keywords = [ "testing", "condition", "toggle", "integration", "ignore" ]
categories = [ "development-tools", "testing" ]
rust-version = "1.77.0" # Due to std::fs::create_new

[lib]
proc-macro = true
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,36 @@ mod test_with_mock {

Please check out examples uder the [example/runner](https://github.com/yanganto/test-with/tree/main/examples/runner) project.

## Lock
`#[test_with::lock(LOCK_NAME)]` is way to make sure your test casess can run one by one by using file locks.

```rust
// `LOCK` is file based lock to prevent test1 an test2 run at the same time
#[test_with::lock(LOCK)]
fn test_1() {
assert!(true);
}

// `LOCK` is file based lock to prevent test1 an test2 run at the same time
#[test_with::lock(LOCK)]
fn test_2() {
assert!(true);
}

// `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
// waiting time.
#[test_with::lock(ANOTHER_LOCK, 3)]
fn test_3() {
assert!(true);
}

// `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
// waiting time.
#[test_with::lock(ANOTHER_LOCK, 3)]
fn test_4() {
assert!(true);
}
```

## Relating issues
* [Solve this in runtime][original-issue]
Expand Down
30 changes: 30 additions & 0 deletions examples/lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
fn main() {}

#[cfg(test)]
mod tests {
// `LOCK` is file based lock to prevent test1 an test2 run at the same time
#[test_with::lock(LOCK)]
fn test_1() {
assert!(true);
}

// `LOCK` is file based lock to prevent test1 an test2 run at the same time
#[test_with::lock(LOCK)]
fn test_2() {
assert!(true);
}

// `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
// waiting time.
#[test_with::lock(ANOTHER_LOCK, 3)]
fn test_3() {
assert!(true);
}

// `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
// waiting time.
#[test_with::lock(ANOTHER_LOCK, 3)]
fn test_4() {
assert!(true);
}
}
58 changes: 12 additions & 46 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
pkgs = import nixpkgs {
inherit system overlays;
};
rust = pkgs.rust-bin.stable."1.74.1".default;
rust = pkgs.rust-bin.stable."1.77.0".default;
dr = dependency-refresh.defaultPackage.${system};

publishScript = pkgs.writeShellScriptBin "crate-publish" ''
Expand Down
49 changes: 48 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ use syn::{Item, ItemStruct, ItemType};
#[cfg(feature = "executable")]
use which::which;

use crate::utils::{fn_macro, is_module, mod_macro, sanitize_env_vars_attr};
use crate::utils::{fn_macro, is_module, lock_macro, mod_macro, sanitize_env_vars_attr};

mod utils;

Expand Down Expand Up @@ -2821,3 +2821,50 @@ mod tests {
}
}
}

/// Run test case one by one when the lock is aquired

Check warning on line 2825 in src/lib.rs

View workflow job for this annotation

GitHub Actions / lint

"aquired" should be "acquired".
/// It will automatically implement a file lock for the test case to prevent it run in the same
/// time. Also, you can pass the second parameter to specific the waiting seconds, default will be
/// 60 seconds.
/// ```
/// #[cfg(test)]
/// mod tests {
///
/// // `LOCK` is file based lock to prevent test1 an test2 run at the same time
/// #[test_with::lock(LOCK)]
/// #[test]
/// fn test_1() {
/// assert!(true);
/// }
///
/// // `LOCK` is file based lock to prevent test1 an test2 run at the same time
/// #[test_with::lock(LOCK)]
/// #[test]
/// fn test_2() {
/// assert!(true);
/// }
///
/// // `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
/// waiting time.
/// #[test_with::lock(ANOTHER_LOCK, 3)]
/// fn test_3() {
/// assert!(true);
/// }
///
/// // `ANOTHER_LOCK` is file based lock to prevent test3 an test4 run at the same time with 3 sec
/// waiting time.
/// #[test_with::lock(ANOTHER_LOCK, 3)]
/// fn test_4() {
/// assert!(true);
/// }
///
/// }
#[proc_macro_attribute]
#[proc_macro_error]
pub fn lock(attr: TokenStream, stream: TokenStream) -> TokenStream {
if is_module(&stream) {
abort_call_site!("#[test_with::lock] only works with fn")
} else {
lock_macro(attr, parse_macro_input!(stream as ItemFn))
}
}
77 changes: 76 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use quote::quote;
#[cfg(feature = "ign-msg")]
use syn::Signature;
use syn::{Attribute, Meta};
use syn::{Ident, Item, ItemFn, ItemMod};
use syn::{Block, Ident, Item, ItemFn, ItemMod};

// check for `#[test]`, `#[tokio::test]`, `#[async_std::test]`
pub(crate) fn has_test_attr(attrs: &[Attribute]) -> bool {
Expand Down Expand Up @@ -258,6 +258,81 @@ pub(crate) fn mod_macro(
}
}

pub(crate) fn lock_macro(attr: TokenStream, input: ItemFn) -> TokenStream {
let ItemFn {
attrs,
vis,
sig,
block,
} = input;
let Block { stmts, .. } = *block;
let attr_str = attr.to_string().replace(' ', "");
let lock_attrs: Vec<&str> = attr_str.split(',').collect();
let (lock_name, wait_time) = match (lock_attrs.first(), lock_attrs.get(1)) {
(Some(name), None) => (name, 60),
(Some(name), Some(sec)) => {
if let Ok(wait_time) = sec.parse::<usize>() {
(name, wait_time)
} else {
abort_call_site!("`The second parameter of #[test_with::lock]` should be a number for waiting time");
}
}
(None, _) => {
abort_call_site!("`#[test_with::lock]` need a name for the file lock");
}
};
let lock_file = std::env::temp_dir().join(lock_name).display().to_string();

check_before_attrs(&attrs);

if has_test_attr(&attrs) {
quote! {
#(#attrs)*
#vis #sig {
let mut _test_with_lock = None;
for i in 0..#wait_time {
if let Ok(file) = std::fs::File::create_new(#lock_file) {
_test_with_lock = Some(file);
break;
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
if _test_with_lock.is_none() {
panic!("Fail to acquire the lock for the testcase")
}
#(#stmts)*
if std::fs::remove_file(#lock_file).is_err() {
panic!("Fail to unlock the testcase")
}
}
}
.into()
} else {
quote! {
#(#attrs)*
#[test]
#vis #sig {
let mut _test_with_lock = None;
for i in 0..#wait_time {
if let Ok(file) = std::fs::File::create_new(#lock_file) {
_test_with_lock = Some(file);
break;
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
if _test_with_lock.is_none() {
panic!("Fail to acquire the lock for the testcase")
}
#(#stmts)*
if std::fs::remove_file(#lock_file).is_err() {
panic!("Fail to unlock the testcase")
}
}
}
.into()
}
}

/// Sanitize the attribute string to remove any leading or trailing whitespace
/// and split the string into an iterator of individual environment variable names.
pub fn sanitize_env_vars_attr(attr_str: &str) -> impl Iterator<Item = &str> {
Expand Down

0 comments on commit b06ad40

Please sign in to comment.