|
| 1 | +//! BDD world fixture for partial frame and fragment feeding scenarios. |
| 2 | +//! |
| 3 | +//! Tracks application configuration, fragmenter state, and collected |
| 4 | +//! responses across behavioural test steps. |
| 5 | +
|
| 6 | +use std::{num::NonZeroUsize, sync::Arc}; |
| 7 | + |
| 8 | +use futures::future::BoxFuture; |
| 9 | +use rstest::fixture; |
| 10 | +use wireframe::{ |
| 11 | + app::{Envelope, WireframeApp}, |
| 12 | + codec::examples::HotlineFrameCodec, |
| 13 | + fragment::Fragmenter, |
| 14 | + serializer::{BincodeSerializer, Serializer}, |
| 15 | +}; |
| 16 | +/// Re-export `TestResult` from `wireframe_testing` for use in steps. |
| 17 | +pub use wireframe_testing::TestResult; |
| 18 | + |
| 19 | +/// BDD world holding the app, codec, fragmenter, and collected responses. |
| 20 | +/// |
| 21 | +/// `WireframeApp` does not implement `Debug`, so this type provides a manual |
| 22 | +/// implementation that redacts the app field. |
| 23 | +#[derive(Default)] |
| 24 | +pub struct PartialFrameFeedingWorld { |
| 25 | + codec: Option<HotlineFrameCodec>, |
| 26 | + app: Option<WireframeApp<BincodeSerializer, (), Envelope, HotlineFrameCodec>>, |
| 27 | + fragmenter: Option<Fragmenter>, |
| 28 | + response_payloads: Vec<Vec<u8>>, |
| 29 | + fragment_feeding_completed: bool, |
| 30 | +} |
| 31 | + |
| 32 | +impl std::fmt::Debug for PartialFrameFeedingWorld { |
| 33 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 34 | + f.debug_struct("PartialFrameFeedingWorld") |
| 35 | + .field("codec", &self.codec) |
| 36 | + .field("app", &self.app.as_ref().map(|_| "..")) |
| 37 | + .field("fragmenter", &self.fragmenter) |
| 38 | + .field("response_payloads", &self.response_payloads.len()) |
| 39 | + .field( |
| 40 | + "fragment_feeding_completed", |
| 41 | + &self.fragment_feeding_completed, |
| 42 | + ) |
| 43 | + .finish() |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +/// Fixture for partial frame feeding scenarios used by rstest-bdd steps. |
| 48 | +/// |
| 49 | +/// Note: rustfmt collapses simple fixtures into one line, which triggers |
| 50 | +/// `unused_braces`, so keep `rustfmt::skip`. |
| 51 | +#[rustfmt::skip] |
| 52 | +#[fixture] |
| 53 | +pub fn partial_frame_feeding_world() -> PartialFrameFeedingWorld { |
| 54 | + PartialFrameFeedingWorld::default() |
| 55 | +} |
| 56 | + |
| 57 | +impl PartialFrameFeedingWorld { |
| 58 | + /// Configure the app with a `HotlineFrameCodec`. |
| 59 | + /// |
| 60 | + /// # Errors |
| 61 | + /// Returns an error if the app or route registration fails. |
| 62 | + pub fn configure_app(&mut self, max_frame_length: usize) -> TestResult { |
| 63 | + let codec = HotlineFrameCodec::new(max_frame_length); |
| 64 | + let app = WireframeApp::<BincodeSerializer, (), Envelope>::new()? |
| 65 | + .with_codec(codec.clone()) |
| 66 | + .route( |
| 67 | + 1, |
| 68 | + Arc::new(|_: &Envelope| -> BoxFuture<'static, ()> { Box::pin(async {}) }), |
| 69 | + )?; |
| 70 | + self.codec = Some(codec); |
| 71 | + self.app = Some(app); |
| 72 | + Ok(()) |
| 73 | + } |
| 74 | + |
| 75 | + /// Configure a fragmenter with the given maximum payload per fragment. |
| 76 | + /// |
| 77 | + /// # Errors |
| 78 | + /// Returns an error if the payload cap is zero. |
| 79 | + pub fn configure_fragmenter(&mut self, max_payload: usize) -> TestResult { |
| 80 | + let cap = NonZeroUsize::new(max_payload).ok_or("fragment payload cap must be non-zero")?; |
| 81 | + self.fragmenter = Some(Fragmenter::new(cap)); |
| 82 | + Ok(()) |
| 83 | + } |
| 84 | + |
| 85 | + /// Drive the app with a single payload chunked into `chunk_size` pieces. |
| 86 | + /// |
| 87 | + /// # Errors |
| 88 | + /// Returns an error if the app or codec is not configured, or driving fails. |
| 89 | + pub async fn drive_chunked(&mut self, chunk_size: usize) -> TestResult { |
| 90 | + let app = self.app.take().ok_or("app not configured")?; |
| 91 | + let codec = self.codec.as_ref().ok_or("codec not configured")?; |
| 92 | + let chunk = NonZeroUsize::new(chunk_size).ok_or("chunk size must be non-zero")?; |
| 93 | + |
| 94 | + let env = Envelope::new(1, Some(7), b"bdd-chunked".to_vec()); |
| 95 | + let serialized = BincodeSerializer.serialize(&env)?; |
| 96 | + |
| 97 | + self.response_payloads = |
| 98 | + wireframe_testing::drive_with_partial_frames(app, codec, vec![serialized], chunk) |
| 99 | + .await?; |
| 100 | + Ok(()) |
| 101 | + } |
| 102 | + |
| 103 | + /// Drive the app with `count` payloads chunked into `chunk_size` pieces. |
| 104 | + /// |
| 105 | + /// # Errors |
| 106 | + /// Returns an error if the app or codec is not configured, or driving fails. |
| 107 | + pub async fn drive_chunked_multiple(&mut self, count: usize, chunk_size: usize) -> TestResult { |
| 108 | + let app = self.app.take().ok_or("app not configured")?; |
| 109 | + let codec = self.codec.as_ref().ok_or("codec not configured")?; |
| 110 | + let chunk = NonZeroUsize::new(chunk_size).ok_or("chunk size must be non-zero")?; |
| 111 | + |
| 112 | + let mut payloads = Vec::with_capacity(count); |
| 113 | + for i in 0..count { |
| 114 | + let env = Envelope::new(1, Some(7), vec![u8::try_from(i).unwrap_or(0)]); |
| 115 | + payloads.push(BincodeSerializer.serialize(&env)?); |
| 116 | + } |
| 117 | + |
| 118 | + self.response_payloads = |
| 119 | + wireframe_testing::drive_with_partial_frames(app, codec, payloads, chunk).await?; |
| 120 | + Ok(()) |
| 121 | + } |
| 122 | + |
| 123 | + /// Drive the app with a fragmented payload. |
| 124 | + /// |
| 125 | + /// # Errors |
| 126 | + /// Returns an error if the app, codec, or fragmenter is not configured. |
| 127 | + pub async fn drive_fragmented(&mut self, payload_len: usize) -> TestResult { |
| 128 | + let app = self.app.take().ok_or("app not configured")?; |
| 129 | + let codec = self.codec.as_ref().ok_or("codec not configured")?; |
| 130 | + let fragmenter = self |
| 131 | + .fragmenter |
| 132 | + .as_ref() |
| 133 | + .ok_or("fragmenter not configured")?; |
| 134 | + |
| 135 | + let _payloads = |
| 136 | + wireframe_testing::drive_with_fragments(app, codec, fragmenter, vec![0; payload_len]) |
| 137 | + .await?; |
| 138 | + self.fragment_feeding_completed = true; |
| 139 | + Ok(()) |
| 140 | + } |
| 141 | + |
| 142 | + /// Drive the app with a fragmented payload fed in chunks. |
| 143 | + /// |
| 144 | + /// # Errors |
| 145 | + /// Returns an error if the app, codec, or fragmenter is not configured. |
| 146 | + pub async fn drive_partial_fragmented( |
| 147 | + &mut self, |
| 148 | + payload_len: usize, |
| 149 | + chunk_size: usize, |
| 150 | + ) -> TestResult { |
| 151 | + let app = self.app.take().ok_or("app not configured")?; |
| 152 | + let codec = self.codec.as_ref().ok_or("codec not configured")?; |
| 153 | + let fragmenter = self |
| 154 | + .fragmenter |
| 155 | + .as_ref() |
| 156 | + .ok_or("fragmenter not configured")?; |
| 157 | + let chunk = NonZeroUsize::new(chunk_size).ok_or("chunk size must be non-zero")?; |
| 158 | + |
| 159 | + let _payloads = wireframe_testing::drive_with_partial_fragments( |
| 160 | + app, |
| 161 | + codec, |
| 162 | + fragmenter, |
| 163 | + vec![0; payload_len], |
| 164 | + chunk, |
| 165 | + ) |
| 166 | + .await?; |
| 167 | + self.fragment_feeding_completed = true; |
| 168 | + Ok(()) |
| 169 | + } |
| 170 | + |
| 171 | + /// Assert that fragment feeding completed without error. |
| 172 | + /// |
| 173 | + /// # Errors |
| 174 | + /// Returns an error if fragment feeding has not been attempted or failed. |
| 175 | + pub fn assert_fragment_feeding_completed(&self) -> TestResult { |
| 176 | + if !self.fragment_feeding_completed { |
| 177 | + return Err("fragment feeding has not completed successfully".into()); |
| 178 | + } |
| 179 | + Ok(()) |
| 180 | + } |
| 181 | + |
| 182 | + /// Assert that response payloads are non-empty. |
| 183 | + /// |
| 184 | + /// # Errors |
| 185 | + /// Returns an error if no response payloads were collected. |
| 186 | + pub fn assert_payloads_non_empty(&self) -> TestResult { |
| 187 | + if self.response_payloads.is_empty() { |
| 188 | + return Err("expected non-empty response payloads".into()); |
| 189 | + } |
| 190 | + Ok(()) |
| 191 | + } |
| 192 | + |
| 193 | + /// Assert that the response contains exactly `expected` payloads. |
| 194 | + /// |
| 195 | + /// # Errors |
| 196 | + /// Returns an error if the payload count does not match. |
| 197 | + pub fn assert_payload_count(&self, expected: usize) -> TestResult { |
| 198 | + let actual = self.response_payloads.len(); |
| 199 | + if actual != expected { |
| 200 | + return Err(format!("expected {expected} response payloads, got {actual}").into()); |
| 201 | + } |
| 202 | + Ok(()) |
| 203 | + } |
| 204 | +} |
0 commit comments