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