cloud_sdk_testkit/
stream.rs1use 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}
24
25impl_static_error!(StreamFixtureError,
26 Self::TooManyChunks => "stream fixture contains too many chunks",
27 Self::SourceScratchTooSmall => "stream fixture scratch storage is too small",
28 Self::SinkStorageTooSmall => "stream fixture sink storage is too small",
29 Self::ZeroWriteLimit => "stream fixture sink write limit is zero",
30);
31
32pub struct StreamFixtureSource<'fixture> {
34 chunks: &'fixture [&'fixture [u8]],
35 index: usize,
36 observations: usize,
37 replayability: StreamReplayability<'fixture>,
38}
39
40impl<'fixture> StreamFixtureSource<'fixture> {
41 pub const fn new(chunks: &'fixture [&'fixture [u8]]) -> Result<Self, StreamFixtureError> {
44 if chunks.len() > MAX_STREAM_FIXTURE_CHUNKS {
45 return Err(StreamFixtureError::TooManyChunks);
46 }
47 Ok(Self {
48 chunks,
49 index: 0,
50 observations: 0,
51 replayability: StreamReplayability::NotReplayable,
52 })
53 }
54
55 pub const fn with_replayability(
57 chunks: &'fixture [&'fixture [u8]],
58 replayability: StreamReplayability<'fixture>,
59 ) -> Result<Self, StreamFixtureError> {
60 if chunks.len() > MAX_STREAM_FIXTURE_CHUNKS {
61 return Err(StreamFixtureError::TooManyChunks);
62 }
63 Ok(Self {
64 chunks,
65 index: 0,
66 observations: 0,
67 replayability,
68 })
69 }
70
71 #[must_use]
73 pub const fn observations(&self) -> usize {
74 self.observations
75 }
76
77 fn read(&mut self, output: &mut [u8]) -> Result<StreamRead, StreamFixtureError> {
78 self.observations = self.observations.saturating_add(1);
79 let Some(chunk) = self.chunks.get(self.index) else {
80 return Ok(StreamRead::End);
81 };
82 let target = output
83 .get_mut(..chunk.len())
84 .ok_or(StreamFixtureError::SourceScratchTooSmall)?;
85 target.copy_from_slice(chunk);
86 self.index = self.index.saturating_add(1);
87 Ok(StreamRead::Chunk(chunk.len()))
88 }
89}
90
91impl BlockingStreamSource for StreamFixtureSource<'_> {
92 type Error = StreamFixtureError;
93
94 fn replayability(&self) -> StreamReplayability<'_> {
95 self.replayability
96 }
97
98 fn read_chunk(&mut self, output: &mut [u8]) -> Result<StreamRead, Self::Error> {
99 self.read(output)
100 }
101}
102
103impl AsyncStreamSource for StreamFixtureSource<'_> {
104 type Error = StreamFixtureError;
105
106 fn replayability(&self) -> StreamReplayability<'_> {
107 self.replayability
108 }
109
110 async fn read_chunk<'operation>(
111 &'operation mut self,
112 output: &'operation mut [u8],
113 ) -> Result<StreamRead, Self::Error> {
114 self.read(output)
115 }
116}
117
118pub struct StreamFixtureSink<'storage> {
120 output: &'storage mut [u8],
121 initialized_len: usize,
122 max_write_bytes: usize,
123 writes: usize,
124 committed: bool,
125 aborted: Option<StreamPartialState>,
126}
127
128impl<'storage> StreamFixtureSink<'storage> {
129 pub fn new(
131 output: &'storage mut [u8],
132 max_write_bytes: usize,
133 ) -> Result<Self, StreamFixtureError> {
134 sanitize_bytes(output);
135 if max_write_bytes == 0 {
136 return Err(StreamFixtureError::ZeroWriteLimit);
137 }
138 Ok(Self {
139 output,
140 initialized_len: 0,
141 max_write_bytes,
142 writes: 0,
143 committed: false,
144 aborted: None,
145 })
146 }
147
148 #[must_use]
150 pub fn bytes(&self) -> &[u8] {
151 self.output.get(..self.initialized_len).unwrap_or_default()
152 }
153
154 #[must_use]
156 pub const fn writes(&self) -> usize {
157 self.writes
158 }
159
160 #[must_use]
162 pub const fn is_committed(&self) -> bool {
163 self.committed
164 }
165
166 #[must_use]
168 pub const fn aborted_with(&self) -> Option<StreamPartialState> {
169 self.aborted
170 }
171
172 fn write(&mut self, input: &[u8]) -> Result<usize, StreamFixtureError> {
173 let accepted = core::cmp::min(input.len(), self.max_write_bytes);
174 let end = self
175 .initialized_len
176 .checked_add(accepted)
177 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
178 let target = self
179 .output
180 .get_mut(self.initialized_len..end)
181 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
182 let source = input
183 .get(..accepted)
184 .ok_or(StreamFixtureError::SinkStorageTooSmall)?;
185 target.copy_from_slice(source);
186 self.initialized_len = end;
187 self.writes = self.writes.saturating_add(1);
188 Ok(accepted)
189 }
190
191 fn commit_inner(&mut self) {
192 self.committed = true;
193 }
194
195 fn abort_inner(&mut self, partial: StreamPartialState) {
196 self.aborted = Some(partial);
197 if matches!(partial, StreamPartialState::RollbackRequired) {
198 sanitize_bytes(self.output);
199 self.initialized_len = 0;
200 }
201 }
202}
203
204impl BlockingStreamSink for StreamFixtureSink<'_> {
205 type Error = StreamFixtureError;
206
207 fn write_chunk(&mut self, input: &[u8]) -> Result<usize, Self::Error> {
208 self.write(input)
209 }
210
211 fn commit(&mut self) -> Result<(), Self::Error> {
212 self.commit_inner();
213 Ok(())
214 }
215
216 fn abort(&mut self, partial: StreamPartialState) {
217 self.abort_inner(partial);
218 }
219}
220
221impl AsyncStreamSink for StreamFixtureSink<'_> {
222 type Error = StreamFixtureError;
223
224 async fn write_chunk<'operation>(
225 &'operation mut self,
226 input: &'operation [u8],
227 ) -> Result<usize, Self::Error> {
228 self.write(input)
229 }
230
231 async fn commit(&mut self) -> Result<(), Self::Error> {
232 self.commit_inner();
233 Ok(())
234 }
235
236 fn abort(&mut self, partial: StreamPartialState) {
237 self.abort_inner(partial);
238 }
239}