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