use std::io::{self, Read};
#[derive(Debug, Clone)]
pub enum Data {
Bytes(Vec<u8>),
Retry,
}
pub trait HintReadterator {
type Item;
type Error;
fn next_bytes(&mut self, size_hint: Option<usize>) -> Option<Result<Self::Item, Self::Error>>;
}
pub struct BatchReader<R: Read> {
reader: R,
batch_size: usize,
eof_on_zero_read: bool,
received_data: bool,
max_consecutive_retries: usize,
consecutive_retries: usize,
done: bool,
}
impl<R: Read> BatchReader<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
batch_size: 512,
received_data: false,
eof_on_zero_read: true,
max_consecutive_retries: 100,
consecutive_retries: 0,
done: false,
}
}
#[must_use]
pub fn batch_size(mut self, size: usize) -> Self {
self.batch_size = size;
self
}
#[must_use]
pub fn eof_on_zero_read(mut self, eof: bool) -> Self {
self.eof_on_zero_read = eof;
self
}
#[must_use]
pub fn max_consecutive_retries(mut self, max: usize) -> Self {
self.max_consecutive_retries = max;
self
}
}
impl<R: Read> Iterator for BatchReader<R> {
type Item = Result<Data, io::Error>;
fn next(&mut self) -> Option<Self::Item> {
let _span = tracing::span!(tracing::Level::TRACE, "next").entered();
self.next_bytes(None)
}
}
impl<R: Read> HintReadterator for BatchReader<R> {
type Item = Data;
type Error = io::Error;
fn next_bytes(&mut self, size_hint: Option<usize>) -> Option<Result<Self::Item, Self::Error>> {
let _span =
tracing::span!(tracing::Level::TRACE, "next_bytes", hint = ?size_hint).entered();
if self.done {
tracing::trace!("Reading is now considered done!");
return None;
}
if size_hint.is_some() && size_hint == Some(0) {
return None;
}
let target_size = size_hint.unwrap_or(self.batch_size);
tracing::trace!("Using target size for buffer: {}", &target_size);
let mut buf = vec![0u8; target_size];
match self.reader.read(&mut buf) {
Ok(0) => {
tracing::trace!("Zero bytes read occured");
if self.eof_on_zero_read {
tracing::trace!("Stream is now considered finished");
self.done = true;
None
} else {
self.consecutive_retries += 1;
if self.consecutive_retries > self.max_consecutive_retries {
self.done = true;
if self.received_data {
return None;
}
Some(Err(io::Error::new(
io::ErrorKind::TimedOut,
"max consecutive retries exceeded without progress",
)))
} else {
tracing::debug!("Sending retry");
Some(Ok(Data::Retry))
}
}
}
Ok(n) => {
tracing::trace!("Received data Bytes(len={})", n);
self.received_data = true;
self.consecutive_retries = 0;
buf.truncate(n);
Some(Ok(Data::Bytes(buf)))
}
Err(e)
if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
{
tracing::error!("Timeout/WouldBlock error received: {:?}", &e);
self.consecutive_retries += 1;
tracing::trace!(
"Checking consecutive errors (max={}): current={}",
&self.max_consecutive_retries,
self.consecutive_retries
);
if self.consecutive_retries > self.max_consecutive_retries {
self.done = true;
if self.received_data {
tracing::trace!("Finished reading, saw data, ending");
return None;
}
tracing::trace!(
"Problematic, max retries reached, returning exahaustion errors"
);
Some(Err(io::Error::new(
e.kind(),
"max consecutive retries exceeded without progress",
)))
} else {
Some(Ok(Data::Retry))
}
}
Err(e) => {
tracing::error!("Read error occured: {:?}", &e);
self.done = true;
Some(Err(e))
}
}
}
}
pub struct FullBodyReader<R: Read> {
inner: BatchReader<R>,
bytes_yielded: usize,
target_size: usize,
}
impl<R: Read> FullBodyReader<R> {
pub fn new(inner: BatchReader<R>, target_size: usize) -> Self {
Self {
inner,
bytes_yielded: 0,
target_size,
}
}
}
impl<R: Read> Iterator for FullBodyReader<R> {
type Item = Result<Data, io::Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.bytes_yielded >= self.target_size {
return None;
}
match self.inner.next() {
Some(Ok(Data::Bytes(bytes))) => {
self.bytes_yielded += bytes.len();
Some(Ok(Data::Bytes(bytes)))
}
other => other, }
}
}
pub struct EofReader<R: Read> {
inner: BatchReader<R>,
}
impl<R: Read> EofReader<R> {
pub fn new(inner: BatchReader<R>) -> Self {
Self { inner }
}
}
impl<R: Read> Iterator for EofReader<R> {
type Item = Result<Data, io::Error>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
pub struct BatchStreamReader<R: Read> {
inner: BatchReader<R>,
}
impl<R: Read> BatchStreamReader<R> {
pub fn new(inner: BatchReader<R>) -> Self {
Self { inner }
}
}
impl<R: Read + Send> Iterator for BatchStreamReader<R> {
type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
fn next(&mut self) -> Option<Self::Item> {
match self.inner.next() {
Some(Ok(data)) => Some(Ok(data)),
Some(Err(e)) => Some(Err(Box::new(e))),
None => None,
}
}
}
pub struct LimitedBatchStreamReader<R: Read> {
inner: BatchReader<R>,
bytes_yielded: usize,
byte_cap: usize,
}
impl<R: Read> LimitedBatchStreamReader<R> {
pub fn new(inner: BatchReader<R>, byte_cap: usize) -> Self {
Self {
inner,
bytes_yielded: 0,
byte_cap,
}
}
}
impl<R: Read + Send> Iterator for LimitedBatchStreamReader<R> {
type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
fn next(&mut self) -> Option<Self::Item> {
tracing::trace!("Pulling next data bytes from stream");
if self.bytes_yielded >= self.byte_cap {
tracing::trace!("Readed max allowed bytes, stopping");
return None;
}
let remaining = self.byte_cap - self.bytes_yielded;
tracing::trace!("Remaining bytes to read: {}", remaining);
match self.inner.next_bytes(Some(remaining)) {
Some(Ok(Data::Bytes(bytes))) => {
tracing::trace!("Readed new bytes size from reader: {}", bytes.len());
self.bytes_yielded += bytes.len();
Some(Ok(Data::Bytes(bytes)))
}
Some(Ok(data)) => Some(Ok(data)), Some(Err(e)) => Some(Err(Box::new(e))),
None => None,
}
}
}
pub struct EOFStreamReader<R: Read> {
inner: BatchReader<R>,
}
impl<R: Read> EOFStreamReader<R> {
pub fn new(inner: BatchReader<R>) -> Self {
Self { inner }
}
}
impl<R: Read + Send> Iterator for EOFStreamReader<R> {
type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
fn next(&mut self) -> Option<Self::Item> {
self.inner
.next()
.map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error + 'static>))
}
}
pub struct LimitedEOFStreamReader<R: Read> {
inner: BatchReader<R>,
bytes_yielded: usize,
byte_cap: usize,
}
impl<R: Read> LimitedEOFStreamReader<R> {
pub fn new(inner: BatchReader<R>, byte_cap: usize) -> Self {
Self {
inner,
bytes_yielded: 0,
byte_cap,
}
}
}
impl<R: Read + Send> Iterator for LimitedEOFStreamReader<R> {
type Item = Result<Data, Box<dyn std::error::Error + 'static>>;
fn next(&mut self) -> Option<Self::Item> {
if self.bytes_yielded > self.byte_cap {
tracing::error!(
"body size {} exceeds max {}",
self.bytes_yielded,
self.byte_cap
);
return Some(Err(Box::new(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"body size {} exceeds max {}",
self.bytes_yielded, self.byte_cap
),
))));
}
let remaining = self.byte_cap - self.bytes_yielded;
tracing::trace!(remaining = %remaining, "Remaining bytes to read: {}", &remaining);
match self.inner.next_bytes(Some(remaining)) {
Some(Ok(Data::Bytes(bytes))) => {
self.bytes_yielded += bytes.len();
if self.bytes_yielded > self.byte_cap {
tracing::error!(
"body size {} exceeds max {}",
self.bytes_yielded,
self.byte_cap
);
return None;
}
Some(Ok(Data::Bytes(bytes)))
}
Some(Ok(Data::Retry)) => Some(Ok(Data::Retry)),
Some(Err(e)) => Some(Err(Box::new(e) as Box<dyn std::error::Error + 'static>)),
None => None,
}
}
}
use std::marker::PhantomData;
pub struct DataBytesIterator<I, E> {
inner: I,
_marker: PhantomData<E>,
}
impl<I, E> DataBytesIterator<I, E> {
pub fn new(inner: I) -> Self {
Self {
inner,
_marker: PhantomData,
}
}
}
impl<I, E> Iterator for DataBytesIterator<I, E>
where
I: Iterator<Item = Result<Data, E>>,
{
type Item = Result<Vec<u8>, E>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.inner.next() {
Some(Ok(Data::Retry)) => continue, Some(Ok(Data::Bytes(bytes))) => return Some(Ok(bytes)),
Some(Err(e)) => return Some(Err(e)),
None => return None,
}
}
}
}
pub trait IntoDataBytes<E> {
fn into_bytes(self) -> DataBytesIterator<Self, E>
where
Self: Iterator<Item = Result<Data, E>> + Sized,
{
DataBytesIterator::new(self)
}
}
impl<I, E> IntoDataBytes<E> for I where I: Iterator<Item = Result<Data, E>> {}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use super::*;
use std::io::Cursor;
use tracing_test::traced_test;
#[test]
fn batch_reader_normal_reads() {
let data = b"hello world";
let reader = BatchReader::new(Cursor::new(data.to_vec())).batch_size(5);
let results: Vec<_> = reader.collect();
assert_eq!(results.len(), 3);
for r in &results {
assert!(r.is_ok());
match r.as_ref().unwrap() {
Data::Bytes(b) => assert!(!b.is_empty()),
Data::Retry => panic!("unexpected retry"),
}
}
}
#[test]
fn batch_reader_empty_source_eof() {
let reader = BatchReader::new(Cursor::new(Vec::<u8>::new()));
let results: Vec<_> = reader.collect();
assert!(results.is_empty());
}
#[test]
fn batch_reader_would_block_handling() {
struct WouldBlockReader {
calls: usize,
data: Vec<u8>,
pos: usize,
}
impl Read for WouldBlockReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls % 2 == 1 && self.pos < self.data.len() {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "would block"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let reader = BatchReader::new(WouldBlockReader {
calls: 0,
data: b"test".to_vec(),
pos: 0,
})
.batch_size(4);
let results: Vec<_> = reader.collect();
let mut got_retry = false;
let mut got_bytes = false;
for r in &results {
match r.as_ref().unwrap() {
Data::Retry => got_retry = true,
Data::Bytes(_) => got_bytes = true,
}
}
assert!(got_retry);
assert!(got_bytes);
}
#[test]
fn batch_reader_retry_limit_exceeded() {
struct AlwaysWouldBlock;
impl Read for AlwaysWouldBlock {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"))
}
}
let reader = BatchReader::new(AlwaysWouldBlock).max_consecutive_retries(3);
let results: Vec<_> = reader.collect();
assert_eq!(results.len(), 4);
assert!(results.last().unwrap().is_err());
}
#[test]
fn batch_reader_eof_on_zero_read_false() {
struct ZeroThenData {
calls: usize,
}
impl Read for ZeroThenData {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls <= 2 {
Ok(0)
} else if self.calls == 3 {
buf[0] = b'x';
Ok(1)
} else {
Ok(0)
}
}
}
let reader = BatchReader::new(ZeroThenData { calls: 0 })
.eof_on_zero_read(false)
.max_consecutive_retries(5);
let results: Vec<_> = reader.take(4).collect();
assert_eq!(results.len(), 4);
assert!(matches!(results[0].as_ref().unwrap(), Data::Retry));
assert!(matches!(results[1].as_ref().unwrap(), Data::Retry));
assert!(matches!(results[2].as_ref().unwrap(), Data::Bytes(_)));
}
#[test]
fn full_body_reader_complete_read() {
let data = b"hello world";
let reader = FullBodyReader::new(BatchReader::new(Cursor::new(data.to_vec())), data.len());
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn full_body_reader_partial_reads() {
struct OneByteReader {
data: Vec<u8>,
pos: usize,
}
impl Read for OneByteReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.pos >= self.data.len() {
return Ok(0);
}
buf[0] = self.data[self.pos];
self.pos += 1;
Ok(1)
}
}
let data = b"hello";
let reader = FullBodyReader::new(
BatchReader::new(OneByteReader {
data: data.to_vec(),
pos: 0,
}),
data.len(),
);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn full_body_reader_retry_handling() {
struct RetryReader {
data: Vec<u8>,
pos: usize,
calls: usize,
}
impl Read for RetryReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls.is_multiple_of(3) {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let data = b"hello world test";
let reader = FullBodyReader::new(
BatchReader::new(RetryReader {
data: data.to_vec(),
pos: 0,
calls: 0,
}),
data.len(),
);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn full_body_reader_unexpected_eof() {
let data = b"hi";
let reader = FullBodyReader::new(BatchReader::new(Cursor::new(data.to_vec())), 10);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_ne!(collected.len(), 10);
assert_eq!(collected, data);
}
#[test]
fn full_body_reader_retry_limit_exceeded() {
struct AlwaysWouldBlock;
impl Read for AlwaysWouldBlock {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"))
}
}
let reader = FullBodyReader::new(
BatchReader::new(AlwaysWouldBlock).max_consecutive_retries(3),
10,
);
let results: Vec<_> = reader.collect();
assert!(results.iter().any(|r| r.is_err()));
}
#[test]
fn batch_stream_reader_exposes_retries() {
struct AlternatingReader {
data: Vec<u8>,
pos: usize,
calls: usize,
}
impl Read for AlternatingReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls % 2 == 1 && self.pos < self.data.len() {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let batch = BatchReader::new(AlternatingReader {
data: b"hello".to_vec(),
pos: 0,
calls: 0,
})
.batch_size(5);
let stream = BatchStreamReader::new(batch);
let mut got_retry = false;
let mut got_bytes = false;
for result in stream {
match result.unwrap() {
Data::Retry => got_retry = true,
Data::Bytes(_) => got_bytes = true,
}
}
assert!(got_retry, "Should expose Data::Retry");
assert!(got_bytes, "Should expose Data::Bytes");
}
#[test]
fn batch_stream_reader_propagates_errors() {
struct ErrorReader;
impl Read for ErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::ConnectionReset, "reset"))
}
}
let batch = BatchReader::new(ErrorReader);
let stream = BatchStreamReader::new(batch);
let results: Vec<_> = stream.collect();
assert_eq!(results.len(), 1);
assert!(results[0].is_err());
}
#[test]
fn batch_stream_reader_empty_source() {
let batch = BatchReader::new(Cursor::new(Vec::<u8>::new()));
let stream = BatchStreamReader::new(batch);
let results: Vec<_> = stream.collect();
assert!(results.is_empty());
}
#[test]
fn eof_reader_complete_read() {
let data = b"hello world test data";
let batch = BatchReader::new(Cursor::new(data.to_vec()))
.batch_size(512)
.max_consecutive_retries(100);
let reader = EofReader::new(batch);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
#[traced_test]
fn eof_reader_with_max_size() {
let data = b"hello world";
let batch = BatchReader::new(Cursor::new(data.to_vec()))
.batch_size(512)
.max_consecutive_retries(100);
let reader = LimitedEOFStreamReader::new(batch, 20);
let collected: Vec<u8> = reader
.filter_map(|r| {
tracing::info!("Filtering first filter map with: {:?}", &r);
r.ok()
})
.filter_map(|d| {
tracing::info!("Filtering results with: {:?}", &d);
match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
}
})
.flatten()
.collect();
assert_eq!(collected, data);
println!("Results generarted from first reader: {:?}", &collected);
let batch = BatchReader::new(Cursor::new(data.to_vec()))
.batch_size(512)
.max_consecutive_retries(100);
let reader = LimitedEOFStreamReader::new(batch, 5);
let results: Vec<Result<Data, Box<dyn std::error::Error + 'static>>> = reader.collect();
println!("Results generarted from reader: {:?}", &results);
assert!(!results.is_empty());
}
#[test]
fn eof_reader_partial_reads() {
struct OneByteReader {
data: Vec<u8>,
pos: usize,
}
impl Read for OneByteReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.pos >= self.data.len() {
return Ok(0);
}
buf[0] = self.data[self.pos];
self.pos += 1;
Ok(1)
}
}
let data = b"hello";
let batch = BatchReader::new(OneByteReader {
data: data.to_vec(),
pos: 0,
})
.batch_size(512)
.max_consecutive_retries(100);
let reader = EofReader::new(batch);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn eof_reader_retry_handling() {
struct RetryReader {
data: Vec<u8>,
pos: usize,
calls: usize,
}
impl Read for RetryReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls.is_multiple_of(3) {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let data = b"hello world test";
let batch = BatchReader::new(RetryReader {
data: data.to_vec(),
pos: 0,
calls: 0,
})
.batch_size(512)
.max_consecutive_retries(100);
let reader = EofReader::new(batch);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn eof_reader_retry_limit_exceeded() {
struct AlwaysWouldBlock;
impl Read for AlwaysWouldBlock {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"))
}
}
let batch = BatchReader::new(AlwaysWouldBlock).max_consecutive_retries(3);
let reader = EofReader::new(batch);
let results: Vec<_> = reader.collect();
assert!(results.iter().any(|r| r.is_err()));
}
#[test]
fn eof_reader_empty_source() {
let batch = BatchReader::new(Cursor::new(Vec::<u8>::new()));
let reader = EofReader::new(batch);
let collected: Vec<u8> = reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert!(collected.is_empty());
}
#[test]
fn data_bytes_iterator_filters_retries() {
struct AlternatingRetryReader {
data: Vec<u8>,
pos: usize,
calls: usize,
}
impl Read for AlternatingRetryReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls % 2 == 1 && self.pos < self.data.len() {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let batch = BatchReader::new(AlternatingRetryReader {
data: b"test".to_vec(),
pos: 0,
calls: 0,
})
.batch_size(5);
let data_stream = BatchStreamReader::new(batch);
let wrapped = DataBytesIterator::new(data_stream);
for result in wrapped {
assert!(result.is_ok());
}
}
#[test]
fn data_bytes_iterator_propagates_errors() {
struct ErrorReader;
impl Read for ErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::ConnectionReset, "reset"))
}
}
let batch = BatchReader::new(ErrorReader);
let data_stream = BatchStreamReader::new(batch);
let mut wrapped = DataBytesIterator::new(data_stream);
assert!(wrapped.next().unwrap().is_err());
}
#[test]
fn data_bytes_iterator_empty_source() {
let batch = BatchReader::new(Cursor::new(Vec::<u8>::new()));
let data_stream = BatchStreamReader::new(batch);
let mut wrapped = DataBytesIterator::new(data_stream);
assert!(wrapped.next().is_none());
}
#[test]
fn limited_batch_stream_reader_stops_at_cap() {
let data = b"hello world";
let batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(5);
let mut limited = LimitedBatchStreamReader::new(batch, 5);
let first = limited.next().unwrap().unwrap();
assert!(matches!(first, Data::Bytes(ref b) if b == b"hello"));
assert!(limited.next().is_none());
}
#[test]
fn limited_batch_stream_reader_exposes_retries() {
struct RetryThenDataReader {
data: Vec<u8>,
pos: usize,
calls: usize,
}
impl Read for RetryThenDataReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.calls += 1;
if self.calls == 1 {
return Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"));
}
if self.pos >= self.data.len() {
return Ok(0);
}
let n = std::cmp::min(buf.len(), self.data.len() - self.pos);
buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
self.pos += n;
Ok(n)
}
}
let batch = BatchReader::new(RetryThenDataReader {
data: b"test".to_vec(),
pos: 0,
calls: 0,
})
.batch_size(100);
let mut limited = LimitedBatchStreamReader::new(batch, 100);
assert!(matches!(limited.next(), Some(Ok(Data::Retry))));
assert!(matches!(limited.next(), Some(Ok(Data::Bytes(_)))));
}
#[test]
fn eof_stream_reader_reads_until_eof() {
let data = b"hello world";
let batch = BatchReader::new(Cursor::new(data.to_vec()))
.batch_size(512)
.eof_on_zero_read(true);
let eof_reader = EOFStreamReader::new(batch);
let collected: Vec<u8> = eof_reader
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn eof_stream_reader_exposes_retries() {
struct RetryReader;
impl Read for RetryReader {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::WouldBlock, "blocked"))
}
}
let batch = BatchReader::new(RetryReader).eof_on_zero_read(false);
let mut eof_reader = EOFStreamReader::new(batch);
assert!(matches!(eof_reader.next(), Some(Ok(Data::Retry))));
}
#[test]
fn limited_eof_stream_reader_enforces_max() {
let data = b"hello world this is long";
let batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(10);
let limited = LimitedEOFStreamReader::new(batch, 10);
let mut collected = Vec::new();
for result in limited {
match result {
Ok(Data::Bytes(bytes)) => collected.extend(bytes),
Ok(Data::Retry) => continue,
Err(e) => {
panic!("got an error form reader and should not occur: {:?}", e,);
}
}
}
assert!(!collected.is_empty())
}
#[test]
fn limited_eof_stream_reader_under_limit() {
let data = b"short";
let batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(100);
let limited = LimitedEOFStreamReader::new(batch, 100);
let collected: Vec<u8> = limited
.filter_map(|r| r.ok())
.filter_map(|d| match d {
Data::Bytes(b) => Some(b),
Data::Retry => None,
})
.flatten()
.collect();
assert_eq!(collected, data);
}
#[test]
fn limited_batch_stream_reader_hint_prevents_overshoot() {
let data = b"hello world";
let batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(100);
let limited = LimitedBatchStreamReader::new(batch, 7);
let mut collected = Vec::new();
for result in limited {
match result {
Ok(Data::Bytes(bytes)) => collected.extend(bytes),
Ok(Data::Retry) => continue,
Err(e) => panic!("unexpected error: {e}"),
}
}
assert_eq!(
collected,
&data[..7],
"should only read 7 bytes, got {} bytes: {:?}",
collected.len(),
std::str::from_utf8(&collected)
);
}
#[test]
fn limited_eof_stream_reader_hint_prevents_overshoot() {
let data = b"hello world";
let batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(100);
let limited = LimitedEOFStreamReader::new(batch, 7);
let mut collected = Vec::new();
for result in limited {
match result {
Ok(Data::Bytes(bytes)) => collected.extend(bytes),
Ok(Data::Retry) => continue,
Err(e) => {
assert!(
e.to_string().contains("exceeds"),
"expected overshoot error, got: {e}"
);
break;
}
}
}
assert_eq!(
collected.len(),
7,
"should read exactly 7 bytes, got {}",
collected.len()
);
assert_eq!(collected, &data[..7]);
}
#[test]
fn batch_reader_next_bytes_respects_hint() {
let data = b"0123456789abcdef";
let mut batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(100);
let result = batch.next_bytes(Some(4)).unwrap().unwrap();
match result {
Data::Bytes(b) => assert_eq!(&b, b"0123", "hint=4 should read exactly 4 bytes"),
Data::Retry => panic!("expected bytes"),
}
let result = batch.next_bytes(Some(3)).unwrap().unwrap();
match result {
Data::Bytes(b) => assert_eq!(&b, b"456", "hint=3 should read exactly 3 bytes"),
Data::Retry => panic!("expected bytes"),
}
let result = batch.next_bytes(Some(1)).unwrap().unwrap();
match result {
Data::Bytes(b) => assert_eq!(&b, b"7", "hint=1 should read exactly 1 byte"),
Data::Retry => panic!("expected bytes"),
}
}
#[test]
fn batch_reader_next_bytes_none_uses_batch_size() {
let data = b"0123456789";
let mut batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(3);
let result = batch.next_bytes(None).unwrap().unwrap();
match result {
Data::Bytes(b) => assert_eq!(&b, b"012", "None hint should use batch_size=3"),
Data::Retry => panic!("expected bytes"),
}
let result = batch.next_bytes(None).unwrap().unwrap();
match result {
Data::Bytes(b) => assert_eq!(&b, b"345", "second call should use batch_size=3 again"),
Data::Retry => panic!("expected bytes"),
}
}
#[test]
fn batch_reader_next_bytes_zero_hint_returns_none() {
let data = b"hello world";
let mut batch = BatchReader::new(Cursor::new(data.to_vec())).batch_size(100);
let result = batch.next_bytes(Some(0));
assert!(result.is_none(), "hint=0 should return None immediately");
}
#[test]
fn into_data_bytes_extension() {
let batch = BatchReader::new(Cursor::new(b"test".to_vec()));
let stream = BatchStreamReader::new(batch);
let wrapped = stream.into_bytes();
for result in wrapped {
let bytes: Vec<u8> = result.unwrap();
assert!(!bytes.is_empty());
}
}
}