Skip to content

Commit

Permalink
Writing "Prevent hidden dependencies" section, update tutorial README.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gohla committed Oct 16, 2023
1 parent f0f22d0 commit 42bae1a
Show file tree
Hide file tree
Showing 7 changed files with 661 additions and 10 deletions.
17 changes: 12 additions & 5 deletions tutorial/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ cargo install mdbook mdbook-admonish mdbook-external-links
cargo install --path mdbook-diff2html
```

If you have [`cargo install-update`](https://github.com/nabijaczleweli/cargo-update) installed, you can instead install and/or update the external binaries with:

```shell
cargo install-update mdbook mdbook-admonish mdbook-external-links
```

## Building

To test all the code fragments and generate outputs in `gen` which the tutorial uses, first run:
Expand Down Expand Up @@ -44,8 +50,8 @@ cargo run -- step-all -d dst --skip-cargo --skip-outputs

## Stack & Structure

The book is built with [mdBook](https://rust-lang.github.io/mdBook/).
We use the following mdBook plugins:
The book is built with [mdBook](https://rust-lang.github.io/mdBook/).
We use the following external mdBook preprocessors:

- [mdbook-admonish](https://github.com/tommilligan/mdbook-admonish)
- [mdbook-external-links](https://github.com/jonahgoldwastaken/mdbook-external-links)
Expand All @@ -55,14 +61,15 @@ Structure:
- `book.toml`: main mdBook configuration file.
- `src`: book source code.
- `src/SUMMARY.md`: main mdBook file with the table of contents for the book.
- `src/custom.css`: custom CSS included with the book.
- `src/diff.js`: custom JS included with the book for diff highlighting.
- `src/mdbook-admonish.css`: CSS for the `mdbook-admonish` plugin. This is automatically generated by the plugin.
- `src/gen`: generated diffs and cargo outputs for the book. Part of `src` for change detection.
- `src/*`: markdown files and code (diff) fragments of the book.
- `theme`: customization of the default theme
- `mdbook-admonish.css`: CSS for the `mdbook-admonish` preprocessor. Can be updated by running `mdbook-admonish install`.
- `stepper`: command-line application (in Rust) that checks all source code (additions, insertions, diffs) by stepping over them in order and building them with cargo, ensuring that the code in the book is actually valid. It also generates diffs between source code fragments and produces outputs (such as cargo stdout) and stores them in `src/gen`.
- `stepper/src/app.rs`: stepper instructions. Modify this to modify what/how the source code fragments of the book are checked.
- `mdbook-diff2html`: mdBook preprocessor that renders diffs with [Diff2Html](https://github.com/rtfpessoa/diff2html)
- `diff2html-ui-base.min.js`: Diff2Html browser-side implementation
- `diff2html.min.css`: Diff2Html CSS (customized, see below)

### Diff2Html

Expand Down
24 changes: 24 additions & 0 deletions tutorial/src/3_min_sound/6_hidden_dep/d_1_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@


// Hidden dependency tests

#[should_panic(expected = "Hidden dependency")]
#[test]
fn test_require_hidden_dependency_panics() {
fn run() -> Result<(), io::Error> {
let mut pie = test_pie();
let temp_dir = create_temp_dir()?;

let file = temp_dir.path().join("in_out.txt");
write(&file, "Hello, World!")?;

let read = ReadFile(file.clone(), FileStamper::Modified);
let write = WriteFile(Box::new(Return("Hi there")), file.clone(), FileStamper::Modified);

pie.require_then_assert_one_execute(&write)?;
pie.require_then_assert_one_execute(&read)?;

Ok(())
}
run().unwrap();
}
21 changes: 21 additions & 0 deletions tutorial/src/3_min_sound/6_hidden_dep/d_2_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

#[should_panic(expected = "Hidden dependency")]
#[test]
fn test_provide_hidden_dependency_panics() {
fn run() -> Result<(), io::Error> {
let mut pie = test_pie();
let temp_dir = create_temp_dir()?;

let file = temp_dir.path().join("in_out.txt");
write(&file, "Hello, World!")?;

let read = ReadFile(file.clone(), FileStamper::Modified);
let write = WriteFile(Box::new(Return("Hi there")), file.clone(), FileStamper::Modified);

pie.require_then_assert_one_execute(&read)?;
pie.require_then_assert_one_execute(&write)?;

Ok(())
}
run().unwrap();
}
136 changes: 136 additions & 0 deletions tutorial/src/3_min_sound/6_hidden_dep/e_1_read_origin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::io::{BufWriter, ErrorKind, Read, Stdout};
use std::path::PathBuf;

use dev_shared::write_until_modified;
use pie::{Context, Pie, Task};
use pie::stamp::FileStamper;
use pie::tracker::CompositeTracker;
use pie::tracker::event::EventTracker;
use pie::tracker::writing::WritingTracker;

/// Testing tracker composed of an [`EventTracker`] for testing and stdout [`WritingTracker`] for debugging.
pub type TestTracker<T> = CompositeTracker<EventTracker<T, <T as Task>::Output>, WritingTracker<BufWriter<Stdout>>>;
pub fn test_tracker<T: Task>() -> TestTracker<T> {
CompositeTracker(EventTracker::default(), WritingTracker::with_stdout())
}

/// Testing [`Pie`] using [`TestTracker`].
pub type TestPie<T> = Pie<T, <T as Task>::Output, TestTracker<T>>;
pub fn test_pie<T: Task>() -> TestPie<T> {
TestPie::with_tracker(test_tracker())
}

/// Testing extensions for [`TestPie`].
pub trait TestPieExt<T: Task> {
/// Require `task` in a new session, assert that there are no dependency check errors, then runs `test_assert_func`
/// on the event tracker for test assertion purposes.
fn require_then_assert(
&mut self,
task: &T,
test_assert_func: impl FnOnce(&EventTracker<T, T::Output>),
) -> T::Output;

/// Require `task` in a new session, asserts that there are no dependency check errors.
fn require(&mut self, task: &T) -> T::Output {
self.require_then_assert(task, |_| {})
}

/// Require `task` in a new session, then assert that it is not executed.
fn require_then_assert_no_execute(&mut self, task: &T) -> T::Output {
self.require_then_assert(task, |t|
assert!(!t.any_execute_of(task), "expected no execution of task {:?}, but it was executed", task),
)
}
/// Require `task` in a new session, then assert that it is executed exactly once.
fn require_then_assert_one_execute(&mut self, task: &T) -> T::Output {
self.require_then_assert(task, |t|
assert!(t.one_execute_of(task), "expected one execution of task {:?}, but it was not executed, or was executed more than once", task),
)
}
}
impl<T: Task> TestPieExt<T> for TestPie<T> {
fn require_then_assert(&mut self, task: &T, test_assert_func: impl FnOnce(&EventTracker<T, T::Output>)) -> T::Output {
let mut session = self.new_session();
let output = session.require(task);
assert!(session.dependency_check_errors().is_empty(), "expected no dependency checking errors, but there are \
dependency checking errors: {:?}", session.dependency_check_errors());
test_assert_func(&self.tracker().0);
output
}
}

/// Testing tasks enumeration.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum TestTask {
Return(&'static str),
ReadFile(PathBuf, FileStamper, Option<Box<TestTask>>),
WriteFile(Box<TestTask>, PathBuf, FileStamper),
ToLower(Box<TestTask>),
ToUpper(Box<TestTask>),
Sequence(Vec<TestTask>),
}
impl Task for TestTask {
type Output = Result<TestOutput, ErrorKind>;
fn execute<C: Context<Self>>(&self, context: &mut C) -> Self::Output {
match self {
TestTask::Return(string) => Ok(string.to_string().into()),
TestTask::ReadFile(path, stamper, origin) => {
if let Some(origin) = origin {
context.require_task(origin)?;
}
let mut string = String::new();
if let Some(mut file) = context.require_file_with_stamper(path, *stamper).map_err(|e| e.kind())? {
file.read_to_string(&mut string).map_err(|e| e.kind())?;
}
Ok(string.into())
}
TestTask::WriteFile(string_provider_task, path, stamper) => {
let string = context.require_task(string_provider_task.as_ref())?.into_string();
write_until_modified(path, string.as_bytes()).map_err(|e| e.kind())?;
context.provide_file_with_stamper(path, *stamper).map_err(|e| e.kind())?;
Ok(TestOutput::Unit)
}
TestTask::ToLower(string_provider_task) => {
let string = context.require_task(string_provider_task)?.into_string();
Ok(string.to_lowercase().into())
}
TestTask::ToUpper(string_provider_task) => {
let string = context.require_task(string_provider_task)?.into_string();
Ok(string.to_uppercase().into())
}
TestTask::Sequence(tasks) => {
for task in tasks {
context.require_task(task)?;
}
Ok(TestOutput::Unit)
}
}
}
}

/// [`TestTask`] output enumeration.
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum TestOutput {
String(String),
Unit,
}
impl From<String> for TestOutput {
fn from(value: String) -> Self { Self::String(value) }
}
impl From<()> for TestOutput {
fn from(_: ()) -> Self { Self::Unit }
}
impl TestOutput {
pub fn as_str(&self) -> &str {
match self {
Self::String(s) => &s,
_ => panic!("{:?} does not contain a string", self),
}
}
pub fn into_string(self) -> String {
match self {
Self::String(s) => s,
_ => panic!("{:?} does not contain a string", self),
}
}
}
Loading

0 comments on commit 42bae1a

Please sign in to comment.