umsh_mac/
test_support.rs

1//! Std-only simulated radio network and dummy platform components for tests.
2//!
3//! This module is intended for development, simulation, and examples. The
4//! simulated network types are useful outside unit tests, but the dummy crypto
5//! and RNG implementations are deliberately insecure and must not be used for
6//! real deployments.
7
8use core::convert::Infallible;
9use std::{
10    cell::{Cell, RefCell},
11    collections::VecDeque,
12    rc::Rc,
13    vec::Vec,
14};
15
16use core::task::{Context, Poll};
17use embedded_hal_async::delay::DelayNs;
18use rand::{Rng, TryCryptoRng, TryRng};
19use umsh_core::PublicKey;
20use umsh_crypto::{
21    AesCipher, AesProvider, CryptoEngine, NodeIdentity, Sha256Provider, SharedSecret,
22};
23use umsh_hal::{Clock, CounterStore, KeyValueStore, Radio, RxInfo, Snr, TxError, TxOptions};
24
25use crate::{
26    DEFAULT_ACKS, DEFAULT_CHANNELS, DEFAULT_DUP, DEFAULT_FRAME, DEFAULT_IDENTITIES, DEFAULT_PEERS,
27    DEFAULT_TX, Mac, OperatingPolicy, Platform, RepeaterConfig,
28};
29
30const DEFAULT_RSSI: i16 = -40;
31const DEFAULT_SNR: Snr = Snr::from_decibels(10);
32
33/// Convenience alias for a `Mac` instantiated with the simulated test components.
34pub type TestMac<
35    const IDENTITIES: usize = DEFAULT_IDENTITIES,
36    const PEERS: usize = DEFAULT_PEERS,
37    const CHANNELS: usize = DEFAULT_CHANNELS,
38    const ACKS: usize = DEFAULT_ACKS,
39    const TX: usize = DEFAULT_TX,
40    const FRAME: usize = DEFAULT_FRAME,
41    const DUP: usize = DEFAULT_DUP,
42> = Mac<TestPlatform, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP>;
43
44/// Convenience alias for a `Mac` instantiated with the modeled simulated components.
45pub type ModeledTestMac<
46    const IDENTITIES: usize = DEFAULT_IDENTITIES,
47    const PEERS: usize = DEFAULT_PEERS,
48    const CHANNELS: usize = DEFAULT_CHANNELS,
49    const ACKS: usize = DEFAULT_ACKS,
50    const TX: usize = DEFAULT_TX,
51    const FRAME: usize = DEFAULT_FRAME,
52    const DUP: usize = DEFAULT_DUP,
53> = Mac<ModeledTestPlatform, IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP>;
54
55/// Platform bundle for the simulated test components.
56pub struct TestPlatform;
57
58impl Platform for TestPlatform {
59    type Identity = DummyIdentity;
60    type Aes = DummyAes;
61    type Sha = DummySha;
62    type Radio = SimulatedRadio;
63    type Delay = DummyDelay;
64    type Clock = DummyClock;
65    type Rng = DummyRng;
66    type CounterStore = DummyCounterStore;
67    type KeyValueStore = DummyKeyValueStore;
68}
69
70/// Platform bundle for the modeled simulated components.
71pub struct ModeledTestPlatform;
72
73impl Platform for ModeledTestPlatform {
74    type Identity = DummyIdentity;
75    type Aes = DummyAes;
76    type Sha = DummySha;
77    type Radio = ModeledRadio;
78    type Delay = DummyDelay;
79    type Clock = DummyClock;
80    type Rng = DummyRng;
81    type CounterStore = DummyCounterStore;
82    type KeyValueStore = DummyKeyValueStore;
83}
84
85/// Create a test MAC coordinator using the simulated components.
86pub fn make_test_mac<
87    const IDENTITIES: usize,
88    const PEERS: usize,
89    const CHANNELS: usize,
90    const ACKS: usize,
91    const TX: usize,
92    const FRAME: usize,
93    const DUP: usize,
94>(
95    radio: SimulatedRadio,
96    clock: DummyClock,
97) -> TestMac<IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP> {
98    Mac::new(
99        radio,
100        CryptoEngine::new(DummyAes, DummySha),
101        clock,
102        DummyRng::default(),
103        DummyCounterStore,
104        RepeaterConfig::default(),
105        OperatingPolicy::default(),
106    )
107}
108
109/// Create a test MAC coordinator using the modeled simulated components.
110pub fn make_modeled_test_mac<
111    const IDENTITIES: usize,
112    const PEERS: usize,
113    const CHANNELS: usize,
114    const ACKS: usize,
115    const TX: usize,
116    const FRAME: usize,
117    const DUP: usize,
118>(
119    radio: ModeledRadio,
120    clock: DummyClock,
121) -> ModeledTestMac<IDENTITIES, PEERS, CHANNELS, ACKS, TX, FRAME, DUP> {
122    Mac::new(
123        radio,
124        CryptoEngine::new(DummyAes, DummySha),
125        clock,
126        DummyRng::default(),
127        DummyCounterStore,
128        RepeaterConfig::default(),
129        OperatingPolicy::default(),
130    )
131}
132
133/// Shared simulated radio topology and frame queues.
134#[derive(Clone)]
135pub struct SimulatedNetwork {
136    inner: Rc<RefCell<NetworkState>>,
137}
138
139struct NetworkState {
140    inboxes: Vec<VecDeque<QueuedFrame>>,
141    links: Vec<Vec<LinkProfile>>,
142}
143
144struct QueuedFrame {
145    data: Vec<u8>,
146    rssi: i16,
147    snr: Snr,
148}
149
150#[derive(Clone, Copy)]
151struct LinkProfile {
152    connected: bool,
153    rssi: i16,
154    snr: Snr,
155}
156
157impl Default for LinkProfile {
158    fn default() -> Self {
159        Self {
160            connected: false,
161            rssi: DEFAULT_RSSI,
162            snr: DEFAULT_SNR,
163        }
164    }
165}
166
167impl Default for SimulatedNetwork {
168    fn default() -> Self {
169        Self::new()
170    }
171}
172
173impl SimulatedNetwork {
174    /// Create an empty simulated network.
175    pub fn new() -> Self {
176        Self {
177            inner: Rc::new(RefCell::new(NetworkState {
178                inboxes: Vec::new(),
179                links: Vec::new(),
180            })),
181        }
182    }
183
184    /// Add a radio with default limits.
185    pub fn add_radio(&self) -> SimulatedRadio {
186        self.add_radio_with_config(256, 10)
187    }
188
189    /// Add a radio with explicit frame-size and airtime limits.
190    pub fn add_radio_with_config(&self, max_frame_size: usize, t_frame_ms: u32) -> SimulatedRadio {
191        let mut state = self.inner.borrow_mut();
192        let id = state.inboxes.len();
193        for row in &mut state.links {
194            row.push(LinkProfile::default());
195        }
196        state.inboxes.push(VecDeque::new());
197        state.links.push(vec![LinkProfile::default(); id + 1]);
198        SimulatedRadio {
199            network: self.clone(),
200            id,
201            max_frame_size,
202            t_frame_ms,
203        }
204    }
205
206    /// Connect `from` to `to` with default signal values.
207    pub fn connect(&self, from: usize, to: usize) {
208        self.set_link(from, to, true, DEFAULT_RSSI, DEFAULT_SNR);
209    }
210
211    /// Connect two radios in both directions.
212    pub fn connect_bidirectional(&self, a: usize, b: usize) {
213        self.connect(a, b);
214        self.connect(b, a);
215    }
216
217    /// Remove the directed link from `from` to `to`.
218    pub fn disconnect(&self, from: usize, to: usize) {
219        self.set_link(from, to, false, DEFAULT_RSSI, DEFAULT_SNR);
220    }
221
222    /// Configure one directed link with explicit signal values.
223    pub fn set_link(&self, from: usize, to: usize, connected: bool, rssi: i16, snr: Snr) {
224        let mut state = self.inner.borrow_mut();
225        let Some(row) = state.links.get_mut(from) else {
226            panic!("unknown simulated radio id {from}");
227        };
228        let Some(link) = row.get_mut(to) else {
229            panic!("unknown simulated radio id {to}");
230        };
231        *link = LinkProfile {
232            connected,
233            rssi,
234            snr,
235        };
236    }
237
238    /// Inject a frame directly into one radio's receive queue.
239    pub fn inject_frame(&self, to: usize, frame: &[u8]) {
240        self.inject_frame_with_info(to, frame, DEFAULT_RSSI, DEFAULT_SNR);
241    }
242
243    /// Inject a frame directly into one radio's receive queue with explicit metadata.
244    pub fn inject_frame_with_info(&self, to: usize, frame: &[u8], rssi: i16, snr: Snr) {
245        let mut state = self.inner.borrow_mut();
246        let Some(queue) = state.inboxes.get_mut(to) else {
247            panic!("unknown simulated radio id {to}");
248        };
249        queue.push_back(QueuedFrame {
250            data: frame.to_vec(),
251            rssi,
252            snr,
253        });
254    }
255
256    fn transmit(&self, from: usize, frame: &[u8]) {
257        let mut state = self.inner.borrow_mut();
258        let Some(row) = state.links.get(from) else {
259            panic!("unknown simulated radio id {from}");
260        };
261        let deliveries: Vec<(usize, i16, Snr)> = row
262            .iter()
263            .enumerate()
264            .filter_map(|(to, link)| link.connected.then_some((to, link.rssi, link.snr)))
265            .collect();
266        for (to, rssi, snr) in deliveries {
267            state.inboxes[to].push_back(QueuedFrame {
268                data: frame.to_vec(),
269                rssi,
270                snr,
271            });
272        }
273    }
274
275    fn receive(&self, id: usize, buf: &mut [u8]) -> RxInfo {
276        let mut state = self.inner.borrow_mut();
277        let Some(queue) = state.inboxes.get_mut(id) else {
278            panic!("unknown simulated radio id {id}");
279        };
280        let Some(frame) = queue.pop_front() else {
281            return RxInfo {
282                len: 0,
283                rssi: 0,
284                snr: Snr::from_decibels(0),
285                lqi: None,
286            };
287        };
288        let len = frame.data.len().min(buf.len());
289        buf[..len].copy_from_slice(&frame.data[..len]);
290        RxInfo {
291            len,
292            rssi: frame.rssi,
293            snr: frame.snr,
294            lqi: None,
295        }
296    }
297}
298
299/// Radio implementation backed by a [`SimulatedNetwork`].
300#[derive(Clone)]
301pub struct SimulatedRadio {
302    network: SimulatedNetwork,
303    id: usize,
304    max_frame_size: usize,
305    t_frame_ms: u32,
306}
307
308impl SimulatedRadio {
309    /// Return the radio's stable identifier within the simulated network.
310    pub fn id(&self) -> usize {
311        self.id
312    }
313}
314
315impl Radio for SimulatedRadio {
316    type Error = ();
317
318    async fn transmit(
319        &mut self,
320        data: &[u8],
321        _options: TxOptions,
322    ) -> Result<(), TxError<Self::Error>> {
323        self.network.transmit(self.id, data);
324        Ok(())
325    }
326
327    fn poll_receive(
328        &mut self,
329        _cx: &mut Context<'_>,
330        buf: &mut [u8],
331    ) -> Poll<Result<RxInfo, Self::Error>> {
332        let rx = self.network.receive(self.id, buf);
333        if rx.len == 0 {
334            Poll::Pending
335        } else {
336            Poll::Ready(Ok(rx))
337        }
338    }
339
340    fn max_frame_size(&self) -> usize {
341        self.max_frame_size
342    }
343
344    fn t_frame_ms(&self) -> u32 {
345        self.t_frame_ms
346    }
347}
348
349/// Link model used by [`ModeledNetwork`] to derive receive metadata and loss.
350#[derive(Clone, Copy, Debug, PartialEq, Eq)]
351pub struct ModeledLinkProfile {
352    pub connected: bool,
353    pub base_rssi: i16,
354    pub base_snr: Snr,
355    pub rssi_jitter_dbm: i16,
356    pub snr_jitter_centibels: i16,
357    pub propagation_delay_ms: u32,
358    pub drop_per_thousand: u16,
359}
360
361impl Default for ModeledLinkProfile {
362    fn default() -> Self {
363        Self {
364            connected: false,
365            base_rssi: DEFAULT_RSSI,
366            base_snr: DEFAULT_SNR,
367            rssi_jitter_dbm: 2,
368            snr_jitter_centibels: 10,
369            propagation_delay_ms: 0,
370            drop_per_thousand: 0,
371        }
372    }
373}
374
375impl ModeledLinkProfile {
376    /// Return a connected profile with the default modeled signal characteristics.
377    pub fn connected() -> Self {
378        Self {
379            connected: true,
380            ..Self::default()
381        }
382    }
383}
384
385/// Shared simulated network with scheduled delivery, jitter, packet loss, and coarse collision modeling.
386#[derive(Clone)]
387pub struct ModeledNetwork {
388    inner: Rc<RefCell<ModeledNetworkState>>,
389    clock: DummyClock,
390}
391
392struct ModeledNetworkState {
393    inboxes: Vec<VecDeque<QueuedFrame>>,
394    links: Vec<Vec<ModeledLinkProfile>>,
395    in_flight: Vec<InFlightTransmission>,
396    scheduled: Vec<ScheduledDelivery>,
397    rng: ModeledRng,
398}
399
400struct InFlightTransmission {
401    from: usize,
402    start_ms: u64,
403    end_ms: u64,
404}
405
406struct ScheduledDelivery {
407    to: usize,
408    available_at_ms: u64,
409    start_ms: u64,
410    end_ms: u64,
411    data: Vec<u8>,
412    rssi: i16,
413    snr: Snr,
414    collided: bool,
415}
416
417impl ModeledNetwork {
418    /// Create an empty modeled network with a shared clock starting at 0 ms.
419    pub fn new() -> Self {
420        Self::with_clock(DummyClock::new(0))
421    }
422
423    /// Create an empty modeled network using a caller-supplied shared clock.
424    pub fn with_clock(clock: DummyClock) -> Self {
425        Self {
426            inner: Rc::new(RefCell::new(ModeledNetworkState {
427                inboxes: Vec::new(),
428                links: Vec::new(),
429                in_flight: Vec::new(),
430                scheduled: Vec::new(),
431                rng: ModeledRng::new(0x554d_5348),
432            })),
433            clock,
434        }
435    }
436
437    /// Return the shared modeled clock.
438    pub fn clock(&self) -> DummyClock {
439        self.clock.clone()
440    }
441
442    /// Advance the shared simulation time.
443    pub fn advance_ms(&self, delta_ms: u64) {
444        self.clock.advance_ms(delta_ms);
445        self.promote_due_frames();
446    }
447
448    /// Override the deterministic RNG seed used for loss/jitter sampling.
449    pub fn reseed(&self, seed: u64) {
450        self.inner.borrow_mut().rng = ModeledRng::new(seed);
451    }
452
453    /// Add a modeled radio with default limits.
454    pub fn add_radio(&self) -> ModeledRadio {
455        self.add_radio_with_config(256, 100)
456    }
457
458    /// Add a modeled radio with explicit frame-size and airtime limits.
459    pub fn add_radio_with_config(&self, max_frame_size: usize, t_frame_ms: u32) -> ModeledRadio {
460        let mut state = self.inner.borrow_mut();
461        let id = state.inboxes.len();
462        for row in &mut state.links {
463            row.push(ModeledLinkProfile::default());
464        }
465        state.inboxes.push(VecDeque::new());
466        state
467            .links
468            .push(vec![ModeledLinkProfile::default(); id + 1]);
469        ModeledRadio {
470            network: self.clone(),
471            id,
472            max_frame_size,
473            t_frame_ms,
474        }
475    }
476
477    /// Connect `from` to `to` using the default modeled link profile.
478    pub fn connect(&self, from: usize, to: usize) {
479        self.set_link_profile(from, to, ModeledLinkProfile::connected());
480    }
481
482    /// Connect two radios in both directions using the default modeled link profile.
483    pub fn connect_bidirectional(&self, a: usize, b: usize) {
484        self.connect(a, b);
485        self.connect(b, a);
486    }
487
488    /// Remove the directed link from `from` to `to`.
489    pub fn disconnect(&self, from: usize, to: usize) {
490        self.set_link_profile(from, to, ModeledLinkProfile::default());
491    }
492
493    /// Configure one directed link with an explicit modeled profile.
494    pub fn set_link_profile(&self, from: usize, to: usize, profile: ModeledLinkProfile) {
495        let mut state = self.inner.borrow_mut();
496        let Some(row) = state.links.get_mut(from) else {
497            panic!("unknown modeled radio id {from}");
498        };
499        let Some(link) = row.get_mut(to) else {
500            panic!("unknown modeled radio id {to}");
501        };
502        *link = profile;
503    }
504
505    /// Return whether any future deliveries are still pending.
506    pub fn has_pending_deliveries(&self) -> bool {
507        !self.inner.borrow().scheduled.is_empty()
508    }
509
510    fn promote_due_frames(&self) {
511        let now_ms = self.clock.now_ms();
512        let mut state = self.inner.borrow_mut();
513        state.in_flight.retain(|tx| tx.end_ms > now_ms);
514        let mut index = 0usize;
515        while index < state.scheduled.len() {
516            if state.scheduled[index].available_at_ms > now_ms {
517                index += 1;
518                continue;
519            }
520            let delivery = state.scheduled.swap_remove(index);
521            if delivery.collided {
522                continue;
523            }
524            state.inboxes[delivery.to].push_back(QueuedFrame {
525                data: delivery.data,
526                rssi: delivery.rssi,
527                snr: delivery.snr,
528            });
529        }
530    }
531
532    fn channel_busy(&self, from: usize, now_ms: u64) -> bool {
533        let state = self.inner.borrow();
534        state.in_flight.iter().any(|tx| {
535            tx.from != from
536                && tx.start_ms <= now_ms
537                && now_ms < tx.end_ms
538                && state
539                    .links
540                    .get(tx.from)
541                    .and_then(|row| row.get(from))
542                    .map(|profile| profile.connected)
543                    .unwrap_or(false)
544        })
545    }
546
547    fn transmit(
548        &self,
549        from: usize,
550        frame: &[u8],
551        t_frame_ms: u32,
552        options: TxOptions,
553    ) -> Result<(), TxError<()>> {
554        self.promote_due_frames();
555        let now_ms = self.clock.now_ms();
556        if options.cad_timeout_ms.is_some() && self.channel_busy(from, now_ms) {
557            return Err(TxError::CadTimeout);
558        }
559
560        let mut state = self.inner.borrow_mut();
561        let Some(row) = state.links.get(from) else {
562            panic!("unknown modeled radio id {from}");
563        };
564        let start_ms = now_ms;
565        let end_ms = now_ms.saturating_add(u64::from(t_frame_ms));
566        let deliveries: Vec<(usize, ModeledLinkProfile)> = row
567            .iter()
568            .enumerate()
569            .filter_map(|(to, link)| link.connected.then_some((to, *link)))
570            .collect();
571
572        for (to, profile) in deliveries {
573            if profile.drop_per_thousand > 0
574                && state.rng.random_u16(1000) < profile.drop_per_thousand
575            {
576                continue;
577            }
578
579            let rssi_jitter = if profile.rssi_jitter_dbm > 0 {
580                state
581                    .rng
582                    .random_i16_inclusive(-profile.rssi_jitter_dbm, profile.rssi_jitter_dbm)
583            } else {
584                0
585            };
586            let snr_jitter = if profile.snr_jitter_centibels > 0 {
587                state.rng.random_i16_inclusive(
588                    -profile.snr_jitter_centibels,
589                    profile.snr_jitter_centibels,
590                )
591            } else {
592                0
593            };
594            let propagation_delay_ms = u64::from(profile.propagation_delay_ms);
595            let available_at_ms = end_ms.saturating_add(propagation_delay_ms);
596            let mut delivery = ScheduledDelivery {
597                to,
598                available_at_ms,
599                start_ms,
600                end_ms,
601                data: frame.to_vec(),
602                rssi: profile.base_rssi.saturating_add(rssi_jitter),
603                snr: Snr::from_centibels(
604                    profile.base_snr.as_centibels().saturating_add(snr_jitter),
605                ),
606                collided: false,
607            };
608
609            for existing in &mut state.scheduled {
610                if existing.to != to {
611                    continue;
612                }
613                if existing.start_ms < end_ms && start_ms < existing.end_ms {
614                    existing.collided = true;
615                    delivery.collided = true;
616                }
617            }
618
619            state.scheduled.push(delivery);
620        }
621
622        state.in_flight.push(InFlightTransmission {
623            from,
624            start_ms,
625            end_ms,
626        });
627        Ok(())
628    }
629
630    fn receive(&self, id: usize, buf: &mut [u8]) -> RxInfo {
631        self.promote_due_frames();
632        let mut state = self.inner.borrow_mut();
633        let Some(queue) = state.inboxes.get_mut(id) else {
634            panic!("unknown modeled radio id {id}");
635        };
636        let Some(frame) = queue.pop_front() else {
637            return RxInfo {
638                len: 0,
639                rssi: 0,
640                snr: Snr::from_decibels(0),
641                lqi: None,
642            };
643        };
644        let len = frame.data.len().min(buf.len());
645        buf[..len].copy_from_slice(&frame.data[..len]);
646        RxInfo {
647            len,
648            rssi: frame.rssi,
649            snr: frame.snr,
650            lqi: None,
651        }
652    }
653}
654
655impl Default for ModeledNetwork {
656    fn default() -> Self {
657        Self::new()
658    }
659}
660
661/// Radio implementation backed by a [`ModeledNetwork`].
662#[derive(Clone)]
663pub struct ModeledRadio {
664    network: ModeledNetwork,
665    id: usize,
666    max_frame_size: usize,
667    t_frame_ms: u32,
668}
669
670#[derive(Clone, Copy)]
671struct ModeledRng(u64);
672
673impl ModeledRng {
674    fn new(seed: u64) -> Self {
675        Self(seed.max(1))
676    }
677
678    fn next_u32(&mut self) -> u32 {
679        let mut x = self.0;
680        x ^= x << 13;
681        x ^= x >> 7;
682        x ^= x << 17;
683        self.0 = x.max(1);
684        x as u32
685    }
686
687    fn random_u16(&mut self, upper_exclusive: u16) -> u16 {
688        if upper_exclusive == 0 {
689            0
690        } else {
691            (self.next_u32() % u32::from(upper_exclusive)) as u16
692        }
693    }
694
695    fn random_i16_inclusive(&mut self, min: i16, max: i16) -> i16 {
696        if min >= max {
697            min
698        } else {
699            let span = (i32::from(max) - i32::from(min) + 1) as u32;
700            (i32::from(min) + (self.next_u32() % span) as i32) as i16
701        }
702    }
703}
704
705impl ModeledRadio {
706    /// Return the radio's stable identifier within the modeled network.
707    pub fn id(&self) -> usize {
708        self.id
709    }
710}
711
712impl Radio for ModeledRadio {
713    type Error = ();
714
715    async fn transmit(
716        &mut self,
717        data: &[u8],
718        options: TxOptions,
719    ) -> Result<(), TxError<Self::Error>> {
720        self.network
721            .transmit(self.id, data, self.t_frame_ms, options)
722    }
723
724    fn poll_receive(
725        &mut self,
726        _cx: &mut Context<'_>,
727        buf: &mut [u8],
728    ) -> Poll<Result<RxInfo, Self::Error>> {
729        let rx = self.network.receive(self.id, buf);
730        if rx.len == 0 {
731            Poll::Pending
732        } else {
733            Poll::Ready(Ok(rx))
734        }
735    }
736
737    fn max_frame_size(&self) -> usize {
738        self.max_frame_size
739    }
740
741    fn t_frame_ms(&self) -> u32 {
742        self.t_frame_ms
743    }
744}
745
746/// Minimal `NodeIdentity` implementation used by tests and simulations.
747///
748/// This type is not suitable for production use.
749#[derive(Clone)]
750pub struct DummyIdentity {
751    public_key: PublicKey,
752}
753
754impl DummyIdentity {
755    /// Construct a dummy identity from fixed public-key bytes.
756    pub fn new(bytes: [u8; 32]) -> Self {
757        Self {
758            public_key: PublicKey(bytes),
759        }
760    }
761}
762
763impl NodeIdentity for DummyIdentity {
764    type Error = ();
765
766    fn public_key(&self) -> &PublicKey {
767        &self.public_key
768    }
769
770    async fn sign(&self, _message: &[u8]) -> Result<[u8; 64], Self::Error> {
771        Ok([0u8; 64])
772    }
773
774    async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error> {
775        let mut out = [0u8; 32];
776        for (index, byte) in out.iter_mut().enumerate() {
777            *byte = self.public_key.0[index] ^ peer.0[index];
778        }
779        Ok(SharedSecret(out))
780    }
781}
782
783/// Dummy XOR-based cipher used by tests.
784///
785/// This type is intentionally insecure.
786pub struct DummyCipher {
787    key: [u8; 16],
788}
789
790impl AesCipher for DummyCipher {
791    fn encrypt_block(&self, block: &mut [u8; 16]) {
792        for (byte, key) in block.iter_mut().zip(self.key.iter()) {
793            *byte ^= *key;
794        }
795    }
796
797    fn decrypt_block(&self, block: &mut [u8; 16]) {
798        self.encrypt_block(block);
799    }
800}
801
802/// Dummy AES provider used by tests.
803///
804/// This type is intentionally insecure.
805#[derive(Clone, Copy)]
806pub struct DummyAes;
807
808impl AesProvider for DummyAes {
809    type Cipher = DummyCipher;
810
811    fn new_cipher(&self, key: &[u8; 16]) -> Self::Cipher {
812        DummyCipher { key: *key }
813    }
814}
815
816/// Dummy SHA/HMAC provider used by tests.
817///
818/// This type is intentionally insecure.
819#[derive(Clone, Copy)]
820pub struct DummySha;
821
822impl Sha256Provider for DummySha {
823    fn hash(&self, data: &[&[u8]]) -> [u8; 32] {
824        let mut out = [0u8; 32];
825        for chunk in data {
826            for (index, byte) in chunk.iter().enumerate() {
827                out[index % 32] ^= *byte;
828            }
829        }
830        out
831    }
832
833    fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32] {
834        let mut out = [0u8; 32];
835        for (index, byte) in key.iter().enumerate() {
836            out[index % 32] ^= *byte;
837        }
838        for chunk in data {
839            for (index, byte) in chunk.iter().enumerate() {
840                out[index % 32] ^= *byte;
841            }
842        }
843        out
844    }
845}
846
847/// Mutable monotonic test clock.
848#[derive(Clone, Default)]
849pub struct DummyClock {
850    now_ms: Rc<Cell<u64>>,
851}
852
853impl DummyClock {
854    /// Create the clock starting at `now_ms`.
855    pub fn new(now_ms: u64) -> Self {
856        Self {
857            now_ms: Rc::new(Cell::new(now_ms)),
858        }
859    }
860
861    /// Advance the clock by `delta_ms`.
862    pub fn advance_ms(&self, delta_ms: u64) {
863        self.now_ms.set(self.now_ms.get().saturating_add(delta_ms));
864    }
865
866    /// Set the current clock value.
867    pub fn set_ms(&self, now_ms: u64) {
868        self.now_ms.set(now_ms);
869    }
870}
871
872impl Clock for DummyClock {
873    fn now_ms(&self) -> u64 {
874        self.now_ms.get()
875    }
876}
877
878/// No-op async delay used only to satisfy the platform bundle in tests.
879#[derive(Clone, Copy, Default)]
880pub struct DummyDelay;
881
882impl DelayNs for DummyDelay {
883    async fn delay_ns(&mut self, _ns: u32) {}
884}
885
886/// Deterministic byte-filling RNG used by tests.
887///
888/// This type is intentionally predictable.
889#[derive(Clone, Default)]
890pub struct DummyRng(pub u8);
891
892impl TryRng for DummyRng {
893    type Error = Infallible;
894
895    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
896        let mut bytes = [0u8; 4];
897        self.fill_bytes(&mut bytes);
898        Ok(u32::from_le_bytes(bytes))
899    }
900
901    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
902        let mut bytes = [0u8; 8];
903        self.fill_bytes(&mut bytes);
904        Ok(u64::from_le_bytes(bytes))
905    }
906
907    fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Infallible> {
908        for byte in dest.iter_mut() {
909            *byte = self.0;
910            self.0 = self.0.wrapping_add(1);
911        }
912        Ok(())
913    }
914}
915
916impl TryCryptoRng for DummyRng {}
917
918/// No-op counter store used by tests.
919#[derive(Clone, Copy, Default)]
920pub struct DummyCounterStore;
921
922impl CounterStore for DummyCounterStore {
923    type Error = ();
924
925    async fn load(&self, _context: &[u8]) -> Result<u32, Self::Error> {
926        Ok(0)
927    }
928
929    async fn store(&self, _context: &[u8], _value: u32) -> Result<(), Self::Error> {
930        Ok(())
931    }
932
933    async fn flush(&self) -> Result<(), Self::Error> {
934        Ok(())
935    }
936}
937
938/// No-op key-value store used only to satisfy the platform bundle in tests.
939#[derive(Clone, Copy, Default)]
940pub struct DummyKeyValueStore;
941
942impl KeyValueStore for DummyKeyValueStore {
943    type Error = ();
944
945    async fn load(&self, _key: &[u8], _buf: &mut [u8]) -> Result<Option<usize>, Self::Error> {
946        Ok(None)
947    }
948
949    async fn store(&self, _key: &[u8], _value: &[u8]) -> Result<(), Self::Error> {
950        Ok(())
951    }
952
953    async fn delete(&self, _key: &[u8]) -> Result<(), Self::Error> {
954        Ok(())
955    }
956}