#![allow(dead_code)]
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::io::SeekFrom;
use fxtranslate::fetch::{DownloadOutcome, Fetch, FetchError, Sink};
#[derive(Clone, Copy)]
struct Fail {
after_bytes: usize,
retryable: bool,
}
#[derive(Default)]
pub struct MockFetch {
routes: HashMap<String, Vec<u8>>,
hits: RefCell<Vec<String>>,
ranges: RefCell<Vec<u64>>,
script: RefCell<VecDeque<Fail>>,
chunk_size: usize,
ignore_range: bool,
}
impl MockFetch {
pub fn new() -> MockFetch {
MockFetch::default()
}
pub fn route(mut self, url: &str, body: Vec<u8>) -> MockFetch {
self.routes.insert(url.to_string(), body);
self
}
pub fn fail_times(self, n: usize, retryable: bool) -> MockFetch {
let fail = Fail {
after_bytes: 0,
retryable,
};
self.script
.borrow_mut()
.extend(std::iter::repeat_n(fail, n));
self
}
pub fn fail_after(self, after_bytes: usize, retryable: bool) -> MockFetch {
self.script.borrow_mut().push_back(Fail {
after_bytes,
retryable,
});
self
}
pub fn chunk_size(mut self, size: usize) -> MockFetch {
self.chunk_size = size;
self
}
pub fn ignore_range(mut self) -> MockFetch {
self.ignore_range = true;
self
}
pub fn hit_count(&self) -> usize {
self.hits.borrow().len()
}
pub fn get_to_ranges(&self) -> Vec<u64> {
self.ranges.borrow().clone()
}
}
impl Fetch for MockFetch {
fn get(&self, url: &str) -> Result<Vec<u8>, String> {
self.hits.borrow_mut().push(url.to_string());
self.routes
.get(url)
.cloned()
.ok_or_else(|| format!("MockFetch: no route for {url}"))
}
fn get_to(
&self,
url: &str,
range_from: u64,
sink: &mut dyn Sink,
on_progress: &mut dyn FnMut(u64, Option<u64>),
) -> Result<DownloadOutcome, FetchError> {
self.hits.borrow_mut().push(url.to_string());
self.ranges.borrow_mut().push(range_from);
let body = self.routes.get(url).cloned().ok_or_else(|| FetchError {
message: format!("MockFetch: no route for {url}"),
retryable: false,
})?;
let resumed = range_from > 0 && !self.ignore_range && (range_from as usize) <= body.len();
let base = if resumed { range_from as usize } else { 0 };
sink.seek(SeekFrom::Start(base as u64))
.map_err(|e| FetchError {
message: e.to_string(),
retryable: false,
})?;
let total = Some(body.len() as u64);
let tail = &body[base..];
let step = if self.chunk_size == 0 {
tail.len().max(1)
} else {
self.chunk_size
};
let fail = self.script.borrow_mut().pop_front();
let mut written = 0usize;
let mut done = base as u64;
on_progress(done, total);
for chunk in tail.chunks(step) {
if let Some(f) = fail {
if written >= f.after_bytes {
return Err(FetchError {
message: format!("scripted failure for {url} after {written} B"),
retryable: f.retryable,
});
}
}
sink.write_all(chunk).map_err(|e| FetchError {
message: e.to_string(),
retryable: false,
})?;
written += chunk.len();
done += chunk.len() as u64;
on_progress(done, total);
}
Ok(DownloadOutcome { resumed, total })
}
}