1use cloud_sdk::buffer::sanitize_bytes;
4use cloud_sdk::transport::{
5 AsyncStreamSink, AsyncStreamSource, BlockingStreamSink, BlockingStreamSource,
6 StreamPartialState, StreamRead, StreamReplayability,
7};
8
9pub const MAX_STREAM_FIXTURE_CHUNKS: usize = 1_024;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum StreamFixtureError {
15 TooManyChunks,
17 SourceScratchTooSmall,
19 SinkStorageTooSmall,
21 ZeroWriteLimit,
23 ZeroFaultIndex,
25 InjectedSourceFault,
27 InjectedSinkFault,
29 EmptyPatternData,
31}
32
33impl_static_error!(StreamFixtureError,
34 Self::TooManyChunks => "stream fixture contains too many chunks",
35 Self::SourceScratchTooSmall => "stream fixture scratch storage is too small",
36 Self::SinkStorageTooSmall => "stream fixture sink storage is too small",
37 Self::ZeroWriteLimit => "stream fixture sink write limit is zero",
38 Self::ZeroFaultIndex => "stream fixture fault index must be nonzero",
39 Self::InjectedSourceFault => "stream fixture injected a source failure",
40 Self::InjectedSinkFault => "stream fixture injected a sink failure",
41 Self::EmptyPatternData => "alternating stream pattern data is empty",
42);
43
44pub struct StreamFixtureSource<'fixture> {
46 chunks: &'fixture [&'fixture [u8]],
47 index: usize,
48 observations: usize,
49 replayability: StreamReplayability<'fixture>,
50 fault_at_observation: Option<usize>,
51}
52
53impl<'fixture> StreamFixtureSource<'fixture> {
54 pub const fn new(chunks: &'fixture [&'fixture [u8]]) -> Result<Self, StreamFixtureError> {
57 if chunks.len() > MAX_STREAM_FIXTURE_CHUNKS {
58 return Err(StreamFixtureError::TooManyChunks);
59 }
60 Ok(Self {
61 chunks,
62 index: 0,
63 observations: 0,
64 replayability: StreamReplayability::NotReplayable,
65 fault_at_observation: None,
66 })
67 }
68
69 pub const fn with_replayability(
71 chunks: &'fixture [&'fixture [u8]],
72 replayability: StreamReplayability<'fixture>,
73 ) -> Result<Self, StreamFixtureError> {
74 if chunks.len() > MAX_STREAM_FIXTURE_CHUNKS {
75 return Err(StreamFixtureError::TooManyChunks);
76 }
77 Ok(Self {
78 chunks,
79 index: 0,
80 observations: 0,
81 replayability,
82 fault_at_observation: None,
83 })
84 }
85
86 pub const fn with_fault_at_observation(
88 mut self,
89 observation: usize,
90 ) -> Result<Self, StreamFixtureError> {
91 if observation == 0 {
92 return Err(StreamFixtureError::ZeroFaultIndex);
93 }
94 self.fault_at_observation = Some(observation);
95 Ok(self)
96 }
97
98 #[must_use]
100 pub const fn observations(&self) -> usize {
101 self.observations
102 }
103
104 fn read(&mut self, output: &mut [u8]) -> Result<StreamRead, StreamFixtureError> {
105 self.observations = self.observations.saturating_add(1);
106 if self.fault_at_observation == Some(self.observations) {
107 return Err(StreamFixtureError::InjectedSourceFault);
108 }
109 let Some(chunk) = self.chunks.get(self.index) else {
110 return Ok(StreamRead::End);
111 };
112 let target = output
113 .get_mut(..chunk.len())
114 .ok_or(StreamFixtureError::SourceScratchTooSmall)?;
115 target.copy_from_slice(chunk);
116 self.index = self.index.saturating_add(1);
117 Ok(StreamRead::Chunk(chunk.len()))
118 }
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub enum StreamPattern<'fixture> {
124 EndlessEmpty,
126 AlternatingEmptyData(&'fixture [u8]),
128}
129
130pub struct StreamPatternSource<'fixture> {
132 pattern: StreamPattern<'fixture>,
133 observations: usize,
134}
135
136impl<'fixture> StreamPatternSource<'fixture> {
137 pub const fn new(pattern: StreamPattern<'fixture>) -> Result<Self, StreamFixtureError> {
139 if matches!(pattern, StreamPattern::AlternatingEmptyData(data) if data.is_empty()) {
140 return Err(StreamFixtureError::EmptyPatternData);
141 }
142 Ok(Self {
143 pattern,
144 observations: 0,
145 })
146 }
147
148 #[must_use]
150 pub const fn observations(&self) -> usize {
151 self.observations
152 }
153
154 fn read(&mut self, output: &mut [u8]) -> Result<StreamRead, StreamFixtureError> {
155 self.observations = self.observations.saturating_add(1);
156 match self.pattern {
157 StreamPattern::EndlessEmpty => Ok(StreamRead::Chunk(0)),
158 StreamPattern::AlternatingEmptyData(_) if self.observations % 2 == 1 => {
159 Ok(StreamRead::Chunk(0))
160 }
161 StreamPattern::AlternatingEmptyData(data) => {
162 let target = output
163 .get_mut(..data.len())
164 .ok_or(StreamFixtureError::SourceScratchTooSmall)?;
165 target.copy_from_slice(data);
166 Ok(StreamRead::Chunk(data.len()))
167 }
168 }
169 }
170}
171
172impl BlockingStreamSource for StreamPatternSource<'_> {
173 type Error = StreamFixtureError;
174
175 fn replayability(&self) -> StreamReplayability<'_> {
176 StreamReplayability::NotReplayable
177 }
178
179 fn read_chunk(&mut self, output: &mut [u8]) -> Result<StreamRead, Self::Error> {
180 self.read(output)
181 }
182}
183
184impl AsyncStreamSource for StreamPatternSource<'_> {
185 type Error = StreamFixtureError;
186
187 fn replayability(&self) -> StreamReplayability<'_> {
188 StreamReplayability::NotReplayable
189 }
190
191 async fn read_chunk<'operation>(
192 &'operation mut self,
193 output: &'operation mut [u8],
194 ) -> Result<StreamRead, Self::Error> {
195 self.read(output)
196 }
197}
198
199impl BlockingStreamSource for StreamFixtureSource<'_> {
200 type Error = StreamFixtureError;
201
202 fn replayability(&self) -> StreamReplayability<'_> {
203 self.replayability
204 }
205
206 fn read_chunk(&mut self, output: &mut [u8]) -> Result<StreamRead, Self::Error> {
207 self.read(output)
208 }
209}
210
211impl AsyncStreamSource for StreamFixtureSource<'_> {
212 type Error = StreamFixtureError;
213
214 fn replayability(&self) -> StreamReplayability<'_> {
215 self.replayability
216 }
217
218 async fn read_chunk<'operation>(
219 &'operation mut self,
220 output: &'operation mut [u8],
221 ) -> Result<StreamRead, Self::Error> {
222 self.read(output)
223 }
224}
225
226pub struct StreamFixtureSink<'storage> {
228 output: &'storage mut [u8],
229 initialized_len: usize,
230 max_write_bytes: usize,
231 writes: usize,
232 committed: bool,
233 aborted: Option<StreamPartialState>,
234 fault_at_write: Option<usize>,
235}
236
237impl<'storage> StreamFixtureSink<'storage> {
238 pub fn new(
240 output: &'storage mut [u8],
241 max_write_bytes: usize,
242 ) -> Result<Self, StreamFixtureError> {
243 sanitize_bytes(output);
244 if max_write_bytes == 0 {
245 return Err(StreamFixtureError::ZeroWriteLimit);
246 }
247 Ok(Self {
248 output,
249 initialized_len: 0,
250 max_write_bytes,
251 writes: 0,
252 committed: false,
253 aborted: None,
254 fault_at_write: None,
255 })
256 }
257
258 pub const fn with_fault_at_write(mut self, write: usize) -> Result<Self, StreamFixtureError> {
260 if write == 0 {
261 return Err(StreamFixtureError::ZeroFaultIndex);
262 }
263 self.fault_at_write = Some(write);
264 Ok(self)
265 }
266
267 #[must_use]
269 pub fn bytes(&self) -> &[u8] {
270 self.output.get(..self.initialized_len).unwrap_or_default()
271 }
272
273 #[must_use]
275 pub const fn writes(&self) -> usize {
276 self.writes
277 }
278
279 #[must_use]
281 pub const fn is_committed(&self) -> bool {
282 self.committed
283 }
284
285 #[must_use]
287 pub const fn aborted_with(&self) -> Option<StreamPartialState> {
288 self.aborted
289 }
290
291 fn write(&mut self, input: &[u8]) -> Result<usize, StreamFixtureError> {
292 let next_write = self.writes.saturating_add(1);
293 if self.fault_at_write == Some(next_write) {
294 return Err(StreamFixtureError::InjectedSinkFault);
295 }
296 let accepted = core::cmp::min(input.len(), self.max_write_bytes);
297 let end = self
298 .initialized_len
299 .checked_add(accepted)
300 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
301 let target = self
302 .output
303 .get_mut(self.initialized_len..end)
304 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
305 let source = input
306 .get(..accepted)
307 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
308 target.copy_from_slice(source);
309 self.initialized_len = end;
310 self.writes = self.writes.saturating_add(1);
311 Ok(accepted)
312 }
313
314 fn commit_inner(&mut self) {
315 self.committed = true;
316 }
317
318 fn abort_inner(&mut self, partial: StreamPartialState) {
319 self.aborted = Some(partial);
320 if matches!(partial, StreamPartialState::RollbackRequired) {
321 sanitize_bytes(self.output);
322 self.initialized_len = 0;
323 }
324 }
325}
326
327impl BlockingStreamSink for StreamFixtureSink<'_> {
328 type Error = StreamFixtureError;
329
330 fn write_chunk(&mut self, input: &[u8]) -> Result<usize, Self::Error> {
331 self.write(input)
332 }
333
334 fn commit(&mut self) -> Result<(), Self::Error> {
335 self.commit_inner();
336 Ok(())
337 }
338
339 fn abort(&mut self, partial: StreamPartialState) {
340 self.abort_inner(partial);
341 }
342}
343
344impl AsyncStreamSink for StreamFixtureSink<'_> {
345 type Error = StreamFixtureError;
346
347 async fn write_chunk<'operation>(
348 &'operation mut self,
349 input: &'operation [u8],
350 ) -> Result<usize, Self::Error> {
351 self.write(input)
352 }
353
354 async fn commit(&mut self) -> Result<(), Self::Error> {
355 self.commit_inner();
356 Ok(())
357 }
358
359 fn abort(&mut self, partial: StreamPartialState) {
360 self.abort_inner(partial);
361 }
362}