settings/
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
5//! The `migration` module exposes structs and traits that can be used to write data migrations for
6//! the settings service.
7//!
8//! # Example:
9//! ```
10//! let mut builder = MigrationManagerBuilder::new();
11//! builder.register(1649199438, Box::new(|file_proxy_generator| async move {
12//!     let file_proxy = (file_proxy_generator)("setting_name").await?;
13//! }))?;
14//! builder.set_dir_proxy(dir_proxy);
15//! let migration_manager = builder.build();
16//! migration_manager.run_migrations().await?;
17//! ```
18
19use anyhow::{anyhow, bail, Context, Error};
20use async_trait::async_trait;
21use fidl_fuchsia_io::{DirectoryProxy, FileProxy, UnlinkOptions};
22use fuchsia_fs::directory::{readdir, DirEntry, DirentKind};
23use fuchsia_fs::file::WriteError;
24use fuchsia_fs::node::{OpenError, RenameError};
25use fuchsia_fs::Flags;
26
27use std::collections::{BTreeMap, HashSet};
28
29/// Errors that can occur during an individual migration.
30#[derive(thiserror::Error, Debug)]
31pub(crate) enum MigrationError {
32    #[error("The data to be migrated from does not exist")]
33    NoData,
34    #[error("The disk is full so the migration cannot be performed")]
35    DiskFull,
36    #[error("An unrecoverable error occurred")]
37    Unrecoverable(#[source] Error),
38}
39
40impl From<Error> for MigrationError {
41    fn from(e: Error) -> Self {
42        MigrationError::Unrecoverable(e)
43    }
44}
45
46/// The migration index is just the migration id.
47type MigrationIndex = u64;
48
49pub(crate) struct MigrationManager {
50    migrations: BTreeMap<MigrationIndex, Box<dyn Migration>>,
51    dir_proxy: DirectoryProxy,
52}
53
54pub(crate) struct MigrationManagerBuilder {
55    migrations: BTreeMap<MigrationIndex, Box<dyn Migration>>,
56    id_set: HashSet<u64>,
57    dir_proxy: Option<DirectoryProxy>,
58}
59
60pub(crate) const MIGRATION_FILE_NAME: &str = "migrations.txt";
61const TMP_MIGRATION_FILE_NAME: &str = "tmp_migrations.txt";
62
63impl MigrationManagerBuilder {
64    /// Construct a new [MigrationManagerBuilder]
65    pub(crate) fn new() -> Self {
66        Self { migrations: BTreeMap::new(), id_set: HashSet::new(), dir_proxy: None }
67    }
68
69    /// Register a `migration` with a unique `id`. This will fail if another migration with the same
70    /// `id` is already registered.
71    pub(crate) fn register(&mut self, migration: impl Migration + 'static) -> Result<(), Error> {
72        self.register_internal(Box::new(migration))
73    }
74
75    fn register_internal(&mut self, migration: Box<dyn Migration>) -> Result<(), Error> {
76        let id = migration.id();
77        if !self.id_set.insert(id) {
78            bail!("migration with id {id} already registered");
79        }
80
81        let _ = self.migrations.insert(id, migration);
82        Ok(())
83    }
84
85    /// Set the directory where migration data will be tracked.
86    pub(crate) fn set_migration_dir(&mut self, dir_proxy: DirectoryProxy) {
87        self.dir_proxy = Some(dir_proxy);
88    }
89
90    /// Construct a [MigrationManager] from the registered migrations and directory proxy. This
91    /// method will panic if no directory proxy is registered.
92    pub(crate) fn build(self) -> MigrationManager {
93        MigrationManager {
94            migrations: self.migrations,
95            dir_proxy: self.dir_proxy.expect("dir proxy must be provided"),
96        }
97    }
98}
99
100impl MigrationManager {
101    /// Run all registered migrations. On success will return final migration number if there are
102    /// any registered migrations. On error it will return the most recent migration number that was
103    /// able to complete, along with the migration error.
104    pub(crate) async fn run_migrations(
105        mut self,
106    ) -> Result<Option<LastMigration>, (Option<LastMigration>, MigrationError)> {
107        let last_migration = {
108            let migration_file = fuchsia_fs::directory::open_file(
109                &self.dir_proxy,
110                MIGRATION_FILE_NAME,
111                fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
112            )
113            .await;
114            match migration_file {
115                Err(e) => {
116                    if let OpenError::OpenError(zx::Status::NOT_FOUND) = e {
117                        if let Some(last_migration) = self
118                            .initialize_migrations()
119                            .await
120                            .context("failed to initialize migrations")
121                            .map_err(|e| (None, e.into()))?
122                        {
123                            last_migration
124                        } else {
125                            // There are no migrations to run.
126                            return Ok(None);
127                        }
128                    } else {
129                        return Err((
130                            None,
131                            Error::from(e).context("failed to load migrations").into(),
132                        ));
133                    }
134                }
135                Ok(migration_file) => {
136                    let migration_data = fuchsia_fs::file::read_to_string(&migration_file)
137                        .await
138                        .context("failed to read migrations.txt")
139                        .map_err(|e| (None, e.into()))?;
140                    let migration_id = migration_data
141                        .parse()
142                        .context("Failed to parse migration id from migrations.txt")
143                        .map_err(|e| (None, e.into()))?;
144                    if self.migrations.keys().any(|id| *id == migration_id) {
145                        LastMigration { migration_id }
146                    } else {
147                        log::warn!("Unknown migration {migration_id}, reverting to default data");
148
149                        // We don't know which migrations to run. Some future build may have
150                        // run migrations, then something caused a boot loop, and the system
151                        // reverted back to this build. This build doesn't know about the
152                        // future migrations, so it doesn't know what state the data is in.
153                        // Report an error but use the last known migration this build is aware of.
154                        // TODO(https://fxbug.dev/42068029) Remove light migration fallback once proper
155                        // fallback handling is in place. This is safe for now since Light settings
156                        // are always overwritten. The follow-up must be compatible with this
157                        // fallback.
158                        let last_migration = LastMigration { migration_id: 1653667210 };
159                        Self::write_migration_file(&self.dir_proxy, last_migration.migration_id)
160                            .await
161                            .map_err(|e| (Some(last_migration), e))?;
162
163                        return Err((
164                            Some(last_migration),
165                            MigrationError::Unrecoverable(anyhow!(format!(
166                                "Unknown migration {migration_id}. Using migration with id {}",
167                                last_migration.migration_id
168                            ))),
169                        ));
170                    }
171                }
172            }
173        };
174
175        // Track the last migration so we know whether to update the migration file.
176        let mut new_last_migration = last_migration;
177
178        // Skip over migrations already previously completed.
179        for (id, migration) in self
180            .migrations
181            .into_iter()
182            .skip_while(|&(id, _)| id != last_migration.migration_id)
183            .skip(1)
184        {
185            Self::run_single_migration(
186                &self.dir_proxy,
187                new_last_migration.migration_id,
188                &*migration,
189            )
190            .await
191            .map_err(|e| {
192                let e = match e {
193                    MigrationError::NoData => MigrationError::NoData,
194                    MigrationError::Unrecoverable(e) => {
195                        MigrationError::Unrecoverable(e.context(format!("Migration {id} failed")))
196                    }
197                    _ => e,
198                };
199                (Some(new_last_migration), e)
200            })?;
201            new_last_migration = LastMigration { migration_id: id };
202        }
203
204        // Remove old files that don't match the current pattern.
205        let file_suffix = format!("_{}.pfidl", new_last_migration.migration_id);
206        let entries: Vec<DirEntry> = readdir(&self.dir_proxy)
207            .await
208            .context("another error")
209            .map_err(|e| (Some(new_last_migration), e.into()))?;
210        let files = entries.into_iter().filter_map(|entry| {
211            if let DirentKind::File = entry.kind {
212                if entry.name == MIGRATION_FILE_NAME || entry.name.ends_with(&file_suffix) {
213                    None
214                } else {
215                    Some(entry.name)
216                }
217            } else {
218                None
219            }
220        });
221
222        for file in files {
223            self.dir_proxy
224                .unlink(&file, &UnlinkOptions::default())
225                .await
226                .context("failed to remove old file from file system")
227                .map_err(|e| (Some(new_last_migration), e.into()))?
228                .map_err(zx::Status::from_raw)
229                .context("another error")
230                .map_err(|e| (Some(new_last_migration), e.into()))?;
231        }
232
233        Ok(Some(new_last_migration))
234    }
235
236    async fn write_migration_file(
237        dir_proxy: &DirectoryProxy,
238        migration_id: u64,
239    ) -> Result<(), MigrationError> {
240        // Scope is important. tmp_migration_file needs to be out of scope when the file is
241        // renamed.
242        {
243            let tmp_migration_file = fuchsia_fs::directory::open_file(
244                dir_proxy,
245                TMP_MIGRATION_FILE_NAME,
246                Flags::FLAG_MAYBE_CREATE | fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
247            )
248            .await
249            .context("unable to create migrations file")?;
250            if let Err(e) =
251                fuchsia_fs::file::write(&tmp_migration_file, migration_id.to_string()).await
252            {
253                return Err(match e {
254                    WriteError::WriteError(zx::Status::NO_SPACE) => MigrationError::DiskFull,
255                    _ => Error::from(e).context("failed to write tmp migration").into(),
256                });
257            };
258            if let Err(e) = tmp_migration_file
259                .close()
260                .await
261                .map_err(Error::from)
262                .context("failed to close")?
263                .map_err(zx::Status::from_raw)
264            {
265                return Err(match e {
266                    zx::Status::NO_SPACE => MigrationError::DiskFull,
267                    _ => anyhow!("{e:?}").context("failed to properly close migration file").into(),
268                });
269            }
270        }
271
272        if let Err(e) =
273            fuchsia_fs::directory::rename(dir_proxy, TMP_MIGRATION_FILE_NAME, MIGRATION_FILE_NAME)
274                .await
275        {
276            return Err(match e {
277                RenameError::RenameError(zx::Status::NO_SPACE) => MigrationError::DiskFull,
278                _ => Error::from(e).context("failed to rename tmp to migrations.txt").into(),
279            });
280        };
281
282        if let Err(e) = dir_proxy
283            .sync()
284            .await
285            .map_err(Error::from)
286            .context("failed to sync dir")?
287            .map_err(zx::Status::from_raw)
288        {
289            match e {
290                // This is only returned when the directory is backed by a VFS, so this is fine to
291                // ignore.
292                zx::Status::NOT_SUPPORTED => {}
293                zx::Status::NO_SPACE => return Err(MigrationError::DiskFull),
294                _ => {
295                    return Err(anyhow!("{e:?}")
296                        .context("failed to sync directory for migration id")
297                        .into())
298                }
299            }
300        }
301
302        Ok(())
303    }
304
305    async fn run_single_migration(
306        dir_proxy: &DirectoryProxy,
307        old_id: u64,
308        migration: &dyn Migration,
309    ) -> Result<(), MigrationError> {
310        let new_id = migration.id();
311        let file_generator = FileGenerator::new(old_id, new_id, Clone::clone(dir_proxy));
312        migration.migrate(file_generator).await?;
313        Self::write_migration_file(dir_proxy, new_id).await
314    }
315
316    /// Runs the initial migration. If it fails due to a [MigrationError::NoData] error, then it
317    /// will return the last migration to initialize all of the data sources.
318    async fn initialize_migrations(&mut self) -> Result<Option<LastMigration>, MigrationError> {
319        // Try to run the first migration. If it fails because there's no data in stash then we can
320        // use default values and skip to the final migration number. If it fails because the disk
321        // is full, propagate the error up so the main client can decide how to gracefully fallback.
322        let &id = if let Some(key) = self.migrations.keys().next() {
323            key
324        } else {
325            return Ok(None);
326        };
327        let migration = self.migrations.get(&id).unwrap();
328        if let Err(migration_error) =
329            Self::run_single_migration(&self.dir_proxy, 0, &**migration).await
330        {
331            match migration_error {
332                MigrationError::NoData => {
333                    // There was no previous data. We just need to use the default value for all
334                    // data and use the most recent migration as the migration number.
335                    let migration_id = self
336                        .migrations
337                        .keys()
338                        .last()
339                        .copied()
340                        .map(|id| LastMigration { migration_id: id });
341                    if let Some(last_migration) = migration_id.as_ref() {
342                        Self::write_migration_file(&self.dir_proxy, last_migration.migration_id)
343                            .await?;
344                    }
345                    return Ok(migration_id);
346                }
347                MigrationError::DiskFull => return Err(MigrationError::DiskFull),
348                MigrationError::Unrecoverable(e) => {
349                    return Err(MigrationError::Unrecoverable(
350                        e.context("Failed to run initial migration"),
351                    ))
352                }
353            }
354        }
355
356        Ok(Some(LastMigration { migration_id: id }))
357    }
358}
359
360pub(crate) struct FileGenerator {
361    // This will be used when migrating in between pfidl storage types,
362    // currently we only have migrations from stash to pfidl so it needs to
363    // annotated.
364    #[allow(dead_code)]
365    old_id: u64,
366    new_id: u64,
367    dir_proxy: DirectoryProxy,
368}
369
370impl FileGenerator {
371    pub(crate) fn new(old_id: u64, new_id: u64, dir_proxy: DirectoryProxy) -> Self {
372        Self { old_id, new_id, dir_proxy }
373    }
374
375    // This will be used when migrating in between pfidl storage types,
376    // currently we only have migrations from stash to pfidl so it needs to
377    // annotated.
378    #[allow(dead_code)]
379    pub(crate) async fn old_file(
380        &self,
381        file_name: impl AsRef<str>,
382    ) -> Result<FileProxy, OpenError> {
383        let file_name = file_name.as_ref();
384        let id = self.new_id;
385        fuchsia_fs::directory::open_file(
386            &self.dir_proxy,
387            &format!("{file_name}_{id}.pfidl"),
388            fuchsia_fs::PERM_READABLE,
389        )
390        .await
391    }
392
393    pub(crate) async fn new_file(
394        &self,
395        file_name: impl AsRef<str>,
396    ) -> Result<FileProxy, OpenError> {
397        let file_name = file_name.as_ref();
398        let id = self.new_id;
399        fuchsia_fs::directory::open_file(
400            &self.dir_proxy,
401            &format!("{file_name}_{id}.pfidl"),
402            Flags::FLAG_MAYBE_CREATE | fuchsia_fs::PERM_READABLE | fuchsia_fs::PERM_WRITABLE,
403        )
404        .await
405    }
406}
407
408#[async_trait(?Send)]
409pub(crate) trait Migration {
410    fn id(&self) -> u64;
411    async fn migrate(&self, file_generator: FileGenerator) -> Result<(), MigrationError>;
412}
413
414#[derive(Copy, Clone, Debug)]
415pub(crate) struct LastMigration {
416    pub(crate) migration_id: u64,
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422    use assert_matches::assert_matches;
423    use fidl_fuchsia_io::DirectoryMarker;
424    use futures::future::LocalBoxFuture;
425    use futures::FutureExt;
426    use std::rc::Rc;
427    use std::sync::atomic::{AtomicBool, Ordering};
428    use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
429
430    #[async_trait(?Send)]
431    impl<T> Migration for (u64, T)
432    where
433        T: Fn(FileGenerator) -> LocalBoxFuture<'static, Result<(), MigrationError>>,
434    {
435        fn id(&self) -> u64 {
436            self.0
437        }
438
439        async fn migrate(&self, file_generator: FileGenerator) -> Result<(), MigrationError> {
440            (self.1)(file_generator).await
441        }
442    }
443
444    const ID: u64 = 20_220_130_120_000;
445    const ID2: u64 = 20_220_523_120_000;
446    const DATA_FILE_NAME: &str = "test_20220130120000.pfidl";
447
448    #[fuchsia::test]
449    fn cannot_register_same_id_twice() {
450        let mut builder = MigrationManagerBuilder::new();
451        builder
452            .register((ID, Box::new(|_| async move { Ok(()) }.boxed_local())))
453            .expect("should register once");
454        let result = builder
455            .register((ID, Box::new(|_| async move { Ok(()) }.boxed_local())))
456            .map_err(|e| format!("{e:}"));
457        assert!(result.is_err());
458        assert_eq!(result.unwrap_err(), "migration with id 20220130120000 already registered");
459    }
460
461    #[fuchsia::test]
462    #[should_panic(expected = "dir proxy must be provided")]
463    fn cannot_build_migration_manager_without_dir_proxy() {
464        let builder = MigrationManagerBuilder::new();
465        let _ = builder.build();
466    }
467
468    // Needs to be async to create the proxy and stream.
469    #[fasync::run_until_stalled(test)]
470    async fn can_build_migration_manager_without_migrations() {
471        let (proxy, _) = fidl::endpoints::create_proxy_and_stream::<DirectoryMarker>();
472        let mut builder = MigrationManagerBuilder::new();
473        builder.set_migration_dir(proxy);
474        let _migration_manager = builder.build();
475    }
476
477    fn open_tempdir(tempdir: &tempfile::TempDir) -> fio::DirectoryProxy {
478        fuchsia_fs::directory::open_in_namespace(
479            tempdir.path().to_str().expect("tempdir path is not valid UTF-8"),
480            fio::PERM_READABLE | fio::PERM_WRITABLE,
481        )
482        .expect("failed to open connection to tempdir")
483    }
484
485    // Test for initial migration
486    #[fuchsia::test]
487    async fn if_no_migration_file_runs_initial_migration() {
488        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
489        let directory = open_tempdir(&tempdir);
490        let mut builder = MigrationManagerBuilder::new();
491        let migration_ran = Rc::new(AtomicBool::new(false));
492
493        builder
494            .register((
495                ID,
496                Box::new({
497                    let migration_ran = Rc::clone(&migration_ran);
498                    move |_| {
499                        let migration_ran = Rc::clone(&migration_ran);
500                        async move {
501                            migration_ran.store(true, Ordering::SeqCst);
502                            Ok(())
503                        }
504                        .boxed_local()
505                    }
506                }),
507            ))
508            .expect("can register");
509        builder.set_migration_dir(Clone::clone(&directory));
510        let migration_manager = builder.build();
511
512        let result = migration_manager.run_migrations().await;
513        assert_matches!(
514            result,
515            Ok(Some(LastMigration { migration_id: id })) if id == ID
516        );
517        let migration_file = fuchsia_fs::directory::open_file(
518            &directory,
519            MIGRATION_FILE_NAME,
520            fuchsia_fs::PERM_READABLE,
521        )
522        .await
523        .expect("migration file should exist");
524        let migration_number = fuchsia_fs::file::read_to_string(&migration_file)
525            .await
526            .expect("should be able to read file");
527        let migration_number: u64 = dbg!(migration_number).parse().expect("should be a number");
528        assert_eq!(migration_number, ID);
529    }
530
531    // Test for initial migration
532    #[fuchsia::test]
533    async fn if_no_migration_file_and_no_data_uses_defaults() {
534        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
535        let directory = open_tempdir(&tempdir);
536        let mut builder = MigrationManagerBuilder::new();
537        let migration_ran = Rc::new(AtomicBool::new(false));
538
539        builder
540            .register((
541                ID,
542                Box::new({
543                    let migration_ran = Rc::clone(&migration_ran);
544                    move |_| {
545                        let migration_ran = Rc::clone(&migration_ran);
546                        async move {
547                            migration_ran.store(true, Ordering::SeqCst);
548                            Err(MigrationError::NoData)
549                        }
550                        .boxed_local()
551                    }
552                }),
553            ))
554            .expect("can register");
555        builder.set_migration_dir(Clone::clone(&directory));
556        let migration_manager = builder.build();
557
558        let result = migration_manager.run_migrations().await;
559        assert_matches!(
560            result,
561            Ok(Some(LastMigration { migration_id: id })) if id == ID
562        );
563        let migration_file = fuchsia_fs::directory::open_file(
564            &directory,
565            MIGRATION_FILE_NAME,
566            fuchsia_fs::PERM_READABLE,
567        )
568        .await
569        .expect("migration file should exist");
570        let migration_number = fuchsia_fs::file::read_to_string(&migration_file)
571            .await
572            .expect("should be able to read file");
573        let migration_number: u64 = dbg!(migration_number).parse().expect("should be a number");
574        assert_eq!(migration_number, ID);
575    }
576
577    #[fuchsia::test]
578    async fn if_no_migration_file_and_no_migrations_no_update() {
579        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
580        let directory = open_tempdir(&tempdir);
581        let mut builder = MigrationManagerBuilder::new();
582        builder.set_migration_dir(Clone::clone(&directory));
583        let migration_manager = builder.build();
584
585        let result = migration_manager.run_migrations().await;
586        assert_matches!(result, Ok(None));
587        let open_result = fuchsia_fs::directory::open_file(
588            &directory,
589            MIGRATION_FILE_NAME,
590            fuchsia_fs::PERM_READABLE,
591        )
592        .await;
593        assert_matches!(
594            open_result,
595            Err(OpenError::OpenError(status)) if status == zx::Status::NOT_FOUND
596        );
597    }
598
599    #[fuchsia::test]
600    async fn if_no_migration_file_but_data_runs_migrations() {
601        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
602        let directory = open_tempdir(&tempdir);
603        let mut builder = MigrationManagerBuilder::new();
604        let migration_1_ran = Rc::new(AtomicBool::new(false));
605        let migration_2_ran = Rc::new(AtomicBool::new(false));
606
607        builder
608            .register((
609                ID,
610                Box::new({
611                    let migration_ran = Rc::clone(&migration_1_ran);
612                    move |_| {
613                        let migration_ran = Rc::clone(&migration_ran);
614                        async move {
615                            migration_ran.store(true, Ordering::SeqCst);
616                            Ok(())
617                        }
618                        .boxed_local()
619                    }
620                }),
621            ))
622            .expect("can register");
623
624        builder
625            .register((
626                ID2,
627                Box::new({
628                    let migration_ran = Rc::clone(&migration_2_ran);
629                    move |_| {
630                        let migration_ran = Rc::clone(&migration_ran);
631                        async move {
632                            migration_ran.store(true, Ordering::SeqCst);
633                            Ok(())
634                        }
635                        .boxed_local()
636                    }
637                }),
638            ))
639            .expect("can register");
640        builder.set_migration_dir(Clone::clone(&directory));
641        let migration_manager = builder.build();
642
643        let result = migration_manager.run_migrations().await;
644        assert_matches!(
645            result,
646            Ok(Some(LastMigration { migration_id: id })) if id == ID2
647        );
648        assert!(migration_1_ran.load(Ordering::SeqCst));
649        assert!(migration_2_ran.load(Ordering::SeqCst));
650    }
651
652    #[fuchsia::test]
653    async fn if_migration_file_and_up_to_date_no_migrations_run() {
654        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
655        std::fs::write(tempdir.path().join(MIGRATION_FILE_NAME), ID.to_string())
656            .expect("failed to write migration file");
657        std::fs::write(tempdir.path().join(DATA_FILE_NAME), "").expect("failed to write data file");
658        let directory = open_tempdir(&tempdir);
659        let mut builder = MigrationManagerBuilder::new();
660        let migration_ran = Rc::new(AtomicBool::new(false));
661
662        builder
663            .register((
664                ID,
665                Box::new({
666                    let migration_ran = Rc::clone(&migration_ran);
667                    move |_| {
668                        let migration_ran = Rc::clone(&migration_ran);
669                        async move {
670                            migration_ran.store(true, Ordering::SeqCst);
671                            Ok(())
672                        }
673                        .boxed_local()
674                    }
675                }),
676            ))
677            .expect("can register");
678        builder.set_migration_dir(directory);
679        let migration_manager = builder.build();
680
681        let result = migration_manager.run_migrations().await;
682        assert_matches!(
683            result,
684            Ok(Some(LastMigration { migration_id: id })) if id == ID
685        );
686        assert!(!migration_ran.load(Ordering::SeqCst));
687    }
688
689    #[fuchsia::test]
690    async fn migration_file_exists_but_missing_data() {
691        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
692        std::fs::write(tempdir.path().join(MIGRATION_FILE_NAME), ID.to_string())
693            .expect("failed to write migration file");
694        let directory = open_tempdir(&tempdir);
695        let mut builder = MigrationManagerBuilder::new();
696        let initial_migration_ran = Rc::new(AtomicBool::new(false));
697
698        builder
699            .register((
700                ID,
701                Box::new({
702                    let initial_migration_ran = Rc::clone(&initial_migration_ran);
703                    move |_| {
704                        let initial_migration_ran = Rc::clone(&initial_migration_ran);
705                        async move {
706                            initial_migration_ran.store(true, Ordering::SeqCst);
707                            Ok(())
708                        }
709                        .boxed_local()
710                    }
711                }),
712            ))
713            .expect("can register");
714
715        let second_migration_ran = Rc::new(AtomicBool::new(false));
716        builder
717            .register((
718                ID2,
719                Box::new({
720                    let second_migration_ran = Rc::clone(&second_migration_ran);
721                    move |_| {
722                        let second_migration_ran = Rc::clone(&second_migration_ran);
723                        async move {
724                            second_migration_ran.store(true, Ordering::SeqCst);
725                            Err(MigrationError::NoData)
726                        }
727                        .boxed_local()
728                    }
729                }),
730            ))
731            .expect("can register");
732        builder.set_migration_dir(directory);
733        let migration_manager = builder.build();
734
735        let result = migration_manager.run_migrations().await;
736        assert_matches!(
737            result,
738            Err((Some(LastMigration { migration_id: id }), MigrationError::NoData))
739                if id == ID
740        );
741        assert!(!initial_migration_ran.load(Ordering::SeqCst));
742        assert!(second_migration_ran.load(Ordering::SeqCst));
743    }
744
745    // Tests that a migration newer than the latest one tracked in the migration file can run.
746    #[fuchsia::test]
747    async fn migration_file_exists_and_newer_migrations_should_update() {
748        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
749        std::fs::write(tempdir.path().join(MIGRATION_FILE_NAME), ID.to_string())
750            .expect("failed to write migration file");
751        std::fs::write(tempdir.path().join(DATA_FILE_NAME), "").expect("failed to write data file");
752        let directory = open_tempdir(&tempdir);
753        let mut builder = MigrationManagerBuilder::new();
754        let migration_ran = Rc::new(AtomicBool::new(false));
755
756        builder
757            .register((
758                ID,
759                Box::new({
760                    let migration_ran = Rc::clone(&migration_ran);
761                    move |_| {
762                        let migration_ran = Rc::clone(&migration_ran);
763                        async move {
764                            migration_ran.store(true, Ordering::SeqCst);
765                            Ok(())
766                        }
767                        .boxed_local()
768                    }
769                }),
770            ))
771            .expect("can register");
772
773        const NEW_CONTENTS: &[u8] = b"test2";
774        builder
775            .register((
776                ID2,
777                Box::new({
778                    let migration_ran = Rc::clone(&migration_ran);
779                    move |file_generator: FileGenerator| {
780                        let migration_ran = Rc::clone(&migration_ran);
781                        async move {
782                            migration_ran.store(true, Ordering::SeqCst);
783                            let file = file_generator.new_file("test").await.expect("can get file");
784                            fuchsia_fs::file::write(&file, NEW_CONTENTS)
785                                .await
786                                .expect("can wite file");
787                            Ok(())
788                        }
789                        .boxed_local()
790                    }
791                }),
792            ))
793            .expect("can register");
794        builder.set_migration_dir(Clone::clone(&directory));
795        let migration_manager = builder.build();
796
797        let result = migration_manager.run_migrations().await;
798        assert_matches!(
799            result,
800            Ok(Some(LastMigration { migration_id: id })) if id == ID2
801        );
802
803        let migration_file = fuchsia_fs::directory::open_file(
804            &directory,
805            MIGRATION_FILE_NAME,
806            fuchsia_fs::PERM_READABLE,
807        )
808        .await
809        .expect("migration file should exist");
810        let migration_number: u64 = fuchsia_fs::file::read_to_string(&migration_file)
811            .await
812            .expect("should be able to read file")
813            .parse()
814            .expect("should be a number");
815        assert_eq!(migration_number, ID2);
816
817        let data_file = fuchsia_fs::directory::open_file(
818            &directory,
819            &format!("test_{ID2}.pfidl"),
820            fuchsia_fs::PERM_READABLE,
821        )
822        .await
823        .expect("migration file should exist");
824        let data = fuchsia_fs::file::read(&data_file).await.expect("should be able to read file");
825        assert_eq!(data, NEW_CONTENTS);
826    }
827
828    #[fuchsia::test]
829    async fn migration_file_unknown_id_uses_light_migration() {
830        const LIGHT_MIGRATION: u64 = 1653667210;
831        const UNKNOWN_ID: u64 = u64::MAX;
832        let tempdir = tempfile::tempdir().expect("failed to create tempdir");
833        std::fs::write(tempdir.path().join(MIGRATION_FILE_NAME), UNKNOWN_ID.to_string())
834            .expect("failed to write migration file");
835        std::fs::write(tempdir.path().join(DATA_FILE_NAME), "").expect("failed to write data file");
836        let directory = open_tempdir(&tempdir);
837        let mut builder = MigrationManagerBuilder::new();
838        let migration_ran = Rc::new(AtomicBool::new(false));
839
840        builder
841            .register((
842                ID,
843                Box::new({
844                    let migration_ran = Rc::clone(&migration_ran);
845                    move |_| {
846                        let migration_ran = Rc::clone(&migration_ran);
847                        async move {
848                            migration_ran.store(true, Ordering::SeqCst);
849                            Ok(())
850                        }
851                        .boxed_local()
852                    }
853                }),
854            ))
855            .expect("can register");
856        builder.set_migration_dir(Clone::clone(&directory));
857        let migration_manager = builder.build();
858
859        let result = migration_manager.run_migrations().await;
860        assert_matches!(result, Err((Some(LastMigration {
861                    migration_id
862                }), MigrationError::Unrecoverable(_)))
863            if migration_id == LIGHT_MIGRATION);
864
865        let migration_file = fuchsia_fs::directory::open_file(
866            &directory,
867            MIGRATION_FILE_NAME,
868            fuchsia_fs::PERM_READABLE,
869        )
870        .await
871        .expect("migration file should exist");
872        let migration_number: u64 = fuchsia_fs::file::read_to_string(&migration_file)
873            .await
874            .expect("should be able to read file")
875            .parse()
876            .expect("should be a number");
877        assert_eq!(migration_number, LIGHT_MIGRATION);
878    }
879}