From 840158fa7ed27472870bbd960d8693b68c1bd84f Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 6 Jul 2025 15:31:16 +0100 Subject: [PATCH 1/3] Use fixtures for extractor tests --- tests/app_data.rs | 33 +++++++++++++++++++++----------- tests/extractor.rs | 47 +++++++++++++++++++++++----------------------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/tests/app_data.rs b/tests/app_data.rs index 98900995..d10f2c9c 100644 --- a/tests/app_data.rs +++ b/tests/app_data.rs @@ -2,6 +2,7 @@ //! //! They verify successful extraction and error handling when state is missing. +use rstest::{fixture, rstest}; use wireframe::extractor::{ ExtractError, FromMessageRequest, @@ -10,20 +11,30 @@ use wireframe::extractor::{ SharedState, }; -#[test] -fn shared_state_extractor_returns_data() { - let mut req = MessageRequest::default(); - req.insert_state(5u32); - let mut payload = Payload::default(); - let extracted = SharedState::::from_message_request(&req, &mut payload).unwrap(); +#[allow(unused_braces)] +#[fixture] +fn request() -> MessageRequest { MessageRequest::default() } + +#[allow(unused_braces)] +#[fixture] +fn empty_payload() -> Payload<'static> { Payload::default() } + +#[rstest] +fn shared_state_extractor_returns_data( + mut request: MessageRequest, + mut empty_payload: Payload<'static>, +) { + request.insert_state(5u32); + let extracted = SharedState::::from_message_request(&request, &mut empty_payload).unwrap(); assert_eq!(*extracted, 5); } -#[test] -fn missing_shared_state_returns_error() { - let req = MessageRequest::default(); - let mut payload = Payload::default(); - let err = SharedState::::from_message_request(&req, &mut payload) +#[rstest] +fn missing_shared_state_returns_error( + request: MessageRequest, + mut empty_payload: Payload<'static>, +) { + let err = SharedState::::from_message_request(&request, &mut empty_payload) .err() .unwrap(); assert!(matches!(err, ExtractError::MissingState(_))); diff --git a/tests/extractor.rs b/tests/extractor.rs index 2f1564c3..767a350a 100644 --- a/tests/extractor.rs +++ b/tests/extractor.rs @@ -4,74 +4,75 @@ use std::net::SocketAddr; +use rstest::{fixture, rstest}; use wireframe::{ extractor::{ConnectionInfo, FromMessageRequest, Message, MessageRequest, Payload}, message::Message as MessageTrait, }; +#[allow(unused_braces)] +#[fixture] +fn request() -> MessageRequest { MessageRequest::default() } + +#[allow(unused_braces)] +#[fixture] +fn empty_payload() -> Payload<'static> { Payload::default() } + #[derive(bincode::Encode, bincode::BorrowDecode, PartialEq, Debug)] struct TestMsg(u8); -#[test] /// Tests that a message can be extracted from a payload and that the payload cursor advances fully. /// /// Verifies that a `TestMsg` instance serialised into bytes can be correctly extracted from a /// `Payload` using `Message::::from_message_request`, and asserts that the payload has no /// remaining unread data after extraction. -fn message_extractor_parses_and_advances() { +#[rstest] +fn message_extractor_parses_and_advances(request: MessageRequest) { let msg = TestMsg(42); let bytes = msg.to_bytes().unwrap(); let mut payload = Payload::new(bytes.as_slice()); - let req = MessageRequest::default(); - let extracted = Message::::from_message_request(&req, &mut payload).unwrap(); + let extracted = Message::::from_message_request(&request, &mut payload).unwrap(); assert_eq!(*extracted, msg); assert_eq!(payload.remaining(), 0); } -#[test] +#[rstest] /// Tests that `ConnectionInfo` correctly reports the peer socket address extracted from a /// `MessageRequest`. -fn connection_info_reports_peer() { +fn connection_info_reports_peer(mut request: MessageRequest, mut empty_payload: Payload<'static>) { let addr: SocketAddr = "127.0.0.1:12345" .parse() .expect("hard-coded socket address must be valid"); - let req = MessageRequest { - peer_addr: Some(addr), - ..Default::default() - }; - let mut payload = Payload::default(); - let info = ConnectionInfo::from_message_request(&req, &mut payload).unwrap(); + request.peer_addr = Some(addr); + let info = ConnectionInfo::from_message_request(&request, &mut empty_payload).unwrap(); assert_eq!(info.peer_addr(), Some(addr)); } -#[test] /// Tests that shared state of type `u8` can be successfully extracted from a `MessageRequest`'s /// `app_data`. /// /// Inserts an `Arc` into the request's shared state, extracts it using the `SharedState` /// extractor, and asserts that the extracted value matches the original. -fn shared_state_extractor() { - let mut req = MessageRequest::default(); - req.insert_state(42u8); - let mut payload = Payload::default(); +#[rstest] +fn shared_state_extractor(mut request: MessageRequest, mut empty_payload: Payload<'static>) { + request.insert_state(42u8); let state = - wireframe::extractor::SharedState::::from_message_request(&req, &mut payload).unwrap(); + wireframe::extractor::SharedState::::from_message_request(&request, &mut empty_payload) + .unwrap(); assert_eq!(*state, 42); } -#[test] /// Tests that extracting a missing shared state from a `MessageRequest` /// returns an `ExtractError::MissingState` containing the type name. /// /// Ensures that when no shared state of the requested type is present, /// the correct error is produced and includes the expected type information. -fn shared_state_missing_error() { - let req = MessageRequest::default(); - let mut payload = Payload::default(); +#[rstest] +fn shared_state_missing_error(request: MessageRequest, mut empty_payload: Payload<'static>) { let Err(err) = - wireframe::extractor::SharedState::::from_message_request(&req, &mut payload) + wireframe::extractor::SharedState::::from_message_request(&request, &mut empty_payload) else { panic!("expected error"); }; From 84111f3c9fdd75ccd5559d434a5cd5bb1156e61e Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 6 Jul 2025 15:52:47 +0100 Subject: [PATCH 2/3] Annotate unused braces allowances --- tests/app_data.rs | 10 ++++++++-- tests/connection_actor.rs | 20 ++++++++++++++++---- tests/extractor.rs | 10 ++++++++-- tests/push_policies.rs | 10 ++++++++-- tests/session_registry.rs | 10 ++++++++-- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/tests/app_data.rs b/tests/app_data.rs index d10f2c9c..f3794f5e 100644 --- a/tests/app_data.rs +++ b/tests/app_data.rs @@ -11,11 +11,17 @@ use wireframe::extractor::{ SharedState, }; -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn request() -> MessageRequest { MessageRequest::default() } -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn empty_payload() -> Payload<'static> { Payload::default() } diff --git a/tests/connection_actor.rs b/tests/connection_actor.rs index b746ded5..60a169a1 100644 --- a/tests/connection_actor.rs +++ b/tests/connection_actor.rs @@ -17,15 +17,24 @@ use wireframe::{ }; #[fixture] -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] fn queues() -> (PushQueues, wireframe::push::PushHandle) { PushQueues::bounded(8, 8) } #[fixture] -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] fn shutdown_token() -> CancellationToken { CancellationToken::new() } #[fixture] -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] fn empty_stream() -> Option> { None } #[rstest] @@ -341,7 +350,10 @@ impl std::ops::DerefMut for LoggerHandle { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard } } -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn logger() -> LoggerHandle { LoggerHandle::new() } diff --git a/tests/extractor.rs b/tests/extractor.rs index 767a350a..57fc3349 100644 --- a/tests/extractor.rs +++ b/tests/extractor.rs @@ -10,11 +10,17 @@ use wireframe::{ message::Message as MessageTrait, }; -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn request() -> MessageRequest { MessageRequest::default() } -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn empty_payload() -> Payload<'static> { Payload::default() } diff --git a/tests/push_policies.rs b/tests/push_policies.rs index ac938f12..3b9114ad 100644 --- a/tests/push_policies.rs +++ b/tests/push_policies.rs @@ -38,11 +38,17 @@ impl std::ops::DerefMut for LoggerHandle { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.guard } } -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn logger() -> LoggerHandle { LoggerHandle::new() } -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] #[fixture] fn rt() -> Runtime { tokio::runtime::Builder::new_current_thread() diff --git a/tests/session_registry.rs b/tests/session_registry.rs index 9bf2b8a5..1f88e68f 100644 --- a/tests/session_registry.rs +++ b/tests/session_registry.rs @@ -6,11 +6,17 @@ use wireframe::{ }; #[fixture] -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] fn registry() -> SessionRegistry { SessionRegistry::default() } #[fixture] -#[allow(unused_braces)] +#[allow( + unused_braces, + reason = "rustc false positive for single line rstest fixtures" +)] fn push_setup() -> (PushQueues, PushHandle) { PushQueues::bounded(1, 1) } /// Test that handles can be retrieved whilst the connection remains alive. From 4fb97041f6981d9f3a3c17d9c8cd0f296a9afad4 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 6 Jul 2025 16:24:36 +0100 Subject: [PATCH 3/3] Document rstest expect limitation --- docs/rust-testing-with-rstest-fixtures.md | 43 ++++++++++++----------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/rust-testing-with-rstest-fixtures.md b/docs/rust-testing-with-rstest-fixtures.md index c9612634..0d2b90c3 100644 --- a/docs/rust-testing-with-rstest-fixtures.md +++ b/docs/rust-testing-with-rstest-fixtures.md @@ -540,9 +540,12 @@ When using `#[once]`, there are critical warnings: the end of the test suite. This makes `#[once]` fixtures best suited for truly passive data or resources whose cleanup is managed by the operating system upon process exit. -2. **Functional Limitations:** `#[once]` fixtures cannot be `async` functions +1. **Functional Limitations:** `#[once]` fixtures cannot be `async` functions and cannot be generic functions (neither with generic type parameters nor using `impl Trait` in arguments or return types). +1. **Attribute Propagation:** `rstest` macros currently drop `#[expect]` + attributes. If you rely on lint expectations, use `#[allow]` instead to + silence false positives. The "never dropped" behaviour arises because `rstest` typically creates a `static` variable to hold the result of the `#[once]` fixture. `static` @@ -1165,13 +1168,13 @@ The following table summarizes key differences: **Table 1:** `rstest` **vs. Standard Rust** `#[test]` **for Fixture Management and Parameterization** -| Feature | Standard #[test] Approach | rstest Approach | +| Feature | Standard #[test] Approach | rstest Approach | | ------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| Fixture Injection | Manual calls to setup functions within each test. | Fixture name as argument in #[rstest] function; fixture defined with #[fixture]. | -| Parameterized Tests (Specific Cases) | Loop inside one test, or multiple distinct #[test] functions. | #[case(...)] attributes on #[rstest] function. | -| Parameterized Tests (Value Combinations) | Nested loops inside one test, or complex manual generation. | #[values(...)] attributes on arguments of #[rstest] function. | -| Async Fixture Setup | Manual async block and .await calls inside test. | async fn fixtures, with #[future] and #[awt] for ergonomic `.await`ing. | -| Reusing Parameter Sets | Manual duplication of cases or custom helper macros. | rstest_reuse crate with #[template] and #[apply] attributes. | +| Fixture Injection | Manual calls to setup functions within each test. | Fixture name as argument in #[rstest] function; fixture defined with #[fixture]. | +| Parameterized Tests (Specific Cases) | Loop inside one test, or multiple distinct #[test] functions. | #[case(...)] attributes on #[rstest] function. | +| Parameterized Tests (Value Combinations) | Nested loops inside one test, or complex manual generation. | #[values(...)] attributes on arguments of #[rstest] function. | +| Async Fixture Setup | Manual async block and .await calls inside test. | async fn fixtures, with #[future] and #[awt] for ergonomic `.await`ing. | +| Reusing Parameter Sets | Manual duplication of cases or custom helper macros. | rstest_reuse crate with #[template] and #[apply] attributes. | This comparison highlights how `rstest`'s attribute-based, declarative approach streamlines common testing patterns, reducing manual effort and improving the @@ -1332,20 +1335,20 @@ provided by `rstest`: **Table 2: Key** `rstest` **Attributes Quick Reference** -| Attribute | Core Purpose | +| Attribute | Core Purpose | | ---------------------------- | -------------------------------------------------------------------------------------------- | -| #[rstest] | Marks a function as an rstest test; enables fixture injection and parameterization. | -| #[fixture] | Defines a function that provides a test fixture (setup data or services). | -| #[case(...)] | Defines a single parameterized test case with specific input values. | -| #[values(...)] | Defines a list of values for an argument, generating tests for each value or combination. | -| #[once] | Marks a fixture to be initialized only once and shared (as a static reference) across tests. | -| #[future] | Simplifies async argument types by removing impl Future boilerplate. | -| #[awt] | (Function or argument level) Automatically .awaits future arguments in async tests. | -| #[from(original_name)] | Allows renaming an injected fixture argument in the test function. | -| #[with(...)] | Overrides default arguments of a fixture for a specific test. | -| #[default(...)] | Provides default values for arguments within a fixture function. | -| #[timeout(...)] | Sets a timeout for an asynchronous test. | -| #[files("glob_pattern",...)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. | +| #[rstest] | Marks a function as an rstest test; enables fixture injection and parameterization. | +| #[fixture] | Defines a function that provides a test fixture (setup data or services). | +| #[case(...)] | Defines a single parameterized test case with specific input values. | +| #[values(...)] | Defines a list of values for an argument, generating tests for each value or combination. | +| #[once] | Marks a fixture to be initialized only once and shared (as a static reference) across tests. | +| #[future] | Simplifies async argument types by removing impl Future boilerplate. | +| #[awt] | (Function or argument level) Automatically .awaits future arguments in async tests. | +| #[from(original_name)] | Allows renaming an injected fixture argument in the test function. | +| #[with(...)] | Overrides default arguments of a fixture for a specific test. | +| #[default(...)] | Provides default values for arguments within a fixture function. | +| #[timeout(...)] | Sets a timeout for an asynchronous test. | +| #[files("glob_pattern",...)] | Injects file paths (or contents, with mode=) matching a glob pattern as test arguments. | By mastering `rstest`, Rust developers can significantly elevate the quality and efficiency of their testing practices, leading to more reliable and maintainable