Skip to main content

settings_test_common/
storage.rs

1// Copyright 2025 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 anyhow::Error;
6use fidl_fuchsia_stash::{StoreAccessorMarker, StoreAccessorProxy, StoreAccessorRequest, Value};
7use fuchsia_inspect::component;
8use futures::lock::Mutex;
9use futures::TryStreamExt;
10use settings_storage::device_storage::{DeviceStorage, DeviceStorageCompatible};
11use settings_storage::fidl_storage::{FidlStorage, FidlStorageConvertible};
12use settings_storage::stash_logger::StashInspectLogger;
13use settings_storage::storage_factory::{
14    DefaultLoader, InitializationState, StorageAccess, StorageFactory,
15};
16use std::any::Any;
17use std::collections::HashMap;
18use std::rc::Rc;
19use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
20
21#[derive(PartialEq)]
22pub(crate) enum StashAction {
23    Get,
24    Flush,
25    Set,
26}
27
28pub(crate) struct StashStats {
29    actions: Vec<StashAction>,
30}
31
32impl StashStats {
33    pub(crate) fn new() -> Self {
34        StashStats { actions: Vec::new() }
35    }
36
37    pub(crate) fn record(&mut self, action: StashAction) {
38        self.actions.push(action);
39    }
40}
41
42/// Storage that does not write to disk, for testing.
43pub struct InMemoryStorageFactory {
44    initial_data: HashMap<&'static str, String>,
45    device_storage_cache: Mutex<InitializationState<DeviceStorage>>,
46    inspect_handle: Rc<Mutex<StashInspectLogger>>,
47}
48
49impl Default for InMemoryStorageFactory {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55const INITIALIZATION_ERROR: &str = "Cannot initialize an already accessed device storage. Make \
56    sure you're not retrieving a DeviceStorage before passing InMemoryStorageFactory to an \
57    EnvironmentBuilder. That must be done after. If you need initial data, use \
58    InMemoryStorageFactory::with_initial_data";
59
60impl InMemoryStorageFactory {
61    /// Constructs a new `InMemoryStorageFactory` with the ability to create a [`DeviceStorage`]
62    /// that can only read and write to the storage keys passed in.
63    pub fn new() -> Self {
64        InMemoryStorageFactory {
65            initial_data: HashMap::new(),
66            device_storage_cache: Mutex::new(InitializationState::new()),
67            inspect_handle: Rc::new(Mutex::new(StashInspectLogger::new(
68                component::inspector().root(),
69            ))),
70        }
71    }
72
73    /// Constructs a new `InMemoryStorageFactory` with the data written to stash. This simulates
74    /// the data existing in storage before the RestoreAgent reads it.
75    pub fn with_initial_data<T>(data: &T) -> Self
76    where
77        T: DeviceStorageCompatible,
78    {
79        let mut map = HashMap::new();
80        let _ = map.insert(T::KEY, serde_json::to_string(data).unwrap());
81        InMemoryStorageFactory {
82            initial_data: map,
83            device_storage_cache: Mutex::new(InitializationState::new()),
84            inspect_handle: Rc::new(Mutex::new(StashInspectLogger::new(
85                component::inspector().root(),
86            ))),
87        }
88    }
89
90    /// Helper method to simplify setup for `InMemoryStorageFactory` in tests.
91    pub async fn initialize_storage<T>(&self)
92    where
93        T: DeviceStorageCompatible,
94    {
95        self.initialize_storage_for_key(T::KEY).await;
96    }
97
98    async fn initialize_storage_for_key(&self, key: &'static str) {
99        match &mut *self.device_storage_cache.lock().await {
100            InitializationState::Initializing(initial_keys, _) => {
101                let _ = initial_keys.insert(key, None);
102            }
103            InitializationState::Initialized(_) => panic!("{}", INITIALIZATION_ERROR),
104            _ => unreachable!(),
105        }
106    }
107
108    async fn initialize_storage_for_key_with_loader(
109        &self,
110        key: &'static str,
111        loader: Box<dyn Any>,
112    ) {
113        match &mut *self.device_storage_cache.lock().await {
114            InitializationState::Initializing(initial_keys, _) => {
115                let _ = initial_keys.insert(key, Some(loader));
116            }
117            InitializationState::Initialized(_) => panic!("{}", INITIALIZATION_ERROR),
118            _ => unreachable!(),
119        }
120    }
121
122    /// Retrieve the [`DeviceStorage`] singleton.
123    pub async fn get_device_storage(&self) -> Rc<DeviceStorage> {
124        let initialization = &mut *self.device_storage_cache.lock().await;
125        match initialization {
126            InitializationState::Initializing(initial_keys, _) => {
127                let mut device_storage = DeviceStorage::with_stash_proxy(
128                    initial_keys.drain(),
129                    || {
130                        let (stash_proxy, _) = spawn_stash_proxy();
131                        stash_proxy
132                    },
133                    Rc::clone(&self.inspect_handle),
134                );
135                device_storage.set_caching_enabled(false);
136                device_storage.set_debounce_writes(false);
137
138                // write initial data to storage
139                for (&key, data) in &self.initial_data {
140                    device_storage
141                        .write_str(key, data.clone())
142                        .await
143                        .expect("Failed to write initial data");
144                }
145
146                let device_storage = Rc::new(device_storage);
147                *initialization = InitializationState::Initialized(Rc::clone(&device_storage));
148                device_storage
149            }
150            InitializationState::Initialized(device_storage) => Rc::clone(device_storage),
151            _ => unreachable!(),
152        }
153    }
154}
155
156impl StorageFactory for InMemoryStorageFactory {
157    type Storage = DeviceStorage;
158
159    async fn initialize<T>(&self) -> Result<(), Error>
160    where
161        T: StorageAccess<Storage = DeviceStorage>,
162    {
163        self.initialize_storage_for_key(T::STORAGE_KEY).await;
164        Ok(())
165    }
166
167    async fn initialize_with_loader<T, L>(&self, loader: L) -> Result<(), Error>
168    where
169        T: StorageAccess<Storage = DeviceStorage>,
170        L: DefaultLoader<Result = T::Data> + 'static,
171    {
172        self.initialize_storage_for_key_with_loader(
173            T::STORAGE_KEY,
174            Box::new(loader) as Box<dyn Any>,
175        )
176        .await;
177        Ok(())
178    }
179
180    async fn get_store(&self) -> Rc<DeviceStorage> {
181        self.get_device_storage().await
182    }
183}
184
185fn spawn_stash_proxy() -> (StoreAccessorProxy, Rc<Mutex<StashStats>>) {
186    let (stash_proxy, mut stash_stream) =
187        fidl::endpoints::create_proxy_and_stream::<StoreAccessorMarker>();
188    let stats = Rc::new(Mutex::new(StashStats::new()));
189    let stats_clone = stats.clone();
190    fasync::Task::local(async move {
191        let mut stored_value: Option<Value> = None;
192        let mut stored_key: Option<String> = None;
193
194        while let Some(req) = stash_stream.try_next().await.unwrap() {
195            #[allow(unreachable_patterns)]
196            match req {
197                StoreAccessorRequest::GetValue { key, responder } => {
198                    stats_clone.lock().await.record(StashAction::Get);
199                    if let Some(key_string) = stored_key {
200                        assert_eq!(key, key_string);
201                    }
202                    stored_key = Some(key);
203
204                    let value = stored_value.as_ref().map(|value| match value {
205                        Value::Intval(v) => Value::Intval(*v),
206                        Value::Floatval(v) => Value::Floatval(*v),
207                        Value::Boolval(v) => Value::Boolval(*v),
208                        Value::Stringval(v) => Value::Stringval(v.clone()),
209                        Value::Bytesval(buffer) => {
210                            let opts = zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE;
211                            Value::Bytesval(fidl_fuchsia_mem::Buffer {
212                                vmo: buffer.vmo.create_child(opts, 0, buffer.size).unwrap(),
213                                size: buffer.size,
214                            })
215                        }
216                    });
217                    responder.send(value).unwrap();
218                }
219                StoreAccessorRequest::SetValue { key, val, control_handle: _ } => {
220                    stats_clone.lock().await.record(StashAction::Set);
221                    if let Some(key_string) = stored_key {
222                        assert_eq!(key, key_string);
223                    }
224                    stored_key = Some(key);
225                    stored_value = Some(val);
226                }
227                StoreAccessorRequest::Flush { responder } => {
228                    stats_clone.lock().await.record(StashAction::Flush);
229                    let _ = responder.send(Ok(()));
230                }
231                _ => {}
232            }
233        }
234    })
235    .detach();
236    (stash_proxy, stats)
237}
238
239/// Storage that does not write to disk, for testing.
240pub struct InMemoryFidlStorageFactory {
241    initial_data: HashMap<&'static str, Vec<u8>>,
242    fidl_storage_cache: Mutex<InitializationState<(Rc<FidlStorage>, tempfile::TempDir)>>,
243}
244
245impl Default for InMemoryFidlStorageFactory {
246    fn default() -> Self {
247        Self::new()
248    }
249}
250
251const FIDL_INITIALIZATION_ERROR: &str =
252    "Cannot initialize an already accessed device storage. Make \
253    sure you're not retrieving a FidlStorage before passing InMemoryFidlStorageFactory to an \
254    EnvironmentBuilder. That must be done after.";
255
256fn open_tempdir(tempdir: &tempfile::TempDir) -> fio::DirectoryProxy {
257    fuchsia_fs::directory::open_in_namespace(
258        tempdir.path().to_str().expect("tempdir path is not valid UTF-8"),
259        fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
260    )
261    .expect("failed to open connection to tempdir")
262}
263
264impl InMemoryFidlStorageFactory {
265    /// Constructs a new `InMemoryFidlStorageFactory` with the ability to create a [`FidlStorage`]
266    /// that can only read and write to the storage keys passed in.
267    pub fn new() -> Self {
268        InMemoryFidlStorageFactory {
269            initial_data: HashMap::new(),
270            fidl_storage_cache: Mutex::new(InitializationState::new()),
271        }
272    }
273
274    /// Helper method to simplify setup for `InMemoryFidlStorageFactory` in tests.
275    pub async fn initialize_storage<T>(&self)
276    where
277        T: FidlStorageConvertible,
278    {
279        self.initialize_storage_for_key(T::KEY).await;
280    }
281
282    async fn initialize_storage_for_key(&self, key: &'static str) {
283        match &mut *self.fidl_storage_cache.lock().await {
284            InitializationState::Initializing(initial_keys, _) => {
285                let _ = initial_keys.insert(key, None);
286            }
287            InitializationState::Initialized(_) => panic!("{}", FIDL_INITIALIZATION_ERROR),
288            _ => unreachable!(),
289        }
290    }
291
292    async fn initialize_storage_for_key_with_loader(
293        &self,
294        key: &'static str,
295        loader: Box<dyn Any>,
296    ) {
297        match &mut *self.fidl_storage_cache.lock().await {
298            InitializationState::Initializing(initial_keys, _) => {
299                let _ = initial_keys.insert(key, Some(loader));
300            }
301            InitializationState::Initialized(_) => panic!("{}", INITIALIZATION_ERROR),
302            _ => unreachable!(),
303        }
304    }
305
306    /// Retrieve the [`FidlStorage`] singleton.
307    pub(crate) async fn get_fidl_storage(&self) -> Rc<FidlStorage> {
308        let initialization = &mut *self.fidl_storage_cache.lock().await;
309        match initialization {
310            InitializationState::Initializing(initial_keys, _) => {
311                let tempdir = tempfile::tempdir().expect("failed to create tempdir");
312                let directory = open_tempdir(&tempdir);
313
314                let (mut fidl_storage, tasks) =
315                    FidlStorage::with_file_proxy(initial_keys.drain(), directory, move |key| {
316                        let temp_file_name = format!("{key}.tmp");
317                        let file_name = format!("{key}.pfidl");
318                        Ok((temp_file_name, file_name))
319                    })
320                    .await
321                    .unwrap();
322
323                for task in tasks {
324                    task.detach();
325                }
326                fidl_storage.set_caching_enabled(false);
327                fidl_storage.set_debounce_writes(false);
328
329                // write initial data to storage
330                for (&key, data) in &self.initial_data {
331                    fidl_storage
332                        .write_test_bytes(key, data.clone())
333                        .await
334                        .expect("Failed to write initial data");
335                }
336
337                let fidl_storage = Rc::new(fidl_storage);
338                *initialization =
339                    InitializationState::Initialized(Rc::new((Rc::clone(&fidl_storage), tempdir)));
340                fidl_storage
341            }
342            InitializationState::Initialized(initialized) => Rc::clone(&initialized.as_ref().0),
343            _ => unreachable!(),
344        }
345    }
346}
347
348impl StorageFactory for InMemoryFidlStorageFactory {
349    type Storage = FidlStorage;
350
351    async fn initialize<T>(&self) -> Result<(), Error>
352    where
353        T: StorageAccess<Storage = FidlStorage>,
354    {
355        self.initialize_storage_for_key(T::STORAGE_KEY).await;
356        Ok(())
357    }
358
359    async fn initialize_with_loader<T, L>(&self, loader: L) -> Result<(), Error>
360    where
361        T: StorageAccess<Storage = FidlStorage>,
362        L: DefaultLoader<Result = T::Data> + 'static,
363    {
364        self.initialize_storage_for_key_with_loader(
365            T::STORAGE_KEY,
366            Box::new(loader) as Box<dyn Any>,
367        )
368        .await;
369        Ok(())
370    }
371
372    async fn get_store(&self) -> Rc<FidlStorage> {
373        self.get_fidl_storage().await
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use serde::{Deserialize, Serialize};
381    use settings_storage::storage_factory::NoneT;
382
383    const VALUE0: i32 = 3;
384    const VALUE1: i32 = 33;
385
386    #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
387    struct TestStruct {
388        value: i32,
389    }
390
391    impl DeviceStorageCompatible for TestStruct {
392        type Loader = NoneT;
393        const KEY: &'static str = "testkey";
394    }
395
396    impl Default for TestStruct {
397        fn default() -> Self {
398            TestStruct { value: VALUE0 }
399        }
400    }
401
402    #[fuchsia::test(allow_stalls = false)]
403    async fn test_in_memory_storage() {
404        let factory = InMemoryStorageFactory::new();
405        factory.initialize_storage::<TestStruct>().await;
406
407        let store_1 = factory.get_device_storage().await;
408        let store_2 = factory.get_device_storage().await;
409
410        // Write initial data through first store.
411        let test_struct = TestStruct { value: VALUE0 };
412
413        // Ensure writing from store1 ends up in store2
414        test_write_propagation(store_1.clone(), store_2.clone(), test_struct).await;
415
416        let test_struct_2 = TestStruct { value: VALUE1 };
417        // Ensure writing from store2 ends up in store1
418        test_write_propagation(store_2.clone(), store_1.clone(), test_struct_2).await;
419    }
420
421    async fn test_write_propagation(
422        store_1: Rc<DeviceStorage>,
423        store_2: Rc<DeviceStorage>,
424        data: TestStruct,
425    ) {
426        assert!(store_1.write(&data).await.is_ok());
427
428        // Ensure it is read in from second store.
429        let retrieved_struct = store_2.get::<TestStruct>().await;
430        assert_eq!(data, retrieved_struct);
431    }
432}