1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Test world for codec fixture behavioural scenarios.
//!
//! Exercises the `wireframe_testing` codec fixture functions with
//! `HotlineFrameCodec` to verify that each fixture category produces
//! wire bytes with the expected decoding behaviour.
use std::io;
use rstest::fixture;
use wireframe::codec::{
FrameCodec,
examples::{HotlineFrame, HotlineFrameCodec},
};
/// Re-export `TestResult` from `wireframe_testing` for use in steps.
pub use wireframe_testing::TestResult;
/// BDD world holding the codec, decoded frames, and any decode error.
#[derive(Debug, Default)]
pub struct CodecFixturesWorld {
codec: Option<HotlineFrameCodec>,
decoded_frames: Vec<HotlineFrame>,
decode_error: Option<io::Error>,
}
/// Fixture for codec fixture scenarios used by rstest-bdd steps.
///
/// Note: `rustfmt` collapses simple fixtures into one line, which triggers
/// `unused_braces`, so keep `rustfmt::skip`.
#[rustfmt::skip]
#[fixture]
pub fn codec_fixtures_world() -> CodecFixturesWorld {
CodecFixturesWorld::default()
}
impl CodecFixturesWorld {
/// Configure the Hotline codec with the given maximum frame length.
///
/// # Errors
/// Returns an error if the codec is already configured.
pub fn configure_codec(&mut self, max_frame_length: usize) -> TestResult {
if self.codec.is_some() {
return Err("codec already configured".into());
}
self.codec = Some(HotlineFrameCodec::new(max_frame_length));
Ok(())
}
/// Decode a valid fixture frame and store the results.
///
/// # Errors
/// Returns an error if the codec is not configured or decoding fails
/// unexpectedly.
pub fn decode_valid_fixture(&mut self) -> TestResult {
let wire = wireframe_testing::valid_hotline_wire(b"fixture-payload", 7);
self.decode_fixture(wire)
}
/// Decode an oversized fixture frame and store the error.
///
/// # Errors
/// Returns an error if the codec is not configured.
pub fn decode_oversized_fixture(&mut self) -> TestResult {
let codec = self.codec.as_ref().ok_or("codec not configured")?;
let wire = wireframe_testing::oversized_hotline_wire(codec.max_frame_length());
self.decode_fixture(wire)
}
/// Decode a truncated fixture frame and store the error.
///
/// # Errors
/// Returns an error if the codec is not configured.
pub fn decode_truncated_fixture(&mut self) -> TestResult {
let wire = wireframe_testing::truncated_hotline_header();
self.decode_fixture(wire)
}
/// Decode correlated fixture frames and store the results.
///
/// # Errors
/// Returns an error if the codec is not configured or decoding fails
/// unexpectedly.
pub fn decode_correlated_fixture(&mut self) -> TestResult {
let wire = wireframe_testing::correlated_hotline_wire(42, &[b"a", b"b", b"c"]);
self.decode_fixture(wire)
}
/// Decode `wire` with the configured codec, storing frames or error.
fn decode_fixture(&mut self, wire: Vec<u8>) -> TestResult {
self.decoded_frames.clear();
self.decode_error = None;
let codec = self.codec.as_ref().ok_or("codec not configured")?;
match wireframe_testing::decode_frames_with_codec(codec, wire) {
Ok(frames) => self.decoded_frames = frames,
Err(e) => self.decode_error = Some(e),
}
Ok(())
}
/// Verify the decoded payload matches the expected fixture input.
///
/// # Errors
/// Returns an error if the assertion fails.
pub fn verify_payload_matches(&self) -> TestResult {
if self.decoded_frames.len() != 1 {
return Err(format!("expected 1 frame, got {}", self.decoded_frames.len()).into());
}
let frame = self
.decoded_frames
.first()
.ok_or("expected at least one frame")?;
if frame.payload.as_ref() != b"fixture-payload" {
return Err(format!(
"payload mismatch: expected b\"fixture-payload\", got {:?}",
frame.payload.as_ref()
)
.into());
}
Ok(())
}
/// Verify the decoder produced an `InvalidData` error for oversized
/// frames.
///
/// # Errors
/// Returns an error if no decode error was captured or it does not
/// contain the expected message.
pub fn verify_invalid_data_error(&self) -> TestResult {
self.verify_error_message_contains("payload too large")
}
/// Verify the decoder produced a "bytes remaining" error for truncated
/// frames.
///
/// # Errors
/// Returns an error if no decode error was captured or it does not
/// contain the expected message.
pub fn verify_bytes_remaining_error(&self) -> TestResult {
self.verify_error_message_contains("bytes remaining")
}
/// Verify the captured decode error contains `expected_substring`.
fn verify_error_message_contains(&self, expected_substring: &str) -> TestResult {
let err = self
.decode_error
.as_ref()
.ok_or("expected a decode error but decoding succeeded")?;
if !err.to_string().contains(expected_substring) {
return Err(format!("expected '{expected_substring}' error, got: {err}").into());
}
Ok(())
}
/// Verify all decoded frames share the expected transaction identifier.
///
/// # Errors
/// Returns an error if no frames were decoded or any frame has a
/// different transaction ID.
pub fn verify_correlated_transaction_ids(&self) -> TestResult {
if self.decoded_frames.len() != 3 {
return Err(format!("expected 3 frames, got {}", self.decoded_frames.len()).into());
}
for (i, frame) in self.decoded_frames.iter().enumerate() {
if frame.transaction_id != 42 {
return Err(format!(
"frame {i}: expected transaction_id 42, got {}",
frame.transaction_id
)
.into());
}
}
Ok(())
}
}