Skip to main content

globally_ordered_mock_mmio/
mock_builder.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use mmio::region::{MmioRegion, UnsafeMmio};
6use std::sync::Arc;
7
8use crate::data_access::AccessSize;
9use crate::scoreboard::Scoreboard;
10use crate::trace_builder::ExpectedTraceBuilder;
11
12/// Mock [`UnsafeMmio`] implementation that routes all calls to [`Scoreboard`].
13///
14/// Guaranteed to implement [`Send`] and [`Sync`].
15#[derive(Clone, Debug)]
16pub struct MockUnsafeMmio {
17    scoreboard: Arc<Scoreboard>,
18}
19
20impl UnsafeMmio for MockUnsafeMmio {
21    fn len(&self) -> usize {
22        self.scoreboard.region_size_bytes()
23    }
24
25    fn align_offset(&self, _align: usize) -> usize {
26        // The mock models an abstract contiguous MMIO region starting at byte offset 0.
27        // Unlike physical VMO mappings, no page-boundary alignment adjustment is needed.
28        0
29    }
30
31    unsafe fn load8_unchecked(&self, offset: usize) -> u8 {
32        let value = self.scoreboard.load(offset, AccessSize::U8);
33        u8::try_from(value).expect("Scoreboard guarantees the value fits in the access size")
34    }
35
36    unsafe fn load16_unchecked(&self, offset: usize) -> u16 {
37        let value = self.scoreboard.load(offset, AccessSize::U16);
38        u16::try_from(value).expect("Scoreboard guarantees the value fits in the access size")
39    }
40
41    unsafe fn load32_unchecked(&self, offset: usize) -> u32 {
42        let value = self.scoreboard.load(offset, AccessSize::U32);
43        u32::try_from(value).expect("Scoreboard guarantees the value fits in the access size")
44    }
45
46    unsafe fn load64_unchecked(&self, offset: usize) -> u64 {
47        self.scoreboard.load(offset, AccessSize::U64)
48    }
49
50    unsafe fn store8_unchecked(&self, offset: usize, value: u8) {
51        self.scoreboard.store(offset, AccessSize::U8, u64::from(value));
52    }
53
54    unsafe fn store16_unchecked(&self, offset: usize, value: u16) {
55        self.scoreboard.store(offset, AccessSize::U16, u64::from(value));
56    }
57
58    unsafe fn store32_unchecked(&self, offset: usize, value: u32) {
59        self.scoreboard.store(offset, AccessSize::U32, u64::from(value));
60    }
61
62    unsafe fn store64_unchecked(&self, offset: usize, value: u64) {
63        self.scoreboard.store(offset, AccessSize::U64, value);
64    }
65
66    fn write_barrier(&self) {
67        self.scoreboard.write_barrier();
68    }
69}
70
71/// [`MmioRegion`] produced by [`MockMmioRegionBuilder`].
72///
73/// The underlying [`UnsafeMmio`] implementation is guaranteed to implement
74/// [`Send`] and [`Sync`].
75pub type MockMmioRegion = MmioRegion<MockUnsafeMmio, Arc<MockUnsafeMmio>>;
76
77/// Strict thread-safe mock (interaction testing) for MMIO ([`MmioRegion`]).
78///
79/// The [`MmioRegion`] implementation imposes a global ordering on MMIO accesses
80/// performed by multiple threads. If the code under test uses a non-thread-safe
81/// [`MmioRegion`] implementation, the mock introduces cross-thread
82/// synchronization points that do not exist in the code under test.
83///
84/// Expectations are verified after this builder and all the [`MockMmioRegion`]s
85/// it produced are dropped. Tests do not need to keep the builder alive while
86/// the code under test uses the regions. Conversely, dropping the builder does
87/// not verify expectations while a region is still in use.
88#[derive(Debug)]
89pub struct MockMmioRegionBuilder {
90    scoreboard: Arc<Scoreboard>,
91}
92
93impl MockMmioRegionBuilder {
94    /// Creates a new mock MMIO region covering `region_size_bytes` bytes.
95    pub fn new(region_size_bytes: usize) -> Self {
96        Self { scoreboard: Arc::new(Scoreboard::new(region_size_bytes)) }
97    }
98
99    /// Returns the configured region size in bytes.
100    pub fn region_size_bytes(&self) -> usize {
101        self.scoreboard.region_size_bytes()
102    }
103
104    /// Posts expectations using the closure trace builder.
105    ///
106    /// [`ExpectedTraceBuilder`] implements the expectation API provided to the
107    /// closure.
108    ///
109    /// Expectations posted by multiple calls are appended to a single global
110    /// expectation list, in call order.
111    pub fn expect<F: FnOnce(&mut ExpectedTraceBuilder<'_>)>(&self, f: F) {
112        let mut builder = ExpectedTraceBuilder::new(&self.scoreboard);
113        f(&mut builder);
114    }
115
116    /// Returns a [`MockUnsafeMmio`] directly connected to this mock's scoreboard.
117    fn mock_unsafe_mmio(&self) -> MockUnsafeMmio {
118        MockUnsafeMmio { scoreboard: Arc::clone(&self.scoreboard) }
119    }
120
121    /// Constructs a splittable, sendable [`MmioRegion`] wrapping this mock.
122    ///
123    /// The returned region, and any sub-region split off of it, keeps the
124    /// expectation list alive. Expectations are verified when the last region
125    /// and this builder are dropped.
126    pub fn build(&self) -> MockMmioRegion {
127        MmioRegion::new(self.mock_unsafe_mmio()).into_split_send()
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use mmio::Mmio;
135
136    #[fuchsia::test]
137    fn test_mock_mmio_region_size_bytes() {
138        let mock_builder = MockMmioRegionBuilder::new(0x2000);
139        assert_eq!(mock_builder.region_size_bytes(), 0x2000);
140    }
141
142    #[fuchsia::test]
143    fn test_mock_mmio_built_region_len() {
144        let mock_builder = MockMmioRegionBuilder::new(0x2000);
145        let mmio = mock_builder.build();
146        assert_eq!(mmio.len(), 0x2000);
147    }
148
149    #[fuchsia::test]
150    fn test_mock_mmio_built_region_operations() {
151        let mock_builder = MockMmioRegionBuilder::new(0x1000);
152        mock_builder.expect(|t| {
153            t.at(0x00).write8(0x12);
154            t.at(0x02).write16(0x3456);
155            t.at(0x04).write32(0x789a_bcde);
156            t.at(0x08).write64(0x0123_4567_89ab_cdef);
157            t.at(0x10).read8(0xaa);
158            t.at(0x12).read16(0xbbcc);
159            t.at(0x14).read32(0xddee_ff00);
160            t.at(0x18).read64(0x1122_3344_5566_7788);
161            t.write_barrier();
162        });
163        let mut mmio = mock_builder.build();
164        mmio.store8(0x00, 0x12);
165        mmio.store16(0x02, 0x3456);
166        mmio.store32(0x04, 0x789a_bcde);
167        mmio.store64(0x08, 0x0123_4567_89ab_cdef);
168        assert_eq!(mmio.load8(0x10), 0xaa);
169        assert_eq!(mmio.load16(0x12), 0xbbcc);
170        assert_eq!(mmio.load32(0x14), 0xddee_ff00);
171        assert_eq!(mmio.load64(0x18), 0x1122_3344_5566_7788);
172        mmio.write_barrier();
173    }
174
175    #[fuchsia::test]
176    fn test_mock_mmio_built_region_split_off() {
177        use mmio::MmioSplit;
178
179        let mock_builder = MockMmioRegionBuilder::new(0x1000);
180        mock_builder.expect(|t| {
181            t.at(0x0).write32(0x1111);
182            t.at(0x100).write32(0x2222);
183        });
184        let mut mmio = mock_builder.build();
185        let mut sub1 = mmio.split_off(0x100);
186        assert_eq!(sub1.len(), 0x100);
187        assert_eq!(mmio.len(), 0xf00);
188
189        sub1.store32(0x0, 0x1111);
190        mmio.store32(0x0, 0x2222);
191    }
192
193    #[fuchsia::test]
194    #[should_panic(expected = "UNRETIRED MMIO EXPECTATIONS")]
195    fn test_dropping_builder_and_region_verifies_expectations() {
196        let mock_builder = MockMmioRegionBuilder::new(0x1000);
197        mock_builder.expect(|t| {
198            t.at(0x0).read8(0x42);
199        });
200        let mmio = mock_builder.build();
201        drop(mmio);
202        drop(mock_builder);
203    }
204
205    #[fuchsia::test]
206    fn test_dropping_builder_alone_does_not_verify_expectations() {
207        let mock_builder = MockMmioRegionBuilder::new(0x1000);
208        mock_builder.expect(|t| {
209            t.at(0x0).read8(0x42);
210        });
211        let mmio = mock_builder.build();
212
213        // The builder is dropped while the code under test still holds a region.
214        // Verification must be deferred until the region is dropped.
215        drop(mock_builder);
216
217        assert_eq!(mmio.load8(0x0), 0x42);
218    }
219
220    #[fuchsia::test]
221    fn test_mock_unsafe_mmio_align_offset() {
222        let mock_builder = MockMmioRegionBuilder::new(0x1000);
223        let mock_unsafe_mmio = mock_builder.mock_unsafe_mmio();
224        assert_eq!(mock_unsafe_mmio.align_offset(4), 0);
225        assert_eq!(mock_unsafe_mmio.align_offset(8), 0);
226    }
227
228    #[fuchsia::test]
229    fn test_mock_unsafe_mmio_load_unchecked() {
230        let mock_builder = MockMmioRegionBuilder::new(0x1000);
231        mock_builder.expect(|t| {
232            t.at(0x0).read8(0x12);
233            t.at(0x2).read16(0x3456);
234            t.at(0x4).read32(0x789a_bcde);
235            t.at(0x8).read64(0x0123_4567_89ab_cdef);
236        });
237        let mock_unsafe = mock_builder.mock_unsafe_mmio();
238        unsafe {
239            assert_eq!(mock_unsafe.load8_unchecked(0x0), 0x12);
240            assert_eq!(mock_unsafe.load16_unchecked(0x2), 0x3456);
241            assert_eq!(mock_unsafe.load32_unchecked(0x4), 0x789a_bcde);
242            assert_eq!(mock_unsafe.load64_unchecked(0x8), 0x0123_4567_89ab_cdef);
243        }
244    }
245
246    #[fuchsia::test]
247    fn test_mock_unsafe_mmio_store_unchecked() {
248        let mock_builder = MockMmioRegionBuilder::new(0x1000);
249        mock_builder.expect(|t| {
250            t.at(0x0).write8(0x12);
251            t.at(0x2).write16(0x3456);
252            t.at(0x4).write32(0x789a_bcde);
253            t.at(0x8).write64(0x0123_4567_89ab_cdef);
254        });
255        let mock_unsafe = mock_builder.mock_unsafe_mmio();
256        unsafe {
257            mock_unsafe.store8_unchecked(0x0, 0x12);
258            mock_unsafe.store16_unchecked(0x2, 0x3456);
259            mock_unsafe.store32_unchecked(0x4, 0x789a_bcde);
260            mock_unsafe.store64_unchecked(0x8, 0x0123_4567_89ab_cdef);
261        }
262    }
263
264    #[fuchsia::test]
265    fn test_mock_unsafe_mmio_write_barrier() {
266        let mock_builder = MockMmioRegionBuilder::new(0x1000);
267        mock_builder.expect(|t| {
268            t.write_barrier();
269        });
270        let mock_unsafe = mock_builder.mock_unsafe_mmio();
271        mock_unsafe.write_barrier();
272    }
273
274    #[fuchsia::test]
275    fn test_mock_unsafe_mmio_send() {
276        fn assert_send<T: Send>() {}
277        assert_send::<MockUnsafeMmio>();
278
279        let mock_builder = MockMmioRegionBuilder::new(0x1000);
280        let mock_unsafe_mmio = mock_builder.mock_unsafe_mmio();
281        let handle = std::thread::spawn(move || {
282            assert_eq!(mock_unsafe_mmio.len(), 0x1000);
283        });
284        handle.join().unwrap();
285    }
286
287    #[fuchsia::test]
288    fn test_mock_unsafe_mmio_sync() {
289        fn assert_sync<T: Sync>() {}
290        assert_sync::<MockUnsafeMmio>();
291
292        let mock_builder = MockMmioRegionBuilder::new(0x1000);
293        let mock_unsafe_mmio = mock_builder.mock_unsafe_mmio();
294        std::thread::scope(|s| {
295            s.spawn(|| {
296                assert_eq!(mock_unsafe_mmio.len(), 0x1000);
297            });
298            assert_eq!(mock_unsafe_mmio.len(), 0x1000);
299        });
300    }
301}