settings_common/config/
default_settings.rs

1// Copyright 2019 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::{format_err, Error};
6use serde::de::DeserializeOwned;
7use std::fmt::{Debug, Display};
8use std::fs::File;
9use std::io::BufReader;
10use std::path::Path;
11use std::rc::Rc;
12use std::sync::Mutex;
13
14use crate::config;
15use crate::config::ConfigLoadInfo;
16use crate::inspect::config_logger::InspectConfigLogger;
17
18pub struct DefaultSetting<T, P>
19where
20    T: DeserializeOwned + Clone + Debug,
21    P: AsRef<Path> + Display,
22{
23    default_value: Option<T>,
24    config_file_path: P,
25    cached_value: Option<Option<T>>,
26    config_logger: Rc<Mutex<InspectConfigLogger>>,
27}
28
29impl<T, P> DefaultSetting<T, P>
30where
31    T: DeserializeOwned + Clone + std::fmt::Debug,
32    P: AsRef<Path> + Display,
33{
34    pub fn new(
35        default_value: Option<T>,
36        config_file_path: P,
37        config_logger: Rc<Mutex<InspectConfigLogger>>,
38    ) -> Self {
39        DefaultSetting { default_value, config_file_path, cached_value: None, config_logger }
40    }
41
42    /// Returns the value of this setting. Loads the value from storage if it hasn't been loaded
43    /// before, otherwise returns a cached value.
44    pub fn get_cached_value(&mut self) -> Result<Option<T>, Error> {
45        if self.cached_value.is_none() {
46            self.cached_value = Some(self.load_default_settings()?);
47        }
48
49        Ok(self.cached_value.as_ref().expect("cached value not present").clone())
50    }
51
52    /// Loads the value of this setting from storage.
53    ///
54    /// If the value isn't present, returns the default value.
55    pub fn load_default_value(&mut self) -> Result<Option<T>, Error> {
56        self.load_default_settings()
57    }
58
59    /// Attempts to load the settings from the given config_file_path.
60    ///
61    /// Returns the default value if unable to read or parse the file. The returned option will
62    /// only be None if the default_value was provided as None.
63    fn load_default_settings(&mut self) -> Result<Option<T>, Error> {
64        let config_load_info: Option<ConfigLoadInfo>;
65        let path = self.config_file_path.to_string();
66        let load_result = match File::open(self.config_file_path.as_ref()) {
67            Ok(file) => {
68                #[allow(clippy::manual_map)]
69                match serde_json::from_reader(BufReader::new(file)) {
70                    Ok(config) => {
71                        // Success path.
72                        config_load_info = Some(ConfigLoadInfo {
73                            status: config::ConfigLoadStatus::Success,
74                            contents: if let Some(ref payload) = config {
75                                Some(format!("{payload:?}"))
76                            } else {
77                                None
78                            },
79                        });
80                        Ok(config)
81                    }
82                    Err(e) => {
83                        // Found file, but failed to parse.
84                        let err_msg = format!("unable to parse config: {e:?}");
85                        config_load_info = Some(ConfigLoadInfo {
86                            status: config::ConfigLoadStatus::ParseFailure(err_msg.clone()),
87                            contents: None,
88                        });
89                        Err(format_err!("{:?}", err_msg))
90                    }
91                }
92            }
93            Err(..) => {
94                // No file found.
95                config_load_info = Some(ConfigLoadInfo {
96                    status: config::ConfigLoadStatus::UsingDefaults(
97                        "File not found, using defaults".to_string(),
98                    ),
99                    contents: None,
100                });
101                Ok(self.default_value.clone())
102            }
103        };
104        if let Some(config_load_info) = config_load_info {
105            self.write_config_load_to_inspect(path, config_load_info);
106        } else {
107            log::error!("Could not load config for {:?}", path);
108        }
109
110        load_result
111    }
112
113    /// Attempts to write the config load to inspect.
114    fn write_config_load_to_inspect(
115        &mut self,
116        path: String,
117        config_load_info: config::ConfigLoadInfo,
118    ) {
119        self.config_logger.lock().unwrap().write_config_load_to_inspect(path, config_load_info);
120    }
121}
122
123#[cfg(test)]
124pub(crate) mod testing {
125    use super::*;
126
127    use crate::clock;
128    use assert_matches::assert_matches;
129    use diagnostics_assertions::{assert_data_tree, AnyProperty};
130    use fuchsia_async::TestExecutor;
131    use fuchsia_inspect::component;
132    use serde::Deserialize;
133    use settings_test_common::helpers::move_executor_forward_and_get;
134
135    #[derive(Clone, Debug, Deserialize)]
136    struct TestConfigData {
137        value: u32,
138    }
139
140    #[fuchsia::test(allow_stalls = false)]
141    async fn test_load_valid_config_data() {
142        let mut setting = DefaultSetting::new(
143            Some(TestConfigData { value: 3 }),
144            "/config/data/fake_config_data.json",
145            Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
146        );
147
148        assert_eq!(
149            setting.load_default_value().expect("Failed to get default value").unwrap().value,
150            10
151        );
152    }
153
154    #[fuchsia::test(allow_stalls = false)]
155    async fn test_load_invalid_config_data() {
156        let mut setting = DefaultSetting::new(
157            Some(TestConfigData { value: 3 }),
158            "/config/data/fake_invalid_config_data.json",
159            Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
160        );
161        assert!(setting.load_default_value().is_err());
162    }
163
164    #[fuchsia::test(allow_stalls = false)]
165    async fn test_load_invalid_config_file_path() {
166        let mut setting = DefaultSetting::new(
167            Some(TestConfigData { value: 3 }),
168            "nuthatch",
169            Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
170        );
171
172        assert_eq!(
173            setting.load_default_value().expect("Failed to get default value").unwrap().value,
174            3
175        );
176    }
177
178    #[fuchsia::test(allow_stalls = false)]
179    async fn test_load_default_none() {
180        let mut setting = DefaultSetting::<TestConfigData, &str>::new(
181            None,
182            "nuthatch",
183            Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
184        );
185
186        assert!(setting.load_default_value().expect("Failed to get default value").is_none());
187    }
188
189    #[fuchsia::test(allow_stalls = false)]
190    async fn test_no_inspect_write() {
191        let mut setting = DefaultSetting::<TestConfigData, &str>::new(
192            None,
193            "nuthatch",
194            Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
195        );
196
197        assert!(setting.load_default_value().expect("Failed to get default value").is_none());
198    }
199
200    #[fuchsia::test]
201    fn test_config_inspect_write() {
202        let mut executor = TestExecutor::new_with_fake_time();
203        clock::mock::set(zx::MonotonicInstant::from_nanos(0));
204
205        let inspector = component::inspector();
206        let mut setting = DefaultSetting::new(
207            Some(TestConfigData { value: 3 }),
208            "nuthatch",
209            Rc::new(Mutex::new(InspectConfigLogger::new(inspector.root()))),
210        );
211        let load_result = move_executor_forward_and_get(
212            &mut executor,
213            async { setting.load_default_value() },
214            "Unable to get default value",
215        );
216
217        assert_matches!(load_result, Ok(Some(TestConfigData { value: 3 })));
218        assert_data_tree!(@executor executor, inspector, root: {
219            config_loads: {
220                "nuthatch": {
221                    "count": AnyProperty,
222                    "result_counts": {
223                        "UsingDefaults": 1u64,
224                    },
225                    "timestamp": "0.000000000",
226                    "value": "ConfigLoadInfo {\n    status: UsingDefaults(\n        \"File not found, using defaults\",\n    ),\n    contents: None,\n}",
227                }
228            }
229        });
230    }
231}