From 681d0bb71ac8e15dc0ef534c7387e19eb6153a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Raz=20Guzm=C3=A1n=20Macedo?= Date: Tue, 3 Oct 2023 13:15:26 -0600 Subject: [PATCH] port durable file, add incremental tests --- exercise-book/src/durable-file.md | 62 +++++++++++ exercise-solutions/Cargo.toml | 1 + .../durable-file/step2/Cargo.toml | 8 ++ .../durable-file/step2/src/lib.rs | 14 +++ .../durable-file/step3/Cargo.toml | 8 ++ .../durable-file/step3/src/lib.rs | 30 +++++ .../durable-file/step4/Cargo.toml | 8 ++ .../durable-file/step4/src/lib.rs | 39 +++++++ .../durable-file/step5/Cargo.toml | 8 ++ .../durable-file/step5/src/lib.rs | 56 ++++++++++ .../durable-file/step6/Cargo.toml | 9 ++ .../durable-file/step6/src/lib.rs | 104 ++++++++++++++++++ 12 files changed, 347 insertions(+) create mode 100644 exercise-book/src/durable-file.md create mode 100644 exercise-solutions/durable-file/step2/Cargo.toml create mode 100644 exercise-solutions/durable-file/step2/src/lib.rs create mode 100644 exercise-solutions/durable-file/step3/Cargo.toml create mode 100644 exercise-solutions/durable-file/step3/src/lib.rs create mode 100644 exercise-solutions/durable-file/step4/Cargo.toml create mode 100644 exercise-solutions/durable-file/step4/src/lib.rs create mode 100644 exercise-solutions/durable-file/step5/Cargo.toml create mode 100644 exercise-solutions/durable-file/step5/src/lib.rs create mode 100644 exercise-solutions/durable-file/step6/Cargo.toml create mode 100644 exercise-solutions/durable-file/step6/src/lib.rs diff --git a/exercise-book/src/durable-file.md b/exercise-book/src/durable-file.md new file mode 100644 index 0000000..57b331d --- /dev/null +++ b/exercise-book/src/durable-file.md @@ -0,0 +1,62 @@ +# Durable File + +In this exercise, we will to implement `std` library traits for a `DurableFile`, a type of file which tracks it's syncs and writes. + +## After completing this exercise you will be able to + +* implement the `Drop` and `Write` traits. +* use the “Drop guard” pattern. +* test panicking APIs. + +## Prerequisites + +For completing this exercise you need to have: + +* an understanding of what `self`, `&self` and `&mut self` means +* knowledge of how to write and wrap a `struct` +* basic knowledge of how to write a `trait` +* the basics on using `Return` and `?` + +`std::fs::File` type has a [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) method which ensures that all data is written to disk. This method is not called by default: syncing is slow and the OS has good heuristics for optimistically delaying syncing. + +In this assignment, we’ll implement a `DurableFile` wrapper for `File`, which helps to ensure that applications calls [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all). Specifically, `DurableFile` tracks syncs and writes. If a `DurableFile` is dropped with an outstanding write, its `Drop` panics. Not calling [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) before disposing the file is considered a bug in this case. A `panic!` helps to elevate silent potential data loss into a loud failure. + +## Task + +1. Create a new libary crate with `cargo new --lib durable-file` +2. Implement the `DurableFile` data structure and its constructor + +```rust +use std::fs::File; + +struct DurableFile { + file: File, + needs_sync: bool, +} + +impl DurableFile { + pub fn new(file: std::fs::File) -> DurableFile { + ... + } +} +``` + +Optional: implement `From` for `DurableFile` + +3. Implement the [std::io::Write](https://doc.rust-lang.org/stable/std/io/trait.Write.html) trait for `DurableFile`. Use [sync_all](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) in the implementation of the flush method. All write operations should set the `needs_sync` flag, the flush method should clear it. + +4. Implement the [std::ops::Drop](https://doc.rust-lang.org/std/ops/trait.Drop.html) trait for `DurableFile` so that it panics if the file is not flushed. What is the right behavior for `Drop`? Are there any edge cases to worry about? + +5. Add an inherent `close` method for `DurableFile`, to explicitly sync, dispose the file, and handle potential errors. What is the appropriate signature (type of self and the return type) for close? + +6. Write a couple of simple smoke tests. You might find the [tempdir](https://docs.rs/tempdir/0.3.7/tempdir/) crate and [#[should_panic]](https://doc.rust-lang.org/reference/attributes/testing.html#the-should_panic-attribute) attribute useful! + +```rust +#[test] +#[should_panic(expected = "not yet implemented")] +fn smoke_test() { + let dir = tempdir::TempDir::new("tests").unwrap(); + let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap(); + todo!() +} +``` diff --git a/exercise-solutions/Cargo.toml b/exercise-solutions/Cargo.toml index a65fb3d..cfb46ee 100644 --- a/exercise-solutions/Cargo.toml +++ b/exercise-solutions/Cargo.toml @@ -9,4 +9,5 @@ members = [ "shapes-part-1", "shapes-part-2", "shapes-part-3", + "durable-file/*", ] diff --git a/exercise-solutions/durable-file/step2/Cargo.toml b/exercise-solutions/durable-file/step2/Cargo.toml new file mode 100644 index 0000000..0f2f7e3 --- /dev/null +++ b/exercise-solutions/durable-file/step2/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "step2" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/exercise-solutions/durable-file/step2/src/lib.rs b/exercise-solutions/durable-file/step2/src/lib.rs new file mode 100644 index 0000000..670415a --- /dev/null +++ b/exercise-solutions/durable-file/step2/src/lib.rs @@ -0,0 +1,14 @@ +use std::fs::File; +struct DurableFile { + file: File, + needs_sync: bool, +} + +impl DurableFile { + pub fn new(file: std::fs::File) -> DurableFile { + DurableFile { + file, + needs_sync: false, + } + } +} diff --git a/exercise-solutions/durable-file/step3/Cargo.toml b/exercise-solutions/durable-file/step3/Cargo.toml new file mode 100644 index 0000000..92b786c --- /dev/null +++ b/exercise-solutions/durable-file/step3/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "step3" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/exercise-solutions/durable-file/step3/src/lib.rs b/exercise-solutions/durable-file/step3/src/lib.rs new file mode 100644 index 0000000..e9190dd --- /dev/null +++ b/exercise-solutions/durable-file/step3/src/lib.rs @@ -0,0 +1,30 @@ +use std::fs::File; +use std::io::Write; + +struct DurableFile { + file: File, + needs_sync: bool, +} + +impl DurableFile { + pub fn new(file: std::fs::File) -> DurableFile { + DurableFile { + file, + needs_sync: false, + } + } +} + +impl Write for DurableFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let written_bytes = self.file.write(buf)?; + self.needs_sync = true; + Ok(written_bytes) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file.sync_all()?; + self.needs_sync = false; + Ok(()) + } +} \ No newline at end of file diff --git a/exercise-solutions/durable-file/step4/Cargo.toml b/exercise-solutions/durable-file/step4/Cargo.toml new file mode 100644 index 0000000..0271912 --- /dev/null +++ b/exercise-solutions/durable-file/step4/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "step4" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/exercise-solutions/durable-file/step4/src/lib.rs b/exercise-solutions/durable-file/step4/src/lib.rs new file mode 100644 index 0000000..a400cb6 --- /dev/null +++ b/exercise-solutions/durable-file/step4/src/lib.rs @@ -0,0 +1,39 @@ +use std::fs::File; +use std::io::Write; + +struct DurableFile { + file: File, + needs_sync: bool, +} + +impl DurableFile { + pub fn new(file: File) -> DurableFile { + DurableFile { + file, + needs_sync: false, + } + } +} + +impl Write for DurableFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let written_bytes = self.file.write(buf)?; + self.needs_sync = true; + Ok(written_bytes) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.file.sync_all()?; + self.needs_sync = false; + Ok(()) + } +} + +impl Drop for DurableFile { + fn drop(&mut self) { + // Any edge cases? + if self.needs_sync { + panic!("You forgot to sync!"); + } + } +} diff --git a/exercise-solutions/durable-file/step5/Cargo.toml b/exercise-solutions/durable-file/step5/Cargo.toml new file mode 100644 index 0000000..f3a6e7c --- /dev/null +++ b/exercise-solutions/durable-file/step5/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "step5" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/exercise-solutions/durable-file/step5/src/lib.rs b/exercise-solutions/durable-file/step5/src/lib.rs new file mode 100644 index 0000000..2feb254 --- /dev/null +++ b/exercise-solutions/durable-file/step5/src/lib.rs @@ -0,0 +1,56 @@ +use std::fs::File; +use std::io::{Result, Write}; + +#[derive(Debug)] +pub struct DurableFile { + file: File, + needs_sync: bool, +} + +#[derive(Debug)] +pub struct CloseError { + file: DurableFile, + error: std::io::Error, +} + +impl Write for DurableFile { + fn write(&mut self, buf: &[u8]) -> Result { + let amt = self.file.write(buf)?; + self.needs_sync = true; + Ok(amt) + } + fn flush(&mut self) -> Result<()> { + + self.file.sync_all()?; + self.needs_sync = false; + Ok(()) + } +} + +impl Drop for DurableFile { + fn drop(&mut self) { + // Any edge cases? + if self.needs_sync { + panic!("You forgot to sync!"); + } + } +} + +impl DurableFile { + pub fn new(file: File) -> DurableFile { + DurableFile { + file, + needs_sync: false, + } + } + + pub fn close(mut self) -> std::result::Result<(), CloseError> { + match self.flush() { + Ok(()) => Ok(()), + Err(e) => Err(CloseError { + file: self, + error: e, + }), + } + } +} \ No newline at end of file diff --git a/exercise-solutions/durable-file/step6/Cargo.toml b/exercise-solutions/durable-file/step6/Cargo.toml new file mode 100644 index 0000000..2c1e739 --- /dev/null +++ b/exercise-solutions/durable-file/step6/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "step6" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tempdir = "0.3.7" diff --git a/exercise-solutions/durable-file/step6/src/lib.rs b/exercise-solutions/durable-file/step6/src/lib.rs new file mode 100644 index 0000000..ea45edd --- /dev/null +++ b/exercise-solutions/durable-file/step6/src/lib.rs @@ -0,0 +1,104 @@ +use std::fs::File; +use std::io::{Result, Write}; + +#[derive(Debug)] +pub struct DurableFile { + file: File, + needs_sync: bool, +} + +#[derive(Debug)] +pub struct CloseError { + file: DurableFile, + error: std::io::Error, +} + +impl Write for DurableFile { + fn write(&mut self, buf: &[u8]) -> Result { + let amt = self.file.write(buf)?; + self.needs_sync = true; + Ok(amt) + } + fn flush(&mut self) -> Result<()> { + + self.file.sync_all()?; + self.needs_sync = false; + Ok(()) + } +} + +impl Drop for DurableFile { + fn drop(&mut self) { + // Any edge cases? + if self.needs_sync { + panic!("You forgot to sync!"); + } + } +} + +impl DurableFile { + pub fn new(file: File) -> DurableFile { + DurableFile { + file, + needs_sync: false, + } + } + + pub fn close(mut self) -> std::result::Result<(), CloseError> { + match self.flush() { + Ok(()) => Ok(()), + Err(e) => Err(CloseError { + file: self, + error: e, + }), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use tempdir; + + #[test] + fn create_no_write() { + let dir = tempdir::TempDir::new("tests").unwrap(); + let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap(); + let _durable = DurableFile::new(file); + + // No writes, let the file drop naturally. + } + + #[test] + #[should_panic(expected = "You forgot to sync!")] + fn create_write_panics() { + let dir = tempdir::TempDir::new("tests").unwrap(); + let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap(); + let mut durable = DurableFile::new(file); + durable.write_all(b"Hello, world!").unwrap(); + // Should panic, we forgot to sync! + } + + #[test] + fn create_write_sync() { + let dir = tempdir::TempDir::new("tests").unwrap(); + let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap(); + let mut durable = DurableFile::new(file); + durable.write_all(b"Hello, world!").unwrap(); + durable.flush().unwrap(); + + // We now drop, shouldn't panic because we flush'd. + } + + #[test] + fn create_write_close() { + let dir = tempdir::TempDir::new("tests").unwrap(); + let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap(); + let mut durable = DurableFile::new(file); + durable.write_all(b"Hello, world!").unwrap(); + + // This will close and drop the durable file, it + // shouldn't panic because we closed it manually. + durable.close().unwrap(); + } +} \ No newline at end of file