settings_common/config/
default_settings.rs1use 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 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 pub fn load_default_value(&mut self) -> Result<Option<T>, Error> {
56 self.load_default_settings()
57 }
58
59 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 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 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 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 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}