Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add running examples to cargo test #65

Merged
merged 5 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,7 @@ pr-run-mode = "plan"

[dev-dependencies]
chwd = "0.2.0"
git2 = "0.18.2"
paste = "1.0.14"
rusty-fork = "0.3.0"
similar = "2.4.0"
26 changes: 26 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ Any complex parts of the code should be commented and propely explained.
If you are unsure on what something does, and cannot decypher the code, please
open an issue - the code probably needs some refactoring if you feel that way.

## Testing
Tests are of two types, integration or unit tests. Both are run with `cargo test`.
Unit tests are your standard doc tests and unit tests, just write them in as
you'd normally would.

Kerblam! is also tested against [the kerblam-examples examples](https://github.com/MrHedmad/kerblam-examples).
Each example is in a directory (e.g. `examples/my_example`) with a `run` bash
script that runs the example and rolls it back.
`cargo test` checks out the *current* version of `kerblam-examples` and runs
the `run` scripts, checking:
- If the `run` script exited successfully;
- If the number or content of the files in the repo has changed from before
the `run` script was executed.

To add a new example/test, write the example (see the `kerblam examples` repo),
add the `run` script to the example and add the `run_example_named!` or
`run_failing_example_named!` macro with the name of the example, e.g.
`run_example_named!("my_example")`.

These integration tests are also run by cargo when you `cargo test`.
They run locally on your machine, so you should make sure that you have
properly installed kerblam!, including both docker and podman executables.

If you find that one of these tests has failed, please be sure that it is not
due to your specific environment before starting the debug process.

## Contributions
Currently, the Kerblam! maintainer is [MrHedmad](https://github.com/MrHedmad).
There are no other maintainers. All contributions are listed below
Expand Down
188 changes: 188 additions & 0 deletions tests/run_local_examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use git2;
use paste;
use std::collections::HashMap;
use std::env::set_current_dir;
use std::mem::drop;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{fs, io};
use tempfile::TempDir;

const EXAMPLE_REPO: &'static str = "https://github.com/MrHedmad/kerblam-examples.git";

/// Setup local tests by cloning the example repo.
fn setup_test() -> TempDir {
let target = TempDir::new().expect("Could not create temp dir");

let mut clone_options = git2::FetchOptions::new();
clone_options.depth(1);
clone_options.download_tags(git2::AutotagOption::None);

let mut fetcher = git2::build::RepoBuilder::new();
fetcher.fetch_options(clone_options);

fetcher
.clone(EXAMPLE_REPO, &target.path())
.expect("Could not clone remote repo.");

target
}

// This is hard, even for me, but all the examples look the same, so a macro
// that generates the tests makes a lot of sense.
//
// To run an example, you:
// - Clone the repo;
// - Move into the example folder;
// - Execute the `run` shell script;
// - Check if the repo is still the same;
// - The `run` script should run the example and roll it back.
//
// This is a rough integration test to see if all the various invocations of
// kerblam! run correctly.
// If a test fails, check the single example to see why it failed and fix the
// root cause.

type Content = HashMap<PathBuf, Vec<u8>>;

// Take a snapshot of the directory, returning the current content
fn snapshot(dir: &PathBuf) -> io::Result<Content> {
let mut content: Content = HashMap::new();

for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();

let metadata = fs::metadata(&path)?;

if metadata.is_dir() {
continue;
}

content.insert(entry.path().to_path_buf(), fs::read(entry.path())?);
}

Ok(content)
}

fn two_way_check<T>(a: &Vec<T>, b: &Vec<T>) -> bool
where
T: PartialEq,
{
a.iter().all(|item| b.contains(item)) & b.iter().all(|item| a.contains(item))
}

fn check_identical(old_snap: Content, target: impl AsRef<Path>) -> io::Result<()> {
let new_snap = snapshot(&target.as_ref().to_owned().to_path_buf()).unwrap();

let old_files: Vec<PathBuf> = old_snap.keys().cloned().collect();
let new_files: Vec<PathBuf> = new_snap.keys().cloned().collect();
if !two_way_check(&old_files, &new_files) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Number of files differ: old: {:?}, new: {:?}",
old_files, new_files
),
));
}

for path in old_files {
let current = fs::read(&path)?;

if current != *old_snap.get(&path).unwrap() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("File {:?} has modified data!", path),
));
}
}

Ok(())
}

macro_rules! run_example_named {
($name:literal) => {
paste::item! {

#[test]
fn [< example_ $name >] () {
let repo = setup_test();
let target = repo.path().join(format!("examples/{}", $name));

// Move into the example
println!("{:?}", target);
set_current_dir(&target).unwrap();

// Take a snapshot of the contents of the repo
let snap = snapshot(&target).expect("Could not take snapshot of repo");

// Run the things
let code = Command::new("bash")
.args([target.join("run")])
.output()
.expect("Could not collect child output");

eprintln!(
"Command Output\nSTDOUT:\n{}\n\nSTDERR:\n{}",
String::from_utf8_lossy(&code.stdout),
String::from_utf8_lossy(&code.stderr),
);

assert!(code.status.success());

assert!(check_identical(snap, &target).is_ok());

// The 'repo' variable gets dropped here, so the tempdir is cleaned up
// SPECIFICALLY here.
drop(repo);
}
}
};
}

// TODO: This is repeated from above with only small differences.
// Can we avaoid it?
macro_rules! run_failing_example_named {
($name:literal) => {
paste::item! {

#[test]
fn [< failing_example_ $name >] () {
let repo = setup_test();
let target = repo.path().join(format!("examples/{}", $name));

// Move into the example
println!("{:?}", target);
set_current_dir(&target).unwrap();

// Take a snapshot of the contents of the repo
let snap = snapshot(&target).expect("Could not take snapshot of repo");

// Run the things
let code = Command::new("bash")
.args([target.join("run")])
.output()
.expect("Could not collect child output");

eprintln!(
"Command Output\nSTDOUT:\n{}\n\nSTDERR:\n{}",
String::from_utf8_lossy(&code.stdout),
String::from_utf8_lossy(&code.stderr),
);

assert!(! code.status.success());

assert!(check_identical(snap, &target).is_ok());

// The 'repo' variable gets dropped here, so the tempdir is cleaned up
// SPECIFICALLY here.
drop(repo);
}
}
};
}

run_example_named!("basic_pipes");
run_example_named!("basic_containers");
run_failing_example_named!("safe_failure");
Loading