#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use dev_report::{CheckResult, Evidence, Producer, Report, Severity};
pub mod adversarial;
pub mod golden;
pub mod mock;
pub mod tree;
pub struct TempProject {
_dir: tempfile::TempDir,
files: Vec<(PathBuf, Vec<u8>)>,
}
impl TempProject {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> TempProjectBuilder {
TempProjectBuilder::default()
}
pub fn path(&self) -> &Path {
self._dir.path()
}
pub fn declared_files(&self) -> impl Iterator<Item = (&Path, &[u8])> {
self.files.iter().map(|(p, b)| (p.as_path(), b.as_slice()))
}
}
#[derive(Default)]
pub struct TempProjectBuilder {
files: Vec<(PathBuf, Vec<u8>)>,
}
impl TempProjectBuilder {
pub fn with_file(
mut self,
relative_path: impl Into<PathBuf>,
contents: impl Into<String>,
) -> Self {
self.files
.push((relative_path.into(), contents.into().into_bytes()));
self
}
pub fn with_bytes(
mut self,
relative_path: impl Into<PathBuf>,
contents: impl Into<Vec<u8>>,
) -> Self {
self.files.push((relative_path.into(), contents.into()));
self
}
pub fn build(self) -> io::Result<TempProject> {
let dir = tempfile::tempdir()?;
for (rel, bytes) in &self.files {
let target = dir.path().join(rel);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&target, bytes)?;
}
Ok(TempProject {
_dir: dir,
files: self.files,
})
}
}
pub trait Fixture {
type Output;
fn set_up(&mut self) -> io::Result<Self::Output>;
fn tear_down(&mut self) -> io::Result<()>;
fn set_up_checked(&mut self, name: impl Into<String>) -> CheckResult {
let name = format!("fixtures::{}", name.into());
match self.set_up() {
Ok(_) => {
let mut c = CheckResult::pass(name).with_detail("set_up succeeded");
c.tags = vec!["fixtures".to_string()];
c.evidence = vec![Evidence::numeric("setup_ok", 1.0)];
c
}
Err(e) => {
let mut c = CheckResult::fail(name, Severity::Critical)
.with_detail(format!("set_up failed: {}", e));
c.tags = vec![
"fixtures".to_string(),
"setup_failed".to_string(),
"regression".to_string(),
];
c.evidence = vec![Evidence::numeric("setup_ok", 0.0)];
c
}
}
}
}
pub struct FixtureProducer<F>
where
F: Fn() -> io::Result<()>,
{
name: String,
subject_version: String,
run: F,
}
impl<F> FixtureProducer<F>
where
F: Fn() -> io::Result<()>,
{
pub fn new(name: impl Into<String>, subject_version: impl Into<String>, run: F) -> Self {
Self {
name: name.into(),
subject_version: subject_version.into(),
run,
}
}
}
impl<F> Producer for FixtureProducer<F>
where
F: Fn() -> io::Result<()>,
{
fn produce(&self) -> Report {
let check_name = format!("fixtures::{}", self.name);
let started = std::time::Instant::now();
let check = match (self.run)() {
Ok(()) => {
let elapsed = started.elapsed();
let mut c = CheckResult::pass(check_name)
.with_duration_ms(elapsed.as_millis() as u64)
.with_detail("fixture lifecycle completed cleanly");
c.tags = vec!["fixtures".to_string()];
c.evidence = vec![
Evidence::numeric("setup_ok", 1.0),
Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
];
c
}
Err(e) => {
let mut c = CheckResult::fail(check_name, Severity::Critical)
.with_detail(format!("fixture lifecycle failed: {}", e));
c.tags = vec![
"fixtures".to_string(),
"setup_failed".to_string(),
"regression".to_string(),
];
c.evidence = vec![Evidence::numeric("setup_ok", 0.0)];
c
}
};
let mut r = Report::new(self.name.clone(), self.subject_version.clone())
.with_producer("dev-fixtures");
r.push(check);
r.finish();
r
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn temp_project_builds_and_writes_files() {
let project = TempProject::new()
.with_file("a.txt", "hello")
.with_file("nested/b.txt", "world")
.build()
.unwrap();
let a = project.path().join("a.txt");
let b = project.path().join("nested").join("b.txt");
assert!(a.exists());
assert!(b.exists());
assert_eq!(std::fs::read_to_string(&a).unwrap(), "hello");
assert_eq!(std::fs::read_to_string(&b).unwrap(), "world");
}
#[test]
fn temp_project_cleans_up_on_drop() {
let path = {
let project = TempProject::new()
.with_file("x.txt", "ephemeral")
.build()
.unwrap();
project.path().to_path_buf()
};
assert!(!path.exists());
}
#[test]
fn temp_project_cleans_up_on_panic() {
let path = {
let project = TempProject::new()
.with_file("x.txt", "panicky")
.build()
.unwrap();
let path = project.path().to_path_buf();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _proj = project;
panic!("test panic");
}));
assert!(result.is_err());
path
};
assert!(!path.exists());
}
struct OkFixture;
impl Fixture for OkFixture {
type Output = ();
fn set_up(&mut self) -> io::Result<()> {
Ok(())
}
fn tear_down(&mut self) -> io::Result<()> {
Ok(())
}
}
struct FailingFixture;
impl Fixture for FailingFixture {
type Output = ();
fn set_up(&mut self) -> io::Result<()> {
Err(io::Error::other("boom"))
}
fn tear_down(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn set_up_checked_pass_path() {
let c = OkFixture.set_up_checked("ok");
assert_eq!(c.verdict, dev_report::Verdict::Pass);
assert!(c.has_tag("fixtures"));
}
#[test]
fn set_up_checked_fail_path() {
let c = FailingFixture.set_up_checked("bad");
assert_eq!(c.verdict, dev_report::Verdict::Fail);
assert!(c.has_tag("setup_failed"));
assert!(c.has_tag("regression"));
}
#[test]
fn fixture_producer_emits_report() {
let producer = FixtureProducer::new("smoke", "0.1.0", || {
let _p = TempProject::new().with_file("a.txt", "x").build()?;
Ok(())
});
let report = producer.produce();
assert_eq!(report.checks.len(), 1);
assert_eq!(report.producer.as_deref(), Some("dev-fixtures"));
assert_eq!(report.overall_verdict(), dev_report::Verdict::Pass);
}
#[test]
fn fixture_producer_failed_setup_yields_fail() {
let producer = FixtureProducer::new("broken", "0.1.0", || {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "nope"))
});
let report = producer.produce();
assert_eq!(report.overall_verdict(), dev_report::Verdict::Fail);
}
}