xref: /aosp_15_r20/bootable/libbootloader/gbl/libefi/mocks/lib.rs (revision 5225e6b173e52d2efc6bcf950c27374fd72adabc)
1 // Copyright 2024, The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! UEFI object mocks to support unit tests.
16 //!
17 //! This module aliases mock objects to their standard names, so that code can just unconditionally
18 //! use e.g. `EfiEntry` and in test code it will switch to `MockEfiEntry`.
19 
20 #![feature(negative_impls)]
21 
22 pub mod protocol;
23 pub mod utils;
24 
25 use efi_types::{EfiConfigurationTable, EfiTimerDelay};
26 use liberror::Result;
27 use mockall::mock;
28 use protocol::{
29     gbl_efi_ab_slot::GblSlotProtocol,
30     gbl_efi_avb::GblAvbProtocol,
31     simple_text_output::{passthrough_con_out, MockSimpleTextOutputProtocol},
32 };
33 use std::cell::RefCell;
34 
35 /// libefi types that can be used in tests as-is.
36 pub use efi::{efi_print, efi_println, DeviceHandle, EventNotify, EventType};
37 
38 /// Holds state to set up a mock UEFI environment.
39 ///
40 /// Parts of the libefi API surface doesn't translate super well to mocks, so this struct helps
41 /// cover over some of the awkwardness. In particular, APIs that return "views" over what is
42 /// really a singleton object are difficult to mock properly.
43 ///
44 /// For example, the [efi::EfiEntry::system_table()] function returns a full [efi::SystemTable]
45 /// object, not a reference. This means that our mocks have to do the same, and return a new
46 /// [MockSystemTable] object - but this sort of defeats the purpose of mocks, which is to have
47 /// a mock that you can set up expectations ahead of time.
48 ///
49 /// You can get around this in a limited fashion if the code under test only needs to grab the
50 /// system table once; in this case you can use the `return_once()` expectation to move the mock
51 /// around. But this will cause a runtime error if the code under test tries to grab the system
52 /// table more than once, since the mock will have been moved out already. And since grabbing the
53 /// system table is very common, this really restricts what you can do in a test.
54 ///
55 /// [MockEfi] works around this by stashing objects like this in `thread_local` state, and the
56 /// mocks created at runtime just forward all their calls to this shared state. This allows
57 /// expectations to be placed at the EFI system level, and ignore any intermediate "view" mocks
58 /// that get created and destroyed over the course of the test.
59 pub struct MockEfi {
60     /// The global [MockEfiEntry] to set expectations on.
61     pub entry: MockEfiEntry,
62     /// The global [MockSystemTable] to set expectations on.
63     pub system_table: MockSystemTable,
64     /// The global [MockBootServices] to set expectations on.
65     pub boot_services: MockBootServices,
66     /// The global [MockSimpleTextOutputProtocol] to set expectations on.
67     pub con_out: MockSimpleTextOutputProtocol,
68 }
69 
70 thread_local! {
71     pub(crate) static MOCK_EFI: RefCell<Option<MockEfi>> = RefCell::new(None);
72 }
73 
74 impl MockEfi {
75     /// Creates a new [MockEfi].
76     ///
77     /// The following expectations will be set by this function, and should generally not be
78     /// adjusted by the caller:
79     /// * `entry.system_table()` will automatically forward to `system_table`
80     /// * `system_table.con_out()` will automatically forward to `con_out`
81     ///
82     /// Other than that, callers may set the other expectations as needed on these mocks.
83     ///
84     /// Once the mocks are ready, call [install] to install the thread-local state.
new() -> Self85     pub fn new() -> Self {
86         let mut entry = MockEfiEntry::default();
87         entry.expect_system_table().returning(|| passthrough_system_table());
88 
89         let mut system_table = MockSystemTable::default();
90         system_table.expect_boot_services().returning(|| passthrough_boot_services());
91         system_table.expect_con_out().returning(|| Ok(passthrough_con_out()));
92 
93         let boot_services = MockBootServices::default();
94         let con_out = MockSimpleTextOutputProtocol::default();
95 
96         Self { entry, system_table, boot_services, con_out }
97     }
98 
99     /// Installs the [MockEfi] in thread-local state.
100     ///
101     /// Only one [MockEfi] can be installed at a time (per thread). Attempting to install a
102     /// second will panic.
103     ///
104     /// Returns an [InstalledMockEfi] which automatically unregisters the state on drop.
install(self) -> InstalledMockEfi105     pub fn install(self) -> InstalledMockEfi {
106         MOCK_EFI.with_borrow_mut(|efi| {
107             // If this error message changes the unittest will need to change as well.
108             assert!(efi.is_none(), "Only one MockEfi can be installed at a time (per-thread)");
109             *efi = Some(self)
110         });
111         InstalledMockEfi { entry: passthrough_efi_entry() }
112     }
113 }
114 
115 /// Scoped wrapper to automatically unregister the global [MockEfi] on drop.
116 pub struct InstalledMockEfi {
117     entry: MockEfiEntry,
118 }
119 
120 impl InstalledMockEfi {
121     /// The user-facing [MockEfiEntry] to use in the code under test.
122     ///
123     /// This is a const ref so you cannot place expectations here, all calls will be forwarded to
124     /// the installed [MockEfi] mocks.
entry(&self) -> &MockEfiEntry125     pub fn entry(&self) -> &MockEfiEntry {
126         &self.entry
127     }
128 }
129 
130 /// [InstalledMockEfi] uses thread-local state so cannot be sent to another thread.
131 impl !Send for InstalledMockEfi {}
132 
133 impl Drop for InstalledMockEfi {
drop(&mut self)134     fn drop(&mut self) {
135         MOCK_EFI.with_borrow_mut(|efi| *efi = None);
136     }
137 }
138 
139 mock! {
140     /// Mock [efi::EfiEntry].
141     pub EfiEntry {
142         /// Returns a [MockSystemTable].
143         pub fn system_table(&self) -> MockSystemTable;
144 
145         /// Returns a real [efi::DeviceHandle], which is data-only so isn't mocked.
146         pub fn image_handle(&self) -> DeviceHandle;
147     }
148 }
149 /// Map to the libefi name so code under test can just use one name.
150 pub type EfiEntry = MockEfiEntry;
151 
152 /// While this mock itself isn't necessarily thread-local, passing through to the thread-local state
153 /// is our primary use case, so we just disallow [Send] entirely.
154 impl !Send for MockEfiEntry {}
155 
156 /// Returns a [MockEfiEntry] that forwards all calls to `MOCK_EFI`.
passthrough_efi_entry() -> MockEfiEntry157 fn passthrough_efi_entry() -> MockEfiEntry {
158     let mut entry = MockEfiEntry::default();
159     entry
160         .expect_system_table()
161         .returning(|| MOCK_EFI.with_borrow_mut(|efi| efi.as_mut().unwrap().entry.system_table()));
162     entry
163         .expect_image_handle()
164         .returning(|| MOCK_EFI.with_borrow_mut(|efi| efi.as_mut().unwrap().entry.image_handle()));
165     entry
166 }
167 
168 mock! {
169     /// Mock [efi::SystemTable].
170     pub SystemTable {
171         /// Returns a [MockBootServices].
172         pub fn boot_services(&self) -> MockBootServices;
173 
174         /// Returns a [MockRuntimeServices].
175         pub fn runtime_services(&self) -> MockRuntimeServices;
176 
177         /// Returns a [MockSimpleTextOutputProtocol]. This is a singleton protocol which is
178         /// always-open, as opposed to most protocols which need to be opened explicitly.
179         pub fn con_out(&self) -> Result<MockSimpleTextOutputProtocol>;
180 
181         /// Returns a real [efi::EfiConfigurationTable], which is data-only so isn't mocked.
182         pub fn configuration_table(&self) -> Option<&'static [EfiConfigurationTable]>;
183     }
184 }
185 /// Map to the libefi name so code under test can just use one name.
186 pub type SystemTable = MockSystemTable;
187 
188 /// While this mock itself isn't necessarily thread-local, passing through to the thread-local state
189 /// is our primary use case, so we just disallow [Send] entirely.
190 impl !Send for MockSystemTable {}
191 
192 /// Returns a [MockSystemTable] that forwards all calls to `MOCK_EFI`.
passthrough_system_table() -> MockSystemTable193 fn passthrough_system_table() -> MockSystemTable {
194     let mut table = MockSystemTable::default();
195     table.expect_boot_services().returning(|| {
196         MOCK_EFI.with_borrow_mut(|efi| efi.as_mut().unwrap().system_table.boot_services())
197     });
198     table
199         .expect_con_out()
200         .returning(|| MOCK_EFI.with_borrow_mut(|efi| efi.as_mut().unwrap().system_table.con_out()));
201     table
202 }
203 
204 mock! {
205     /// Mock [efi::BootServices].
206     pub BootServices {
207         /// Returns an instance of the requested type `T`.
208         ///
209         /// This is slightly different than the original API because it's difficult to mock an
210         /// [efi::Protocol] wrapping [efi::ProtocolInfo]. To simplify, we just return a mock
211         /// that looks like the protocol object.
212         pub fn open_protocol<T: 'static>(&self, handle: DeviceHandle) -> Result<T>;
213 
214         /// Similar to [open_protocol], returns the type `T`.
215         pub fn find_first_and_open<T: 'static>(&self) -> Result<T>;
216 
217         /// Returns a [MockEvent].
218         pub fn create_event(
219             &self,
220             event_type: EventType,
221             mut cb: Option<&'static mut EventNotify<'static>>,
222         ) -> Result<MockEvent>;
223 
224         /// Sets a [MockEvent] timer.
225         pub fn set_timer(
226             &self,
227             event: &MockEvent,
228             delay_type: EfiTimerDelay,
229             trigger_time: u64,
230         ) -> Result<()>;
231     }
232 }
233 /// Map to the libefi name so code under test can just use one name.
234 pub type BootServices = MockBootServices;
235 
236 /// Returns a [MockBootServices] that forwards all calls to `MOCK_EFI`.
passthrough_boot_services() -> MockBootServices237 fn passthrough_boot_services() -> MockBootServices {
238     let mut services = MockBootServices::default();
239     services.expect_find_first_and_open::<GblAvbProtocol>().returning(|| {
240         MOCK_EFI.with_borrow_mut(|efi| {
241             efi.as_mut().unwrap().boot_services.find_first_and_open::<GblAvbProtocol>()
242         })
243     });
244     services.expect_find_first_and_open::<GblSlotProtocol>().returning(|| {
245         MOCK_EFI.with_borrow_mut(|efi| {
246             efi.as_mut().unwrap().boot_services.find_first_and_open::<GblSlotProtocol>()
247         })
248     });
249 
250     services
251 }
252 
253 mock! {
254     /// Mock [efi::LocatedHandles].
255     pub LocatedHandles {}
256 }
257 /// Map to the libefi name so code under test can just use one name.
258 pub type LocatedHandles = MockLocatedHandles;
259 
260 mock! {
261     /// Mock [efi::Event].
262     pub Event {}
263 }
264 /// Map to the libefi name so code under test can just use one name.
265 pub type Event = MockEvent;
266 
267 mock! {
268     /// Mock [efi::RuntimeServices].
269     pub RuntimeServices {
270         /// Performs a cold reset.
271         pub fn cold_reset(&self);
272     }
273 }
274 
275 /// Map to the libefi name so code under test can just use one name.
276 pub type RuntimeServices = MockRuntimeServices;
277 
278 #[cfg(test)]
279 pub mod test {
280     use super::*;
281     use mockall::predicate::eq;
282     use std::fmt::Write;
283 
284     #[test]
efi_state_install()285     fn efi_state_install() {
286         MOCK_EFI.with_borrow_mut(|efi| assert!(efi.is_none()));
287 
288         // Global should still be `None` until we call `install()`.
289         let mock_efi = MockEfi::new();
290         MOCK_EFI.with_borrow_mut(|efi| assert!(efi.is_none()));
291 
292         let installed = mock_efi.install();
293         MOCK_EFI.with_borrow_mut(|efi| assert!(efi.is_some()));
294 
295         // Global goes back to `None` once the install goes out of scope.
296         drop(installed);
297         MOCK_EFI.with_borrow_mut(|efi| assert!(efi.is_none()));
298     }
299 
300     #[test]
301     #[should_panic(expected = "Only one MockEfi can be installed at a time (per-thread)")]
efi_state_double_install_fails()302     fn efi_state_double_install_fails() {
303         let mock_efi = MockEfi::new();
304         let mock_efi_2 = MockEfi::new();
305 
306         let installed = mock_efi.install();
307         mock_efi_2.install();
308 
309         // Explicit drop to keep it in scope until here.
310         drop(installed);
311     }
312 
313     #[test]
efi_state_con_out_write_once()314     fn efi_state_con_out_write_once() {
315         let mut mock_efi = MockEfi::new();
316         mock_efi.con_out.expect_write_str().once().with(eq("foo 123")).return_const(Ok(()));
317 
318         let installed = mock_efi.install();
319         let efi_entry = installed.entry();
320 
321         assert!(write!(efi_entry.system_table().con_out().unwrap(), "{} {}", "foo", 123).is_ok());
322     }
323 
324     #[test]
efi_state_con_out_write_twice_same_mock()325     fn efi_state_con_out_write_twice_same_mock() {
326         let mut mock_efi = MockEfi::new();
327         mock_efi.con_out.expect_write_str().once().with(eq("foo 123")).return_const(Ok(()));
328         mock_efi.con_out.expect_write_str().once().with(eq("bar 456")).return_const(Ok(()));
329 
330         let installed = mock_efi.install();
331         let efi_entry = installed.entry();
332 
333         let mut con_out = efi_entry.system_table().con_out().unwrap();
334         assert!(write!(con_out, "{} {}", "foo", 123).is_ok());
335         assert!(write!(con_out, "{} {}", "bar", 456).is_ok());
336     }
337 
338     #[test]
efi_state_con_out_write_twice_different_mock()339     fn efi_state_con_out_write_twice_different_mock() {
340         let mut mock_efi = MockEfi::new();
341         mock_efi.con_out.expect_write_str().once().with(eq("foo 123")).return_const(Ok(()));
342         mock_efi.con_out.expect_write_str().once().with(eq("bar 456")).return_const(Ok(()));
343 
344         let installed = mock_efi.install();
345         let efi_entry = installed.entry();
346 
347         // Call `write!` on two separate passthrough mocks, both should forward the calls to
348         // the "real" global mock.
349         //
350         // A common instance of this is `efi_print!` which fetches a new `con_out` on every call.
351         assert!(write!(efi_entry.system_table().con_out().unwrap(), "{} {}", "foo", 123).is_ok());
352         assert!(write!(efi_entry.system_table().con_out().unwrap(), "{} {}", "bar", 456).is_ok());
353     }
354 }
355