settings/storage_migrations/
v1653667208_light_migration.rs

1// Copyright 2022 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 crate::light::types::LightInfo;
6use crate::migration::{FileGenerator, Migration, MigrationError};
7use anyhow::{anyhow, Context};
8use fidl::endpoints::create_proxy;
9use fidl::persist;
10use fidl_fuchsia_io::FileProxy;
11use fidl_fuchsia_settings::LightGroup as LightGroupFidl;
12use fidl_fuchsia_settings_storage::LightGroups;
13use fidl_fuchsia_stash::{StoreProxy, Value};
14
15use settings_storage::fidl_storage::FidlStorageConvertible;
16
17const LIGHT_KEY: &str = "settings_light_info";
18
19/// Migrates Light settings data from stash and stores it in persistent fidl-serialized files.
20pub(crate) struct V1653667208LightMigration(pub(crate) StoreProxy);
21
22#[async_trait::async_trait(?Send)]
23impl Migration for V1653667208LightMigration {
24    fn id(&self) -> u64 {
25        1653667208
26    }
27
28    async fn migrate(&self, file_generator: FileGenerator) -> Result<(), MigrationError> {
29        let (stash_proxy, server_end) = create_proxy();
30        self.0.create_accessor(true, server_end).expect("failed to create accessor for stash");
31        let value = stash_proxy.get_value(LIGHT_KEY).await.context("failed to call get_value")?;
32        let str_json = match value {
33            None => return Err(MigrationError::NoData),
34            Some(value) => {
35                if let Value::Stringval(str_json) = *value {
36                    str_json
37                } else {
38                    return Err(MigrationError::Unrecoverable(anyhow!("data in incorrect format")));
39                }
40            }
41        };
42        let light_data = serde_json::from_str::<LightInfo>(&str_json)
43            .context("failed to deserialize light data")?;
44
45        let light_groups = LightGroups {
46            groups: light_data
47                .light_groups
48                .into_values()
49                .map(LightGroupFidl::from)
50                .collect::<Vec<_>>(),
51        };
52        let file: FileProxy = file_generator
53            .new_file(<LightInfo as FidlStorageConvertible>::KEY)
54            .await
55            .map_err(|file_error| match file_error {
56                fuchsia_fs::node::OpenError::OpenError(status)
57                    if status == zx::Status::NO_SPACE =>
58                {
59                    MigrationError::DiskFull
60                }
61                _ => Err::<(), _>(file_error)
62                    .context("unable to open new_file: {:?}")
63                    .unwrap_err()
64                    .into(),
65            })?;
66        let encoded = persist(&light_groups).context("failed to serialize new fidl format")?;
67        let _ = file.write(&encoded).await.context("file to call write")?.map_err(|e| {
68            let status = zx::Status::from_raw(e);
69            if status == zx::Status::NO_SPACE {
70                MigrationError::DiskFull
71            } else {
72                MigrationError::Unrecoverable(anyhow!("failed to write migration: {:?}", e))
73            }
74        })?;
75        Ok(())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::storage_migrations::tests::open_tempdir;
83    use assert_matches::assert_matches;
84    use fidl_fuchsia_settings::{LightGroup, LightState, LightType, LightValue};
85    use fidl_fuchsia_stash::{StoreAccessorRequest, StoreMarker, StoreRequest};
86    use fidl_fuchsia_ui_types::ColorRgb;
87    use fuchsia_async as fasync;
88    use futures::StreamExt;
89
90    // Ensure that failing to get data from stash (i.e. not out of space or not found) results in
91    // an unrecoverable error.
92    #[fuchsia::test]
93    async fn v1653667208_light_migration_error_getting_value() {
94        let (store_proxy, server_end) = create_proxy::<StoreMarker>();
95        let mut request_stream = server_end.into_stream();
96        let task = fasync::Task::local(async move {
97            let mut tasks = vec![];
98            while let Some(Ok(request)) = request_stream.next().await {
99                if let StoreRequest::CreateAccessor { accessor_request, .. } = request {
100                    let mut request_stream = accessor_request.into_stream();
101                    tasks.push(fasync::Task::local(async move {
102                        while let Some(Ok(request)) = request_stream.next().await {
103                            if let StoreAccessorRequest::GetValue { responder, .. } = request {
104                                // Just ignore the responder so the client receives an error.
105                                drop(responder);
106                            } else {
107                                panic!("unexpected request: {request:?}");
108                            }
109                        }
110                    }))
111                }
112            }
113            for task in tasks {
114                task.await
115            }
116        });
117
118        let migration = V1653667208LightMigration(store_proxy);
119        let fs = tempfile::tempdir().expect("failed to create tempdir");
120        let directory = open_tempdir(&fs);
121        let file_generator = FileGenerator::new(0, migration.id(), Clone::clone(&directory));
122        let result = migration.migrate(file_generator).await;
123        assert_matches!(result, Err(MigrationError::Unrecoverable(_)));
124        let err = result.unwrap_err();
125        assert!(format!("{err:?}").contains("failed to call get_value"));
126
127        drop(migration);
128
129        task.await;
130    }
131
132    // Ensure we see that no data is available when the data is not in stash.
133    #[fuchsia::test]
134    async fn v1653667208_light_migration_no_data() {
135        let (store_proxy, server_end) = create_proxy::<StoreMarker>();
136        let mut request_stream = server_end.into_stream();
137        let task = fasync::Task::local(async move {
138            let mut tasks = vec![];
139            while let Some(Ok(request)) = request_stream.next().await {
140                if let StoreRequest::CreateAccessor { accessor_request, .. } = request {
141                    let mut request_stream = accessor_request.into_stream();
142                    tasks.push(fasync::Task::local(async move {
143                        while let Some(Ok(request)) = request_stream.next().await {
144                            if let StoreAccessorRequest::GetValue { responder, .. } = request {
145                                responder.send(None).expect("should be able to send response");
146                            } else {
147                                panic!("unexpected request: {request:?}");
148                            }
149                        }
150                    }))
151                }
152            }
153            for task in tasks {
154                task.await
155            }
156        });
157
158        let migration = V1653667208LightMigration(store_proxy);
159        let fs = tempfile::tempdir().expect("failed to create tempdir");
160        let directory = open_tempdir(&fs);
161        let file_generator = FileGenerator::new(0, migration.id(), Clone::clone(&directory));
162        assert_matches!(migration.migrate(file_generator).await, Err(MigrationError::NoData));
163
164        drop(migration);
165
166        task.await;
167    }
168
169    // Ensure we can properly migrate the original json data in stash over to persistent fidl.
170    #[fuchsia::test]
171    async fn v1653667208_light_migration_test() {
172        let (store_proxy, server_end) = create_proxy::<StoreMarker>();
173        let mut request_stream = server_end.into_stream();
174        let task = fasync::Task::local(async move {
175            let mut tasks = vec![];
176            while let Some(Ok(request)) = request_stream.next().await {
177                if let StoreRequest::CreateAccessor { read_only, accessor_request, .. } = request {
178                    assert!(read_only);
179                    let mut request_stream = accessor_request.into_stream();
180                    tasks.push(fasync::Task::local(async move {
181                        while let Some(Ok(request)) = request_stream.next().await {
182                            if let StoreAccessorRequest::GetValue { key, responder } = request {
183                                assert_eq!(key, LIGHT_KEY);
184                                responder
185                                    .send(Some(Value::Stringval(
186                                        r#"{
187                                            "light_groups":{
188                                                "abc":{
189                                                    "name":"abc",
190                                                    "enabled":false,
191                                                    "light_type":"Brightness",
192                                                    "lights":[{
193                                                        "value": {
194                                                            "Brightness": 0.5
195                                                        }
196                                                    }],
197                                                    "hardware_index":[],
198                                                    "disable_conditions":[]
199                                                },
200                                                "def":{
201                                                    "name":"def",
202                                                    "enabled":true,
203                                                    "light_type":"Rgb",
204                                                    "lights":[{
205                                                        "value": {
206                                                            "Rgb": {
207                                                                "red": 1.0,
208                                                                "green": 0.5,
209                                                                "blue": 0.0
210                                                            }
211                                                        }
212                                                    }],
213                                                    "hardware_index":[1, 2, 3],
214                                                    "disable_conditions":[]
215                                                },
216                                                "ghi":{
217                                                    "name":"ghi",
218                                                    "enabled":false,
219                                                    "light_type":"Simple",
220                                                    "lights":[{
221                                                        "value": {
222                                                            "Simple": true
223                                                        }
224                                                    }],
225                                                    "hardware_index":[1],
226                                                    "disable_conditions":[]
227                                                },
228                                                "jkl":{
229                                                    "name":"jkl",
230                                                    "enabled":false,
231                                                    "light_type":"Simple",
232                                                    "lights":[{
233                                                        "value": null
234                                                    }],
235                                                    "hardware_index":[1],
236                                                    "disable_conditions":["MicSwitch"]
237                                                }
238                                            }
239                                        }"#
240                                        .to_owned(),
241                                    )))
242                                    .expect("should be able to respond");
243                            } else {
244                                panic!("unexpected request: {request:?}");
245                            }
246                        }
247                    }))
248                }
249            }
250            for task in tasks {
251                task.await
252            }
253        });
254
255        let migration = V1653667208LightMigration(store_proxy);
256        let fs = tempfile::tempdir().expect("failed to create tempdir");
257        let directory = open_tempdir(&fs);
258        let file_generator = FileGenerator::new(0, migration.id(), Clone::clone(&directory));
259        assert_matches!(migration.migrate(file_generator).await, Ok(()));
260
261        let file = fuchsia_fs::directory::open_file(
262            &directory,
263            "light_info_1653667208.pfidl",
264            fuchsia_fs::PERM_READABLE,
265        )
266        .await
267        .expect("file should exist");
268        let LightGroups { groups } = fuchsia_fs::file::read_fidl::<LightGroups>(&file)
269            .await
270            .expect("should be able to read and deserialize light groups");
271        assert_eq!(groups.len(), 4);
272        assert!(groups.contains(&LightGroup {
273            name: Some("abc".to_owned()),
274            enabled: Some(false),
275            type_: Some(LightType::Brightness),
276            lights: Some(vec![LightState {
277                value: Some(LightValue::Brightness(0.5)),
278                ..Default::default()
279            }]),
280            ..Default::default()
281        }));
282        assert!(groups.contains(&LightGroup {
283            name: Some("def".to_owned()),
284            enabled: Some(true),
285            type_: Some(LightType::Rgb),
286            lights: Some(vec![LightState {
287                value: Some(LightValue::Color(ColorRgb { red: 1.0, green: 0.5, blue: 0.0 })),
288                ..Default::default()
289            }]),
290            ..Default::default()
291        }));
292        assert!(groups.contains(&LightGroup {
293            name: Some("ghi".to_owned()),
294            enabled: Some(false),
295            type_: Some(LightType::Simple),
296            lights: Some(vec![LightState {
297                value: Some(LightValue::On(true)),
298                ..Default::default()
299            }]),
300            ..Default::default()
301        }));
302        assert!(groups.contains(&LightGroup {
303            name: Some("jkl".to_owned()),
304            enabled: Some(false),
305            type_: Some(LightType::Simple),
306            lights: Some(vec![LightState { value: None, ..Default::default() }]),
307            ..Default::default()
308        }));
309
310        drop(migration);
311
312        task.await;
313    }
314}