Skip to main content

vcon_server/
fs.rs

1//! The filesystem calls Store makes for object files, behind an interface
2//! so tests can simulate a crash. The catalog is SQLite, which does its own
3//! I/O and is durable once a commit returns.
4
5use std::ffi::OsString;
6use std::fs::{DirBuilder, File, OpenOptions};
7use std::io::{self, Write};
8use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
9use std::path::Path;
10
11/// The mode of every file the server makes: the service account's alone,
12/// whatever the umask of the process. The umask can only take bits away,
13/// and these have none for group or others to lose.
14pub const FILE_MODE: u32 = 0o600;
15
16/// The mode of every directory the server makes, as `FILE_MODE`.
17pub const DIR_MODE: u32 = 0o700;
18
19/// The filesystem calls Store makes.
20pub trait Fs: Send + Sync {
21    /// Creates a file that must not exist yet and writes `data` to it,
22    /// without syncing.
23    ///
24    /// # Errors
25    ///
26    /// `AlreadyExists` if the path is taken, `NotFound` if its directory
27    /// does not exist, or any other I/O error.
28    fn write_new(&self, path: &Path, data: &[u8]) -> io::Result<()>;
29    /// Makes a file's data durable.
30    ///
31    /// # Errors
32    ///
33    /// `NotFound` if there is no such file, or any other I/O error.
34    fn sync_file(&self, path: &Path) -> io::Result<()>;
35    /// Renames a file, replacing any file at `to`.
36    ///
37    /// # Errors
38    ///
39    /// The I/O error that stopped the rename.
40    fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
41    /// Makes a directory's entries durable: files created, renamed or
42    /// removed in it.
43    ///
44    /// # Errors
45    ///
46    /// `NotFound` if there is no such directory, or any other I/O error.
47    fn sync_dir(&self, dir: &Path) -> io::Result<()>;
48    /// Reads a whole file.
49    ///
50    /// # Errors
51    ///
52    /// `NotFound` if there is no such file, or any other I/O error.
53    fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
54    /// Removes a file.
55    ///
56    /// # Errors
57    ///
58    /// `NotFound` if there is no such file, an error if the path is a
59    /// directory, or any other I/O error.
60    fn remove(&self, path: &Path) -> io::Result<()>;
61    /// The names in a directory, sorted.
62    ///
63    /// # Errors
64    ///
65    /// `NotFound` if there is no such directory, or any other I/O error.
66    fn list(&self, dir: &Path) -> io::Result<Vec<OsString>>;
67    /// Creates a directory and its parents, without syncing. A new
68    /// directory survives a crash once the directory holding it is synced.
69    ///
70    /// # Errors
71    ///
72    /// An error if a file stands in the path, or any other I/O error.
73    fn create_dir_all(&self, dir: &Path) -> io::Result<()>;
74    /// Removes a directory and everything in it, without syncing. The
75    /// removal survives a crash once the directory that held it is synced.
76    ///
77    /// # Errors
78    ///
79    /// `NotFound` if there is no such directory, an error if the path is a
80    /// file, or any other I/O error.
81    fn remove_dir_all(&self, dir: &Path) -> io::Result<()>;
82}
83
84/// The operating system's filesystem. No test can see whether a sync
85/// reached the disk without cutting power, so Store's crash tests use the
86/// crash filesystem's model of syncing instead.
87#[derive(Debug, Clone, Copy, Default)]
88pub struct OsFs;
89
90impl Fs for OsFs {
91    fn write_new(&self, path: &Path, data: &[u8]) -> io::Result<()> {
92        OpenOptions::new()
93            .write(true)
94            .create_new(true)
95            .mode(FILE_MODE)
96            .open(path)?
97            .write_all(data)
98    }
99
100    fn sync_file(&self, path: &Path) -> io::Result<()> {
101        File::open(path)?.sync_all()
102    }
103
104    fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
105        std::fs::rename(from, to)
106    }
107
108    fn sync_dir(&self, dir: &Path) -> io::Result<()> {
109        File::open(dir)?.sync_all()
110    }
111
112    fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
113        std::fs::read(path)
114    }
115
116    fn remove(&self, path: &Path) -> io::Result<()> {
117        std::fs::remove_file(path)
118    }
119
120    fn list(&self, dir: &Path) -> io::Result<Vec<OsString>> {
121        let mut names = std::fs::read_dir(dir)?
122            .map(|entry| entry.map(|e| e.file_name()))
123            .collect::<io::Result<Vec<_>>>()?;
124        names.sort();
125        Ok(names)
126    }
127
128    fn create_dir_all(&self, dir: &Path) -> io::Result<()> {
129        DirBuilder::new().recursive(true).mode(DIR_MODE).create(dir)
130    }
131
132    fn remove_dir_all(&self, dir: &Path) -> io::Result<()> {
133        std::fs::remove_dir_all(dir)
134    }
135}
136
137#[cfg(test)]
138pub(crate) use crash::{CrashFs, Op};
139
140#[cfg(test)]
141mod crash {
142    use std::collections::BTreeMap;
143    use std::ffi::OsString;
144    use std::io;
145    use std::path::{Path, PathBuf};
146    use std::sync::Mutex;
147
148    use super::Fs;
149
150    /// One call, as the crash filesystem logs it.
151    #[derive(Debug, Clone, PartialEq, Eq)]
152    pub(crate) enum Op {
153        Write(PathBuf),
154        SyncFile(PathBuf),
155        Rename(PathBuf, PathBuf),
156        SyncDir(PathBuf),
157        Remove(PathBuf),
158        CreateDir(PathBuf),
159        RemoveDir(PathBuf),
160    }
161
162    /// An in-memory filesystem that keeps what was synced apart from what
163    /// was not, so a simulated crash can drop the latter. A file's data
164    /// survives a crash once the file is synced; an entry in a directory, a
165    /// file or a directory made, renamed or removed there, once that
166    /// directory is synced. A directory lost in a crash takes everything
167    /// in it.
168    ///
169    /// The model is rooted at a directory that exists on disk and is
170    /// durable. A directory made in the model is made on disk too, because
171    /// SQLite opens its catalogs there; a crash drops it only from the model.
172    /// A directory is removed only from the model, since a crash may bring
173    /// it back and SQLite's files in it must then still be there.
174    pub(crate) struct CrashFs {
175        state: Mutex<State>,
176    }
177
178    struct State {
179        root: PathBuf,
180        dirs: BTreeMap<PathBuf, Dir>,
181        files: Vec<File>,
182        log: Vec<Op>,
183        /// From this many changes on, every change fails.
184        fail_from: Option<usize>,
185    }
186
187    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
188    enum Entry {
189        /// An index into `State::files`.
190        File(usize),
191        Dir,
192    }
193
194    /// A directory's entries, as they are and as a crash would leave them.
195    #[derive(Default)]
196    struct Dir {
197        now: BTreeMap<OsString, Entry>,
198        synced: BTreeMap<OsString, Entry>,
199    }
200
201    struct File {
202        now: Vec<u8>,
203        synced: Vec<u8>,
204    }
205
206    fn not_found() -> io::Error {
207        io::Error::from(io::ErrorKind::NotFound)
208    }
209
210    /// A path's directory and name.
211    fn split(path: &Path) -> io::Result<(PathBuf, OsString)> {
212        match (path.parent(), path.file_name()) {
213            (Some(dir), Some(name)) => Ok((dir.to_owned(), name.to_owned())),
214            _ => Err(io::Error::from(io::ErrorKind::InvalidInput)),
215        }
216    }
217
218    impl State {
219        /// Fails once the fail point is reached, as a dead process would.
220        fn alive(&self) -> io::Result<()> {
221            match self.fail_from {
222                Some(n) if self.log.len() >= n => Err(io::Error::other("the process has died")),
223                _ => Ok(()),
224            }
225        }
226
227        /// Whether `dir` is the root or is reached from it through
228        /// entries as they are now.
229        fn exists(&self, dir: &Path) -> bool {
230            if dir == self.root {
231                return true;
232            }
233            let Ok((parent, name)) = split(dir) else {
234                return false;
235            };
236            self.exists(&parent)
237                && self
238                    .dirs
239                    .get(&parent)
240                    .is_some_and(|p| p.now.get(&name) == Some(&Entry::Dir))
241        }
242
243        fn dir(&mut self, dir: &Path) -> io::Result<&mut Dir> {
244            if !self.exists(dir) {
245                return Err(not_found());
246            }
247            self.dirs.get_mut(dir).ok_or_else(not_found)
248        }
249
250        fn file(&mut self, path: &Path) -> io::Result<usize> {
251            let (dir, name) = split(path)?;
252            match self.dir(&dir)?.now.get(&name) {
253                Some(Entry::File(index)) => Ok(*index),
254                _ => Err(not_found()),
255            }
256        }
257    }
258
259    impl CrashFs {
260        /// A model rooted at `root`, an existing directory.
261        pub(crate) fn new(root: &Path) -> CrashFs {
262            CrashFs {
263                state: Mutex::new(State {
264                    root: root.to_owned(),
265                    dirs: BTreeMap::from([(root.to_owned(), Dir::default())]),
266                    files: Vec::new(),
267                    log: Vec::new(),
268                    fail_from: None,
269                }),
270            }
271        }
272
273        fn state(&self) -> std::sync::MutexGuard<'_, State> {
274            self.state
275                .lock()
276                .expect("no test panicked holding the lock")
277        }
278
279        /// Loses everything not yet synced.
280        pub(crate) fn crash(&self) {
281            let mut state = self.state();
282            state.fail_from = None;
283            for dir in state.dirs.values_mut() {
284                dir.now = dir.synced.clone();
285            }
286            for file in &mut state.files {
287                file.now = file.synced.clone();
288            }
289        }
290
291        /// From the `n`th change on (counting from 0 over the whole log),
292        /// every change fails and changes nothing, as for a process that
293        /// has died. `crash` clears it.
294        pub(crate) fn fail_from(&self, n: usize) {
295            self.state().fail_from = Some(n);
296        }
297
298        /// Every change so far, in order.
299        pub(crate) fn log(&self) -> Vec<Op> {
300            self.state().log.clone()
301        }
302    }
303
304    impl Fs for CrashFs {
305        fn write_new(&self, path: &Path, data: &[u8]) -> io::Result<()> {
306            let (dir, name) = split(path)?;
307            let mut state = self.state();
308            state.alive()?;
309            let index = state.files.len();
310            let entries = &mut state.dir(&dir)?.now;
311            if entries.contains_key(&name) {
312                return Err(io::Error::from(io::ErrorKind::AlreadyExists));
313            }
314            entries.insert(name, Entry::File(index));
315            state.files.push(File {
316                now: data.to_vec(),
317                synced: Vec::new(),
318            });
319            state.log.push(Op::Write(path.to_owned()));
320            Ok(())
321        }
322
323        fn sync_file(&self, path: &Path) -> io::Result<()> {
324            let mut state = self.state();
325            state.alive()?;
326            let index = state.file(path)?;
327            let file = &mut state.files[index];
328            file.synced = file.now.clone();
329            state.log.push(Op::SyncFile(path.to_owned()));
330            Ok(())
331        }
332
333        fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
334            let ((from_dir, from_name), (to_dir, to_name)) = (split(from)?, split(to)?);
335            let mut state = self.state();
336            state.alive()?;
337            let index = state.file(from)?;
338            state.dir(&to_dir)?;
339            state.dir(&from_dir)?.now.remove(&from_name);
340            state.dir(&to_dir)?.now.insert(to_name, Entry::File(index));
341            state.log.push(Op::Rename(from.to_owned(), to.to_owned()));
342            Ok(())
343        }
344
345        fn sync_dir(&self, dir: &Path) -> io::Result<()> {
346            let mut state = self.state();
347            state.alive()?;
348            let entries = state.dir(dir)?;
349            entries.synced = entries.now.clone();
350            state.log.push(Op::SyncDir(dir.to_owned()));
351            Ok(())
352        }
353
354        fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
355            let mut state = self.state();
356            let index = state.file(path)?;
357            Ok(state.files[index].now.clone())
358        }
359
360        fn remove(&self, path: &Path) -> io::Result<()> {
361            let (dir, name) = split(path)?;
362            let mut state = self.state();
363            state.alive()?;
364            state.file(path)?;
365            state.dir(&dir)?.now.remove(&name);
366            state.log.push(Op::Remove(path.to_owned()));
367            Ok(())
368        }
369
370        fn list(&self, dir: &Path) -> io::Result<Vec<OsString>> {
371            Ok(self.state().dir(dir)?.now.keys().cloned().collect())
372        }
373
374        fn create_dir_all(&self, dir: &Path) -> io::Result<()> {
375            let mut state = self.state();
376            state.alive()?;
377            let below = dir
378                .strip_prefix(&state.root)
379                .map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?
380                .to_owned();
381            // Find what is missing before touching the disk, so a refusal
382            // makes nothing there either.
383            let mut path = state.root.clone();
384            let mut missing = Vec::new();
385            for part in &below {
386                let next = path.join(part);
387                let parent = std::mem::replace(&mut path, next);
388                if !missing.is_empty() {
389                    missing.push(path.clone());
390                    continue;
391                }
392                match state.dir(&parent)?.now.get(part) {
393                    Some(Entry::Dir) => {}
394                    Some(Entry::File(_)) => {
395                        return Err(io::Error::from(io::ErrorKind::AlreadyExists));
396                    }
397                    None => missing.push(path.clone()),
398                }
399            }
400            if missing.is_empty() {
401                return Ok(());
402            }
403            std::fs::create_dir_all(dir)?;
404            for path in missing {
405                let (parent, name) = split(&path)?;
406                state.dir(&parent)?.now.insert(name, Entry::Dir);
407                state.dirs.insert(path, Dir::default());
408            }
409            state.log.push(Op::CreateDir(dir.to_owned()));
410            Ok(())
411        }
412
413        fn remove_dir_all(&self, dir: &Path) -> io::Result<()> {
414            let (parent, name) = split(dir)?;
415            let mut state = self.state();
416            state.alive()?;
417            if !state.exists(dir) {
418                return Err(not_found());
419            }
420            state.dir(&parent)?.now.remove(&name);
421            state.log.push(Op::RemoveDir(dir.to_owned()));
422            Ok(())
423        }
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use std::io::ErrorKind::{AlreadyExists, NotFound};
430    use std::path::PathBuf;
431
432    use super::*;
433
434    /// What every implementation must do, crash or no crash.
435    fn behaves_like_a_filesystem(fs: &dyn Fs, root: &Path) {
436        let dir = root.join("objects");
437        let (a, b) = (dir.join("a"), dir.join("b"));
438        assert_eq!(
439            fs.write_new(&a, b"x").map_err(|e| e.kind()),
440            Err(NotFound),
441            "no dir yet"
442        );
443        fs.create_dir_all(&dir).expect("create the dir");
444        fs.create_dir_all(&dir).expect("create it again");
445        fs.write_new(&a, b"one").expect("write");
446        assert_eq!(
447            fs.write_new(&a, b"two").map_err(|e| e.kind()),
448            Err(AlreadyExists),
449            "write_new never replaces a file"
450        );
451        assert!(fs.create_dir_all(&a).is_err(), "a file has the name");
452        assert!(
453            fs.create_dir_all(&a.join("d")).is_err(),
454            "a file stands in the way"
455        );
456        assert!(!a.join("d").exists(), "and nothing is made on disk");
457        assert_eq!(
458            fs.read(&a).expect("read"),
459            b"one",
460            "the refused write left the first one"
461        );
462        fs.sync_file(&a).expect("sync the file");
463        fs.sync_dir(&dir).expect("sync the dir");
464        fs.rename(&a, &b).expect("rename");
465        assert_eq!(
466            fs.read(&a).map_err(|e| e.kind()),
467            Err(NotFound),
468            "rename moves the file away"
469        );
470        assert_eq!(
471            fs.read(&b).expect("read"),
472            b"one",
473            "the file is under its new name"
474        );
475        fs.rename(&b, &b).expect("rename onto itself");
476        assert_eq!(
477            fs.read(&b).expect("read"),
478            b"one",
479            "renaming onto itself changes nothing"
480        );
481        fs.write_new(&a, b"three").expect("write again");
482        fs.rename(&a, &b).expect("rename over");
483        assert_eq!(
484            fs.read(&b).expect("read"),
485            b"three",
486            "rename replaces the target"
487        );
488        fs.write_new(&a, b"four").expect("write");
489        fs.create_dir_all(&dir.join("c"))
490            .expect("a directory inside");
491        assert_eq!(
492            fs.list(&dir).expect("list"),
493            ["a", "b", "c"].map(OsString::from),
494            "files and directories, sorted"
495        );
496        assert_eq!(
497            fs.write_new(&dir.join("c"), b"x").map_err(|e| e.kind()),
498            Err(AlreadyExists),
499            "a directory's name is taken"
500        );
501        assert!(
502            fs.remove(&dir.join("c")).is_err(),
503            "remove takes only a file"
504        );
505        assert!(
506            fs.remove_dir_all(&a).is_err(),
507            "remove_dir_all takes only a directory"
508        );
509        fs.create_dir_all(&dir.join("c/d"))
510            .expect("a directory inside c");
511        fs.write_new(&dir.join("c/f"), b"x")
512            .expect("a file inside c");
513        fs.remove_dir_all(&dir.join("c"))
514            .expect("remove c and all in it");
515        assert_eq!(
516            fs.list(&dir.join("c")).map_err(|e| e.kind()),
517            Err(NotFound),
518            "remove_dir_all removed c itself"
519        );
520        assert_eq!(
521            fs.remove_dir_all(&dir.join("c")).map_err(|e| e.kind()),
522            Err(NotFound),
523            "a directory that is gone cannot be removed again"
524        );
525        fs.create_dir_all(&dir.join("c")).expect("c again");
526        fs.remove(&a).expect("remove");
527        fs.remove(&b).expect("remove");
528        assert_eq!(
529            fs.list(&dir).expect("list"),
530            [OsString::from("c")],
531            "the removed files are gone and c is left"
532        );
533        assert_eq!(
534            fs.remove(&b).map_err(|e| e.kind()),
535            Err(NotFound),
536            "b was removed"
537        );
538        assert_eq!(
539            fs.sync_file(&b).map_err(|e| e.kind()),
540            Err(NotFound),
541            "b was removed"
542        );
543        assert_eq!(
544            fs.sync_dir(&root.join("none")).map_err(|e| e.kind()),
545            Err(NotFound),
546            "no directory has that name"
547        );
548        assert_eq!(
549            fs.list(&root.join("none")).map_err(|e| e.kind()),
550            Err(NotFound),
551            "no directory has that name"
552        );
553    }
554
555    #[test]
556    fn the_os_filesystem_behaves_like_a_filesystem() {
557        let root = tempfile::tempdir().expect("temp dir");
558        behaves_like_a_filesystem(&OsFs, root.path());
559    }
560
561    #[test]
562    fn the_crash_filesystem_behaves_like_a_filesystem() {
563        let root = tempfile::tempdir().expect("temp dir");
564        behaves_like_a_filesystem(&CrashFs::new(root.path()), root.path());
565    }
566
567    /// A crash filesystem rooted at a temporary directory.
568    struct Model {
569        root: tempfile::TempDir,
570        fs: CrashFs,
571    }
572
573    impl Model {
574        fn new() -> Model {
575            let root = tempfile::tempdir().expect("temp dir");
576            let fs = CrashFs::new(root.path());
577            Model { root, fs }
578        }
579
580        fn root(&self) -> &Path {
581            self.root.path()
582        }
583
584        /// The directory the file tests use, made and synced.
585        fn dir(&self) -> PathBuf {
586            self.root().join("objects")
587        }
588
589        fn f(&self) -> PathBuf {
590            self.dir().join("f")
591        }
592    }
593
594    /// A model with a durable `dir()`, after `steps` on its file `f()`.
595    fn after(steps: impl FnOnce(&CrashFs, &Path)) -> Model {
596        let m = Model::new();
597        m.fs.create_dir_all(&m.dir()).expect("create the dir");
598        m.fs.sync_dir(m.root()).expect("sync the root");
599        steps(&m.fs, &m.f());
600        m
601    }
602
603    fn synced(fs: &CrashFs, f: &Path) {
604        fs.write_new(f, b"data").expect("write");
605        fs.sync_file(f).expect("sync the file");
606        fs.sync_dir(f.parent().expect("a dir"))
607            .expect("sync the dir");
608    }
609
610    #[test]
611    fn a_crash_drops_a_file_whose_entry_was_not_synced() {
612        for sync_data in [false, true] {
613            let m = after(|fs, f| {
614                fs.write_new(f, b"data").expect("write");
615                if sync_data {
616                    fs.sync_file(f).expect("sync the file");
617                }
618            });
619            m.fs.crash();
620            assert_eq!(
621                m.fs.read(&m.f()).map_err(|e| e.kind()),
622                Err(NotFound),
623                "the entry was never synced, so the crash loses the file (data synced: {sync_data})"
624            );
625        }
626    }
627
628    #[test]
629    fn a_crash_empties_a_file_whose_data_was_not_synced() {
630        let m = after(|fs, f| {
631            fs.write_new(f, b"data").expect("write");
632            fs.sync_dir(f.parent().expect("a dir"))
633                .expect("sync the dir");
634        });
635        m.fs.crash();
636        assert_eq!(
637            m.fs.read(&m.f()).expect("the entry survives"),
638            b"",
639            "the entry was synced but the data was not, so the file comes back empty"
640        );
641    }
642
643    #[test]
644    fn a_crash_keeps_a_file_synced_in_both_ways() {
645        let m = after(synced);
646        m.fs.crash();
647        assert_eq!(
648            m.fs.read(&m.f()).expect("survives"),
649            b"data",
650            "data and entry were both synced, so the crash keeps the file whole"
651        );
652    }
653
654    #[test]
655    fn a_rename_or_remove_survives_a_crash_only_once_the_dir_is_synced() {
656        for sync in [false, true] {
657            let renamed = after(|fs, f| {
658                synced(fs, f);
659                fs.rename(f, &f.with_file_name("g")).expect("rename");
660                if sync {
661                    fs.sync_dir(f.parent().expect("a dir"))
662                        .expect("sync the dir");
663                }
664            });
665            renamed.fs.crash();
666            let names = if sync { ["g"] } else { ["f"] };
667            assert_eq!(
668                renamed.fs.list(&renamed.dir()).expect("list"),
669                names.map(OsString::from),
670                "rename, sync {sync}"
671            );
672            assert_eq!(
673                renamed
674                    .fs
675                    .read(&renamed.dir().join(names[0]))
676                    .expect("read"),
677                b"data",
678                "whichever name survives, the synced data is under it"
679            );
680
681            let removed = after(|fs, f| {
682                synced(fs, f);
683                fs.remove(f).expect("remove");
684                if sync {
685                    fs.sync_dir(f.parent().expect("a dir"))
686                        .expect("sync the dir");
687                }
688            });
689            removed.fs.crash();
690            let left = removed.fs.list(&removed.dir()).expect("list").len();
691            assert_eq!(left, usize::from(!sync), "remove, sync {sync}");
692        }
693    }
694
695    #[test]
696    fn a_new_directory_survives_a_crash_only_once_its_parent_is_synced() {
697        let m = Model::new();
698        let (a, b) = (m.root().join("a"), m.root().join("a/b"));
699        m.fs.create_dir_all(&b).expect("create a/b");
700        m.fs.crash();
701        assert_eq!(
702            m.fs.list(m.root()).expect("list"),
703            Vec::<OsString>::new(),
704            "nothing was synced, so the crash loses a"
705        );
706        assert_eq!(
707            m.fs.list(&a).map_err(|e| e.kind()),
708            Err(NotFound),
709            "a is gone after the crash"
710        );
711
712        m.fs.create_dir_all(&b).expect("create a/b");
713        m.fs.sync_dir(&a).expect("sync a, not the root");
714        m.fs.crash();
715        assert_eq!(
716            m.fs.list(&b).map_err(|e| e.kind()),
717            Err(NotFound),
718            "b is lost with a"
719        );
720
721        m.fs.create_dir_all(&b).expect("create a/b");
722        m.fs.sync_dir(m.root()).expect("sync the root");
723        m.fs.crash();
724        assert_eq!(
725            m.fs.list(m.root()).expect("list"),
726            [OsString::from("a")],
727            "the root was synced, so its entry for a survives"
728        );
729        assert_eq!(
730            m.fs.list(&a).expect("a survives"),
731            Vec::<OsString>::new(),
732            "b's entry in a was never synced"
733        );
734
735        m.fs.create_dir_all(&b).expect("create b");
736        m.fs.sync_dir(&a).expect("sync a");
737        m.fs.crash();
738        assert_eq!(
739            m.fs.list(&b).expect("b survives"),
740            Vec::<OsString>::new(),
741            "a was synced, so its entry for b survives, and b is empty"
742        );
743    }
744
745    #[test]
746    fn a_lost_directory_takes_its_synced_files_with_it() {
747        let m = Model::new();
748        let a = m.root().join("a");
749        m.fs.create_dir_all(&a).expect("create a");
750        synced(&m.fs, &a.join("f"));
751        m.fs.crash();
752        assert_eq!(
753            m.fs.list(m.root()).expect("list"),
754            Vec::<OsString>::new(),
755            "the root was never synced, so a is lost with its synced file"
756        );
757        m.fs.create_dir_all(&a).expect("create a again");
758        assert_eq!(
759            m.fs.list(&a).expect("list"),
760            Vec::<OsString>::new(),
761            "a new directory starts empty"
762        );
763    }
764
765    #[test]
766    fn a_directory_removal_survives_a_crash_only_once_its_parent_is_synced() {
767        for sync in [false, true] {
768            let m = Model::new();
769            let a = m.root().join("a");
770            m.fs.create_dir_all(&a).expect("create a");
771            synced(&m.fs, &a.join("f"));
772            m.fs.sync_dir(m.root()).expect("sync the root");
773            m.fs.remove_dir_all(&a).expect("remove a");
774            if sync {
775                m.fs.sync_dir(m.root()).expect("sync the root");
776            }
777            m.fs.crash();
778            if sync {
779                assert_eq!(
780                    m.fs.list(&a).map_err(|e| e.kind()),
781                    Err(NotFound),
782                    "the root was synced after the removal, so it holds"
783                );
784            } else {
785                assert_eq!(
786                    m.fs.read(&a.join("f")).expect("back"),
787                    b"data",
788                    "the removal was never synced, so a comes back with its file"
789                );
790            }
791        }
792    }
793
794    /// A crash may bring a removed directory back, and SQLite's files in
795    /// it must then still be there, so removal touches only the model.
796    #[test]
797    fn a_directory_is_removed_only_from_the_model() {
798        let m = Model::new();
799        let a = m.root().join("a");
800        m.fs.create_dir_all(&a).expect("create a");
801        m.fs.remove_dir_all(&a).expect("remove a");
802        assert!(
803            a.is_dir(),
804            "the directory stays on disk, for a crash that brings it back"
805        );
806    }
807
808    /// SQLite opens its files on disk, so a directory made in the model is
809    /// made on disk too.
810    #[test]
811    fn a_directory_is_also_made_on_disk() {
812        let m = Model::new();
813        let b = m.root().join("a/b");
814        m.fs.create_dir_all(&b).expect("create a/b");
815        assert!(
816            b.is_dir(),
817            "the directory is on disk, where SQLite will open its files"
818        );
819    }
820
821    #[test]
822    fn nothing_outside_the_root_can_be_made() {
823        let m = Model::new();
824        let outside = m.root().with_file_name("elsewhere-5c1e");
825        assert!(
826            m.fs.create_dir_all(&outside).is_err(),
827            "the model refuses a path outside its root"
828        );
829        assert!(!outside.exists(), "and makes nothing there on disk");
830    }
831
832    #[test]
833    fn every_call_that_changes_state_is_logged_in_order() {
834        let m = Model::new();
835        let (dir, f, g) = (m.dir(), m.f(), m.dir().join("g"));
836        m.fs.create_dir_all(&dir).expect("create the dir");
837        m.fs.create_dir_all(&dir).expect("already there: no change");
838        synced(&m.fs, &f);
839        m.fs.rename(&f, &g).expect("rename");
840        m.fs.remove(&g).expect("remove");
841        m.fs.remove_dir_all(&dir).expect("remove the dir");
842        assert_eq!(
843            m.fs.log(),
844            [
845                Op::CreateDir(dir.clone()),
846                Op::Write(f.clone()),
847                Op::SyncFile(f.clone()),
848                Op::SyncDir(dir.clone()),
849                Op::Rename(f, g.clone()),
850                Op::Remove(g),
851                Op::RemoveDir(dir),
852            ],
853            "one entry per change, in order; the create that changed nothing is not logged"
854        );
855    }
856
857    #[test]
858    fn from_the_fail_point_on_no_change_happens_until_a_crash() {
859        let m = after(|fs, f| fs.write_new(f, b"data").expect("write"));
860        let ops = m.fs.log().len();
861        m.fs.fail_from(ops);
862        let (f, g) = (m.f(), m.dir().join("g"));
863        assert!(m.fs.sync_file(&f).is_err(), "sync");
864        assert!(m.fs.write_new(&g, b"x").is_err(), "write");
865        assert!(m.fs.rename(&f, &g).is_err(), "rename");
866        assert!(m.fs.sync_dir(&m.dir()).is_err(), "sync dir");
867        assert!(m.fs.remove(&f).is_err(), "remove");
868        assert!(
869            m.fs.create_dir_all(&m.dir().join("d")).is_err(),
870            "create dir"
871        );
872        assert!(m.fs.remove_dir_all(&m.dir()).is_err(), "remove dir");
873        assert!(!m.dir().join("d").exists(), "not made on disk either");
874        assert_eq!(m.fs.log().len(), ops, "nothing changed");
875        assert_eq!(
876            m.fs.read(&f).expect("reads still work"),
877            b"data",
878            "the fail point stops changes, not reads"
879        );
880        m.fs.crash();
881        m.fs.write_new(&g, b"x")
882            .expect("works again after the crash");
883    }
884}