Skip to main content

fast_pull/core/
mock.rs

1//! A deterministic in-memory [`Puller`](crate::Puller) for tests, plus a helper
2//! to build mock payloads.
3
4use crate::{ProgressEntry, PullResult, PullStream, Puller};
5use futures::stream;
6use std::{sync::Arc, vec::Vec};
7
8/// Build a deterministic byte array for mock testing.
9///
10/// The payload is a xorshift64 keystream, which has no short period: distinct
11/// equal-length windows of the result differ. This matters because the usual
12/// end-to-end check is `assert_eq!(downloaded, build_mock_data(size))`, and a
13/// periodic payload makes that assertion blind to whole blocks written at the wrong
14/// offset or swapped between workers.
15///
16/// The result is prefix-stable: `build_mock_data(n)` is the first `n` bytes of
17/// `build_mock_data(m)` for every `m >= n`, so a range of the full array can be used
18/// as the expected value for a partial download.
19#[must_use]
20pub fn build_mock_data(size: usize) -> Vec<u8> {
21    let mut out = Vec::with_capacity(size.next_multiple_of(8));
22    let mut state: u64 = 0x2545_F491_4F6C_DD1D;
23    while out.len() < size {
24        state ^= state << 13;
25        state ^= state >> 7;
26        state ^= state << 17;
27        out.extend_from_slice(&state.to_le_bytes());
28    }
29    out.truncate(size);
30    out
31}
32
33/// A [`Puller`] implementation backed by an in-memory byte slice, used for testing.
34#[derive(Debug, Clone)]
35pub struct MockPuller(pub Arc<[u8]>);
36impl MockPuller {
37    /// Wrap a byte slice as an in-memory puller.
38    #[must_use]
39    pub fn new(data: &[u8]) -> Self {
40        Self(Arc::from(data))
41    }
42}
43impl Puller for MockPuller {
44    type Error = std::convert::Infallible;
45    fn pull(
46        &mut self,
47        range: Option<&ProgressEntry>,
48    ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> {
49        let data = match range {
50            #[allow(clippy::cast_possible_truncation)]
51            Some(r) => &self.0[r.start as usize..r.end as usize],
52            None => &self.0,
53        };
54        std::future::ready(Ok(stream::iter(
55            data.chunks(2).map(|c| Ok(c.iter().copied().collect())),
56        )))
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use futures::TryStreamExt;
64
65    async fn pull_all(puller: &mut MockPuller, range: Option<&ProgressEntry>) -> Vec<u8> {
66        let mut stream = puller.pull(range).await.unwrap();
67        let mut got = Vec::new();
68        while let Some(chunk) = stream.try_next().await.unwrap() {
69            got.extend_from_slice(&chunk[..]);
70        }
71        got
72    }
73
74    // The mock must yield exactly the requested byte range, reassembled in order,
75    // regardless of its internal 2-byte chunking.
76    #[tokio::test]
77    async fn mock_puller_yields_exact_range_in_order() {
78        let data = build_mock_data(30);
79        let mut puller = MockPuller::new(&data);
80        let got = pull_all(&mut puller, Some(&(10..20))).await;
81        assert_eq!(got, data[10..20]);
82    }
83
84    // `None` requests the entire source.
85    #[tokio::test]
86    async fn mock_puller_none_yields_full_source() {
87        let data = build_mock_data(17);
88        let mut puller = MockPuller::new(&data);
89        let got = pull_all(&mut puller, None).await;
90        assert_eq!(got, data);
91    }
92
93    // Callers slice the full array to build the expected value for a partial range,
94    // so a shorter build must be a prefix of a longer one, and repeated calls must
95    // agree.
96    #[test]
97    fn build_mock_data_is_deterministic_and_prefix_stable() {
98        let long = build_mock_data(1024);
99        for size in [0, 1, 7, 8, 9, 300, 1024] {
100            let short = build_mock_data(size);
101            assert_eq!(short.len(), size);
102            assert_eq!(short, build_mock_data(size));
103            assert_eq!(short[..], long[..size]);
104        }
105    }
106
107    // End-to-end assertions compare the whole payload, so they can only detect a
108    // block written at the wrong offset if no two windows of the payload are equal.
109    // A periodic pattern such as `i % 256` fails this and silently accepts any swap
110    // of two 256-aligned blocks.
111    #[test]
112    fn build_mock_data_has_no_repeating_window() {
113        let data = build_mock_data(64 * 1024);
114        let mut seen = std::collections::HashSet::with_capacity(data.len());
115        for window in data.windows(8) {
116            assert!(seen.insert(window), "window {window:?} occurs twice");
117        }
118    }
119}