Skip to main content

proxima_primitives/pipe/
mutate.rs

1use alloc::sync::Arc;
2use alloc::vec::Vec;
3use portable_atomic::{AtomicU64, Ordering};
4
5pub use crate::pipe::capabilities::BytePayload;
6use crate::pipe::when::When;
7use bytes::Bytes;
8use serde::{Deserialize, Serialize};
9
10use crate::pipe::request::{Request, Response};
11
12/// The config-expressible seeded mutation kind set. Each kind is a pure
13/// function of `(seed, call_index, input bytes)` — same inputs, same output.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(tag = "kind", rename_all = "snake_case")]
16pub enum Mutation {
17    /// Flip `bits` randomly-chosen bit positions in the payload. A zero-length
18    /// payload or `bits == 0` is returned unchanged.
19    BitFlip {
20        /// How many bit positions to flip per application.
21        bits: u32,
22    },
23    /// Truncate the payload to a seeded length in `[0, len)`. Already-empty
24    /// payloads are returned unchanged.
25    Truncate,
26    /// Duplicate a seeded contiguous slice of the payload, inserting the copy
27    /// immediately after it (grows the payload). Empty payloads are unchanged.
28    Duplicate,
29}
30
31impl Mutation {
32    /// Deterministically mutate `input` for call `call_index`. Pure in
33    /// `(seed, call_index, input)`: the rng is seeded per call so the result
34    /// never depends on prior calls, ordering, or any global state.
35    #[must_use]
36    pub fn mutate(&self, seed: u64, call_index: u64, input: &[u8]) -> Bytes {
37        if input.is_empty() {
38            return Bytes::copy_from_slice(input);
39        }
40        let mut rng = fastrand::Rng::with_seed(seed.wrapping_add(call_index));
41        match self {
42            Mutation::BitFlip { bits } => bit_flip(&mut rng, input, *bits),
43            Mutation::Truncate => truncate(&mut rng, input),
44            Mutation::Duplicate => duplicate(&mut rng, input),
45        }
46    }
47}
48
49fn bit_flip(rng: &mut fastrand::Rng, input: &[u8], bits: u32) -> Bytes {
50    let mut buffer = input.to_vec();
51    let total_bits = (buffer.len() as u64).saturating_mul(8);
52    for _ in 0..bits {
53        let position = rng.u64(0..total_bits);
54        let byte_index = (position / 8) as usize;
55        let bit = (position % 8) as u8;
56        buffer[byte_index] ^= 1 << bit;
57    }
58    Bytes::from(buffer)
59}
60
61fn truncate(rng: &mut fastrand::Rng, input: &[u8]) -> Bytes {
62    let new_len = rng.usize(0..input.len());
63    Bytes::copy_from_slice(&input[..new_len])
64}
65
66fn duplicate(rng: &mut fastrand::Rng, input: &[u8]) -> Bytes {
67    let start = rng.usize(0..input.len());
68    let end = rng.usize(start + 1..=input.len());
69    let mut buffer = Vec::with_capacity(input.len() + (end - start));
70    buffer.extend_from_slice(&input[..end]);
71    buffer.extend_from_slice(&input[start..end]);
72    buffer.extend_from_slice(&input[end..]);
73    Bytes::from(buffer)
74}
75
76/// A seeded mutation as a `Transform` op. Carries a per-op call counter so
77/// successive applications advance the deterministic mutation sequence.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct MutateOp {
80    #[serde(flatten)]
81    mutation: Mutation,
82    seed: u64,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    when: Option<When>,
85    #[serde(skip)]
86    calls: Arc<AtomicU64>,
87}
88
89impl PartialEq for MutateOp {
90    fn eq(&self, other: &Self) -> bool {
91        self.mutation == other.mutation && self.seed == other.seed && self.when == other.when
92    }
93}
94
95impl MutateOp {
96    /// Build a mutation op from a kind and a seed, starting its call counter at zero.
97    #[must_use]
98    pub fn new(mutation: Mutation, seed: u64) -> Self {
99        Self {
100            mutation,
101            seed,
102            when: None,
103            calls: Arc::new(AtomicU64::new(0)),
104        }
105    }
106
107    /// Gate the mutation behind a seeded stochastic [`When`].
108    #[must_use]
109    pub fn with_when(mut self, when: When) -> Self {
110        self.when = Some(when);
111        self
112    }
113
114    #[must_use]
115    pub fn mutation(&self) -> &Mutation {
116        &self.mutation
117    }
118
119    #[must_use]
120    pub fn seed(&self) -> u64 {
121        self.seed
122    }
123
124    /// Apply this op to a byte payload, advancing the call counter.
125    pub fn apply_to<Payload: BytePayload>(&self, mut payload: Payload) -> Payload {
126        let index = self.calls.fetch_add(1, Ordering::Relaxed);
127        if let Some(gate) = self.when
128            && !gate.fires(index)
129        {
130            return payload;
131        }
132        let mutated = self.mutation.mutate(self.seed, index, payload.bytes());
133        payload.set_bytes(mutated);
134        payload
135    }
136}
137
138// ── HTTP instantiation — Request/Response present their body bytes ───────────
139
140impl BytePayload for Request<Bytes> {
141    fn set_bytes(&mut self, bytes: Bytes) {
142        self.payload = bytes;
143    }
144
145    fn bytes(&self) -> &[u8] {
146        &self.payload
147    }
148}
149
150impl BytePayload for Response<Bytes> {
151    fn set_bytes(&mut self, bytes: Bytes) {
152        self.payload = bytes;
153    }
154
155    fn bytes(&self) -> &[u8] {
156        &self.payload
157    }
158}
159
160#[cfg(test)]
161#[allow(clippy::unwrap_used, clippy::expect_used)]
162mod tests {
163    use super::*;
164
165    const SAMPLE: &[u8] = b"the quick brown fox jumps over the lazy dog";
166
167    #[test]
168    fn same_seed_and_index_yields_identical_bytes() {
169        let mutation = Mutation::BitFlip { bits: 3 };
170        let first = mutation.mutate(0xABCD, 7, SAMPLE);
171        let second = mutation.mutate(0xABCD, 7, SAMPLE);
172        assert_eq!(
173            first, second,
174            "same (seed, index) reproduces the same mutation"
175        );
176    }
177
178    #[test]
179    fn bit_flip_changes_only_a_bounded_number_of_bits() {
180        let mutated = Mutation::BitFlip { bits: 2 }.mutate(1, 0, SAMPLE);
181        assert_eq!(mutated.len(), SAMPLE.len(), "bit flip preserves length");
182        let differing_bits: u32 = SAMPLE
183            .iter()
184            .zip(mutated.iter())
185            .map(|(original, changed)| (original ^ changed).count_ones())
186            .sum();
187        assert!(differing_bits >= 1, "at least one bit flips");
188        assert!(differing_bits <= 2, "no more than `bits` flips");
189    }
190
191    #[test]
192    fn truncate_produces_a_strictly_shorter_payload() {
193        let mutated = Mutation::Truncate.mutate(5, 0, SAMPLE);
194        assert!(
195            mutated.len() < SAMPLE.len(),
196            "truncate shortens the payload"
197        );
198        assert_eq!(
199            &mutated[..],
200            &SAMPLE[..mutated.len()],
201            "truncate keeps a prefix"
202        );
203    }
204
205    #[test]
206    fn duplicate_grows_the_payload() {
207        let mutated = Mutation::Duplicate.mutate(9, 0, SAMPLE);
208        assert!(mutated.len() > SAMPLE.len(), "duplicate grows the payload");
209    }
210
211    #[test]
212    fn empty_payload_is_returned_unchanged_for_every_kind() {
213        for mutation in [
214            Mutation::BitFlip { bits: 4 },
215            Mutation::Truncate,
216            Mutation::Duplicate,
217        ] {
218            let mutated = mutation.mutate(3, 0, b"");
219            assert!(
220                mutated.is_empty(),
221                "empty input stays empty under {mutation:?}"
222            );
223        }
224    }
225
226    #[test]
227    fn distinct_indices_diverge() {
228        let mutation = Mutation::BitFlip { bits: 4 };
229        let early = mutation.mutate(42, 0, SAMPLE);
230        let late = mutation.mutate(42, 1, SAMPLE);
231        assert_ne!(
232            early, late,
233            "different call indices walk different mutations"
234        );
235    }
236
237    #[test]
238    fn config_builder_round_trip_parity() {
239        let op = MutateOp::new(Mutation::BitFlip { bits: 5 }, 0xC0FFEE);
240        let json = serde_json::to_value(&op).expect("serialize");
241        let parsed: MutateOp = serde_json::from_value(json.clone()).expect("deserialize");
242        assert_eq!(
243            parsed, op,
244            "serde round-trip is lossless (ignoring live counter)"
245        );
246        assert_eq!(
247            json,
248            serde_json::json!({"kind": "bit_flip", "bits": 5, "seed": 0xC0FFEE_u64}),
249            "mutation flattens into a single tagged op object"
250        );
251    }
252
253    #[test]
254    fn truncate_and_duplicate_round_trip_through_config() {
255        for op in [
256            MutateOp::new(Mutation::Truncate, 11),
257            MutateOp::new(Mutation::Duplicate, 12),
258        ] {
259            let json = serde_json::to_value(&op).expect("serialize");
260            let parsed: MutateOp = serde_json::from_value(json).expect("deserialize");
261            assert_eq!(parsed, op, "every mutation kind round-trips through config");
262        }
263    }
264
265    #[test]
266    fn http_request_body_is_mutated_in_place() {
267        let request = Request::builder()
268            .method("POST")
269            .path("/")
270            .body(Bytes::from_static(SAMPLE))
271            .build()
272            .expect("builder");
273        let mutated = MutateOp::new(Mutation::Truncate, 7).apply_to(request);
274        assert!(
275            mutated.payload.len() < SAMPLE.len(),
276            "request body was truncated"
277        );
278    }
279
280    #[test]
281    fn http_response_body_is_mutated_in_place() {
282        let response = Response::ok(Bytes::from_static(SAMPLE));
283        let mutated = MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).apply_to(response);
284        assert_eq!(
285            mutated.payload.len(),
286            SAMPLE.len(),
287            "bit flip preserves response length"
288        );
289        assert_ne!(&mutated.payload[..], SAMPLE, "response body was corrupted");
290    }
291
292    #[test]
293    fn applying_an_op_twice_advances_the_sequence() {
294        let op = MutateOp::new(Mutation::BitFlip { bits: 2 }, 99);
295        let once = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
296        let twice = op.apply_to(Response::ok(once.payload.clone()));
297
298        let direct_first = Mutation::BitFlip { bits: 2 }.mutate(99, 0, SAMPLE);
299        let direct_second = Mutation::BitFlip { bits: 2 }.mutate(99, 1, &direct_first);
300        assert_eq!(
301            &once.payload[..],
302            &direct_first[..],
303            "first apply uses index 0"
304        );
305        assert_eq!(
306            &twice.payload[..],
307            &direct_second[..],
308            "second apply uses index 1"
309        );
310    }
311
312    #[test]
313    fn gated_op_mutates_only_on_a_fire() {
314        let always =
315            MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(1.0).seed(1));
316        let never =
317            MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(0.0).seed(1));
318
319        let corrupted = always.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
320        assert_ne!(
321            &corrupted.payload[..],
322            SAMPLE,
323            "a fired gate corrupts the body"
324        );
325
326        let untouched = never.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
327        assert_eq!(
328            &untouched.payload[..],
329            SAMPLE,
330            "a never-firing gate leaves the body untouched"
331        );
332    }
333
334    #[test]
335    fn gated_op_advances_the_counter_even_on_a_miss() {
336        let op = MutateOp::new(Mutation::BitFlip { bits: 3 }, 7).with_when(When::prob(1.0).seed(1));
337        let first = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
338        let second = op.apply_to(Response::ok(Bytes::from_static(SAMPLE)));
339        let at_zero = Mutation::BitFlip { bits: 3 }.mutate(7, 0, SAMPLE);
340        let at_one = Mutation::BitFlip { bits: 3 }.mutate(7, 1, SAMPLE);
341        assert_eq!(
342            &first.payload[..],
343            &at_zero[..],
344            "first apply walks index 0"
345        );
346        assert_eq!(
347            &second.payload[..],
348            &at_one[..],
349            "second apply walks index 1"
350        );
351    }
352
353    #[derive(Clone, PartialEq, Debug)]
354    struct Blob(Vec<u8>);
355
356    impl BytePayload for Blob {
357        fn set_bytes(&mut self, bytes: Bytes) {
358            self.0 = bytes.to_vec();
359        }
360
361        fn bytes(&self) -> &[u8] {
362            &self.0
363        }
364    }
365
366    #[test]
367    fn mutation_is_generic_over_a_non_http_byte_payload() {
368        let blob = MutateOp::new(Mutation::Duplicate, 0x5EED).apply_to(Blob(SAMPLE.to_vec()));
369        assert!(
370            blob.0.len() > SAMPLE.len(),
371            "non-http blob grows under Duplicate"
372        );
373
374        let again = MutateOp::new(Mutation::Duplicate, 0x5EED).apply_to(Blob(SAMPLE.to_vec()));
375        assert_eq!(blob, again, "same seed reproduces the same mutated blob");
376    }
377}