diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b1866..3bc785a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Changed +- Add feature `crate-name` (enable by default to opt-in crate rename +support. See [#258](https://github.com/la10736/rstest/issues/258) + ### Add ### Fixed diff --git a/README.md b/README.md index 82fec41..97cd365 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ # Fixture-based test framework for Rust - - ## Introduction `rstest` uses procedural macros to help you on writing @@ -19,6 +17,11 @@ following lines to your `Cargo.toml` file: rstest = "0.20.0" ``` +### Features + +- `async-timeout`: `timeout` for `async` tests (Default enabled) +- `crate-name`: Import `rstest` package with different name (Default enabled) + ### Fixture The core idea is that you can inject your test dependencies diff --git a/rstest/Cargo.toml b/rstest/Cargo.toml index 7a1aef8..32af1d4 100644 --- a/rstest/Cargo.toml +++ b/rstest/Cargo.toml @@ -21,7 +21,8 @@ async-timeout = [ "dep:futures-timer", "rstest_macros/async-timeout", ] -default = ["async-timeout"] +crate-name = ["rstest_macros/crate-name"] +default = ["async-timeout", "crate-name"] [lib] diff --git a/rstest/src/lib.rs b/rstest/src/lib.rs index 61443f5..1c8c9da 100644 --- a/rstest/src/lib.rs +++ b/rstest/src/lib.rs @@ -93,6 +93,10 @@ //! assert!(string_processor.output.contains("Alice")); //! } //! ``` +//! ### Features +//! +//! - `async-timeout`: `timeout` for `async` tests (Default enabled) +//! - `crate-name`: Import `rstest` package with different name (Default enabled) //! //! ## Injecting fixtures as function arguments //! diff --git a/rstest/tests/resources/rstest/convert_string_literal_other_name.rs b/rstest/tests/resources/rstest/convert_string_literal_other_name.rs new file mode 100644 index 0000000..b5a0f88 --- /dev/null +++ b/rstest/tests/resources/rstest/convert_string_literal_other_name.rs @@ -0,0 +1,75 @@ +use other_name::*; +use std::net::SocketAddr; + +#[rstest] +#[case(true, "1.2.3.4:42")] +#[case(true, r#"4.3.2.1:24"#)] +#[case(false, "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443")] +#[case(false, r#"[2aa1:db8:85a3:8af:1319:8a2e:375:4873]:344"#)] +#[case(false, "this.is.not.a.socket.address")] +#[case(false, r#"this.is.not.a.socket.address"#)] +fn cases(#[case] expected: bool, #[case] addr: SocketAddr) { + assert_eq!(expected, addr.is_ipv4()); +} + +#[rstest] +fn values( + #[values( + "1.2.3.4:42", + r#"4.3.2.1:24"#, + "this.is.not.a.socket.address", + r#"this.is.not.a.socket.address"# + )] + addr: SocketAddr, +) { + assert!(addr.is_ipv4()) +} + +#[rstest] +#[case(b"12345")] +fn not_convert_byte_array(#[case] cases: &[u8], #[values(b"abc")] values: &[u8]) { + assert_eq!(5, cases.len()); + assert_eq!(3, values.len()); +} + +trait MyTrait { + fn my_trait(&self) -> u32 { + 42 + } +} + +impl MyTrait for &str {} + +#[rstest] +#[case("impl", "nothing")] +fn not_convert_impl(#[case] that_impl: impl MyTrait, #[case] s: &str) { + assert_eq!(42, that_impl.my_trait()); + assert_eq!(42, s.my_trait()); +} + +#[rstest] +#[case("1.2.3.4", "1.2.3.4:42")] +#[case("1.2.3.4".to_owned(), "1.2.3.4:42")] +fn not_convert_generics>(#[case] ip: S, #[case] addr: SocketAddr) { + assert_eq!(addr.ip().to_string(), ip.as_ref()); +} + +struct MyType(String); +struct E; +impl core::str::FromStr for MyType { + type Err = E; + + fn from_str(s: &str) -> Result { + match s { + "error" => Err(E), + inner => Ok(MyType(inner.to_owned())), + } + } +} + +#[rstest] +#[case("hello", "hello")] +#[case("doesn't mater", "error")] +fn convert_without_debug(#[case] expected: &str, #[case] converted: MyType) { + assert_eq!(expected, converted.0); +} diff --git a/rstest/tests/resources/rstest/timeout_other_name.rs b/rstest/tests/resources/rstest/timeout_other_name.rs new file mode 100644 index 0000000..b360b31 --- /dev/null +++ b/rstest/tests/resources/rstest/timeout_other_name.rs @@ -0,0 +1,114 @@ +use other_name::*; +use std::time::Duration; + +fn ms(ms: u32) -> Duration { + Duration::from_millis(ms.into()) +} + +mod thread { + use super::*; + + fn delayed_sum(a: u32, b: u32, delay: Duration) -> u32 { + std::thread::sleep(delay); + a + b + } + + #[rstest] + #[timeout(ms(80))] + fn single_pass() { + assert_eq!(4, delayed_sum(2, 2, ms(10))); + } + + #[rstest] + #[timeout(ms(100))] + fn single_fail_value() { + assert_eq!(5, delayed_sum(2, 2, ms(1))); + } + + #[rstest] + #[timeout(ms(1000))] + #[should_panic = "user message"] + fn fail_with_user_message() { + panic!("user message"); + } + + #[rstest] + #[timeout(ms(10))] + fn single_fail_timeout() { + assert_eq!(4, delayed_sum(2, 2, ms(80))); + } + + #[rstest] + #[timeout(ms(80))] + #[case(ms(10))] + fn one_pass(#[case] delay: Duration) { + assert_eq!(4, delayed_sum(2, 2, delay)); + } + + #[rstest] + #[timeout(ms(10))] + #[case(ms(80))] + fn one_fail_timeout(#[case] delay: Duration) { + assert_eq!(4, delayed_sum(2, 2, delay)); + } + + #[rstest] + #[timeout(ms(100))] + #[case(ms(1))] + fn one_fail_value(#[case] delay: Duration) { + assert_eq!(5, delayed_sum(2, 2, delay)); + } + + #[rstest] + #[case::pass(ms(1), 4)] + #[case::fail_timeout(ms(80), 4)] + #[case::fail_value(ms(1), 5)] + #[timeout(ms(40))] + fn group_same_timeout(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay)); + } + + #[rstest] + #[timeout(ms(100))] + #[case::pass(ms(1), 4)] + #[timeout(ms(30))] + #[case::fail_timeout(ms(70), 4)] + #[timeout(ms(100))] + #[case::fail_value(ms(1), 5)] + fn group_single_timeout(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay)); + } + + #[rstest] + #[case::pass(ms(1), 4)] + #[timeout(ms(10))] + #[case::fail_timeout(ms(60), 4)] + #[case::fail_value(ms(1), 5)] + #[timeout(ms(100))] + fn group_one_timeout_override(#[case] delay: Duration, #[case] expected: u32) { + assert_eq!(expected, delayed_sum(2, 2, delay)); + } + + struct S {} + + #[rstest] + #[case(S{})] + fn compile_with_no_copy_arg(#[case] _s: S) { + assert!(true); + } + + #[fixture] + fn no_copy() -> S { + S {} + } + + #[rstest] + fn compile_with_no_copy_fixture(no_copy: S) { + assert!(true); + } + + #[rstest] + fn default_timeout_failure() { + assert_eq!(4, delayed_sum(2, 2, ms(1100))); + } +} diff --git a/rstest/tests/rstest/mod.rs b/rstest/tests/rstest/mod.rs index 39278cc..1016521 100644 --- a/rstest/tests/rstest/mod.rs +++ b/rstest/tests/rstest/mod.rs @@ -1057,29 +1057,65 @@ fn timeout() { mod import_crate_with_other_name { use super::*; - fn prj(res: &str) -> Project { + fn prj(res: &str, features: Option<&[&str]>) -> Project { let prj = crate::base_prj(); + let default_features = features.is_none(); + let features = features + .map(|features| { + features + .iter() + .map(|f| format!(r#""{}""#, f)) + .collect::>() + .join(",") + }) + .unwrap_or_else(|| "".to_string()); prj.add_dependency( "other_name", &format!( - r#"{{path="{}", package = "rstest"}}"#, + r#"{{path="{}", package = "rstest", default-features = {}, features = [{}]}}"#, prj.exec_dir_str().as_str(), + default_features, + features ), ); prj.set_code_file(resources(res)) } #[test] - fn timeout_should_compile_and_run() { - let mut prj = prj("timeout.rs"); - prj.set_default_timeout(1); - assert!(prj.run_tests().is_ok()); + fn should_fails_to_compile_if_crate_name_feature_is_not_enabled() { + let prj = prj("timeout_other_name.rs", Some(&[])); + assert!(!prj.compile().unwrap().status.success()); } #[test] - fn convert_string_literal_should_compile_and_run() { - let prj = prj("convert_string_literal.rs"); - assert!(prj.run_tests().is_ok()); + fn should_always_compile_project_that_use_default_name() { + let prj = crate::base_prj(); + prj.add_dependency( + "rstest", + &format!( + r#"{{path="{}", default-features = false}}"#, + prj.exec_dir_str().as_str(), + ), + ); + let prj = prj.set_code_file(resources("convert_string_literal.rs")); + + assert!(prj.compile().unwrap().status.success()); + } + + #[rstest] + #[case::default_features(None)] + #[case::with_crate_name_feature(Some(["crate-name"].as_slice()))] + fn timeout_should_compile_and_run(#[case] features: Option<&[&str]>) { + let prj = prj("timeout_other_name.rs", features); + assert!(prj.compile().unwrap().status.success()); + } + + #[rstest] + #[case::default(None)] + #[case::with_crate_name_feature(Some(["crate-name"].as_slice()))] + fn convert_string_literal_should_compile_and_run(#[case] features: Option<&[&str]>) { + let prj = prj("convert_string_literal_other_name.rs", features); + assert!(prj.compile().unwrap().status.success()); } } diff --git a/rstest_macros/Cargo.toml b/rstest_macros/Cargo.toml index 6e7e05d..556e148 100644 --- a/rstest_macros/Cargo.toml +++ b/rstest_macros/Cargo.toml @@ -19,7 +19,8 @@ proc-macro = true [features] async-timeout = [] -default = ["async-timeout"] +default = ["async-timeout", "crate-name"] +crate-name = ["dep:proc-macro-crate"] [dependencies] cfg-if = "1.0.0" @@ -36,7 +37,7 @@ syn = { version = "2.0.2", features = [ "visit-mut", ] } unicode-ident = "1.0.5" -proc-macro-crate = "3.1.0" +proc-macro-crate = { version = "3.1.0", optional = true } [dev-dependencies] actix-rt = "2.7.0" diff --git a/rstest_macros/src/render/crate_resolver.rs b/rstest_macros/src/render/crate_resolver.rs index c795a21..39095d7 100644 --- a/rstest_macros/src/render/crate_resolver.rs +++ b/rstest_macros/src/render/crate_resolver.rs @@ -1,14 +1,21 @@ -use quote::format_ident; use syn::parse_quote; pub fn crate_name() -> syn::Path { - use proc_macro_crate::FoundCrate; + cfg_if::cfg_if! { + if #[cfg(feature = "crate-name")] { + use proc_macro_crate::FoundCrate; + use quote::format_ident; - match proc_macro_crate::crate_name("rstest").expect("rstest is present in `Cargo.toml` qed") { - FoundCrate::Itself => parse_quote! { rstest }, - FoundCrate::Name(name) => { - let myself = format_ident!("{name}"); - parse_quote! { #myself } + match proc_macro_crate::crate_name("rstest").expect("rstest is present in `Cargo.toml` qed") + { + FoundCrate::Itself => parse_quote! { rstest }, + FoundCrate::Name(name) => { + let myself = format_ident!("{name}"); + parse_quote! { #myself } + } } + } else { + parse_quote! { rstest } + } } }