settings/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::base::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::base::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::base::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::base::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::base::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 crate::tests::helpers::move_executor_forward_and_get;
129
130 use assert_matches::assert_matches;
131 use diagnostics_assertions::{assert_data_tree, AnyProperty};
132 use fuchsia_async::TestExecutor;
133 use fuchsia_inspect::component;
134 use serde::Deserialize;
135 use zx::MonotonicInstant;
136
137 #[derive(Clone, Debug, Deserialize)]
138 struct TestConfigData {
139 value: u32,
140 }
141
142 #[fuchsia::test(allow_stalls = false)]
143 async fn test_load_valid_config_data() {
144 let mut setting = DefaultSetting::new(
145 Some(TestConfigData { value: 3 }),
146 "/config/data/fake_config_data.json",
147 Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
148 );
149
150 assert_eq!(
151 setting.load_default_value().expect("Failed to get default value").unwrap().value,
152 10
153 );
154 }
155
156 #[fuchsia::test(allow_stalls = false)]
157 async fn test_load_invalid_config_data() {
158 let mut setting = DefaultSetting::new(
159 Some(TestConfigData { value: 3 }),
160 "/config/data/fake_invalid_config_data.json",
161 Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
162 );
163 assert!(setting.load_default_value().is_err());
164 }
165
166 #[fuchsia::test(allow_stalls = false)]
167 async fn test_load_invalid_config_file_path() {
168 let mut setting = DefaultSetting::new(
169 Some(TestConfigData { value: 3 }),
170 "nuthatch",
171 Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
172 );
173
174 assert_eq!(
175 setting.load_default_value().expect("Failed to get default value").unwrap().value,
176 3
177 );
178 }
179
180 #[fuchsia::test(allow_stalls = false)]
181 async fn test_load_default_none() {
182 let mut setting = DefaultSetting::<TestConfigData, &str>::new(
183 None,
184 "nuthatch",
185 Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
186 );
187
188 assert!(setting.load_default_value().expect("Failed to get default value").is_none());
189 }
190
191 #[fuchsia::test(allow_stalls = false)]
192 async fn test_no_inspect_write() {
193 let mut setting = DefaultSetting::<TestConfigData, &str>::new(
194 None,
195 "nuthatch",
196 Rc::new(Mutex::new(InspectConfigLogger::new(component::inspector().root()))),
197 );
198
199 assert!(setting.load_default_value().expect("Failed to get default value").is_none());
200 }
201
202 #[fuchsia::test]
203 fn test_config_inspect_write() {
204 clock::mock::set(MonotonicInstant::from_nanos(0));
205
206 let mut executor = TestExecutor::new_with_fake_time();
207
208 let inspector = component::inspector();
209 let mut setting = DefaultSetting::new(
210 Some(TestConfigData { value: 3 }),
211 "nuthatch",
212 Rc::new(Mutex::new(InspectConfigLogger::new(inspector.root()))),
213 );
214 let load_result = move_executor_forward_and_get(
215 &mut executor,
216 async { setting.load_default_value() },
217 "Unable to get default value",
218 );
219
220 assert_matches!(load_result, Ok(Some(TestConfigData { value: 3 })));
221 assert_data_tree!(inspector, root: {
222 config_loads: {
223 "nuthatch": {
224 "count": AnyProperty,
225 "result_counts": {
226 "UsingDefaults": 1u64,
227 },
228 "timestamp": "0.000000000",
229 "value": "ConfigLoadInfo {\n status: UsingDefaults(\n \"File not found, using defaults\",\n ),\n contents: None,\n}",
230 }
231 }
232 });
233 }
234}