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