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