ensc-testsuite 0.1.6

Tool to generate TAP or JUnit reports
Documentation
use std::sync::Mutex;
use std::sync::Arc;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{ Path, PathBuf };
use std::os::unix::fs::PermissionsExt;

use super::formatters::{ PlanFormatter, PlanFormatterBuilder };

#[cfg(feature = "use-static-destructors")]
mod g {
    use static_init::{dynamic};

    use super::*;

    #[allow(clippy::redundant_closure_call)]
    #[dynamic(drop)]
    pub(super) static mut OUTPUT_FILES: Mutex<RefCell<HashMap<&'static str, Arc<GlobalFile>>>> = Mutex::default();
}

#[cfg(not(feature = "use-static-destructors"))]
mod g {
    use lazy_static::lazy_static;

    use super::*;

    lazy_static! {
	pub(super) static ref OUTPUT_ID: Mutex<RefCell<HashMap<&'static str, u32>>> = Mutex::default();
    }
}

#[derive(Clone)]
struct TempFile(Arc<Mutex<RefCell<tempfile::NamedTempFile>>>);

impl TempFile {
    fn new(f: tempfile::NamedTempFile) -> Self {
	Self(Arc::new(Mutex::new(RefCell::new(f))))
    }

    fn into_inner(self) -> Option<tempfile::NamedTempFile> {
	let res = Arc::try_unwrap(self.0).ok()?;

	Some(res.into_inner().unwrap().into_inner())
    }
}

impl std::io::Write for TempFile {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize>
    {
	let lock = self.0.lock().unwrap();
	let mut w = lock.borrow_mut();

	w.write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()>
    {
	let lock = self.0.lock().unwrap();
	let mut w = lock.borrow_mut();

	w.flush()
    }
}

#[derive(Default)]
struct GlobalFile {
    inner:	Mutex<Option<(Box<dyn PlanFormatter + Send>,
			      TempFile)>>,
    filename:	PathBuf,
}

impl GlobalFile {
    pub fn new<B>(builder: &B, basename: PathBuf) -> std::io::Result<Self>
    where
	B: PlanFormatterBuilder<TempFile>,
	B::Target: PlanFormatter + Send + 'static,
    {
	let res = {
	    let tmpfile = TempFile::new(tempfile::NamedTempFile::new_in(
		basename.parent().unwrap_or_else(|| Path::new(".")))?);

	    let formatter = Box::new(builder.from_writer(tmpfile.clone()));

	    Self {
		inner:		Mutex::new(Some((formatter, tmpfile))),
		filename:	basename,
	    }
	};

	Ok(res)
    }

    pub fn persist(&mut self) {
	let output = {
	    let mut lock = self.inner.lock().unwrap();

	    lock.take().map(|v| v.1)
	};

	if let Some(o_ref) = output {
	    let o = o_ref.into_inner().unwrap();

	    if let Ok(meta) = std::fs::metadata(o.path()) {
		let mut permissions = meta.permissions();

		permissions.set_mode(0o644);
		std::fs::set_permissions(o.path(), permissions).unwrap();
	    }

	    o.persist(&self.filename).unwrap();
	}
    }

    pub fn serialize(&self, plan: &super::ReportPlan) -> Result<(), ()>
    {
	let lock = self.inner.lock().unwrap();
	let fmt = &lock.as_ref().unwrap().0;

	fmt.serialize(plan)
    }
}

impl Drop for GlobalFile {
    fn drop(&mut self) {
	self.persist();
    }
}

#[cfg(feature = "use-static-destructors")]
fn create_file<B>(builder: B, basename: &str, suffix: &'static str) -> std::io::Result<Arc<GlobalFile>>
where
    B: PlanFormatterBuilder<TempFile>,
    B::Target: PlanFormatter + Send + 'static,
{
    let files_lock = unsafe { g::OUTPUT_FILES.lock().unwrap() };
    let mut is_first = true;

    loop {
	if let Some(v) = (*files_lock).borrow().get(suffix) {
	    break Ok(v.clone())
	}

	let mut path = PathBuf::new();

	path.push(basename);
	path.set_extension(suffix);

	let f = Arc::new(GlobalFile::new::<B>(&builder, path)?);

	assert!(is_first);
	is_first = false;

	(*files_lock).borrow_mut().insert(suffix, f);
    }
}

#[cfg(not(feature = "use-static-destructors"))]
fn create_file<B>(builder: B, basename: &str, suffix: &'static str) -> std::io::Result<Arc<GlobalFile>>
where
    B: PlanFormatterBuilder<TempFile> + Send + 'static
{
    let output_id_lock = g::OUTPUT_ID.lock().unwrap();
    let output_id = *(*output_id_lock).borrow().get(suffix).unwrap_or(&0);

    let mut path = PathBuf::new();

    path.push(basename);
    path.set_extension(format!("{}.{}", output_id, suffix));

    let f = Arc::new(GlobalFile::new::<B>(path)?);

    (*output_id_lock).borrow_mut().insert(suffix, output_id + 1);

    Ok(f)
}

fn output_tap(basename: &str, plan: &super::ReportPlan) -> Result<(), ()>
{
    use crate::report::formatters::tap::TapFormatterBuilder as Builder;

    let f = create_file(Builder::new(), basename, "tap").unwrap();

    f.serialize(plan)
}

fn output_json(basename: &str, plan: &super::ReportPlan) -> Result<(), ()>
{
    use crate::report::formatters::json::JsonFormatterBuilder as Builder;

    let f = create_file(Builder::new(), basename, "json").unwrap();

    f.serialize(plan)
}

fn output_junit(basename: &str, plan: &super::ReportPlan, nested: bool) -> Result<(), ()>
{
    use crate::report::formatters::junit::JUnitFormatterBuilder as Builder;

    let f = create_file(Builder::new(nested), basename,
			match nested {
			    true  => "nested.junit",
			    false => "flat.junit",
			}).unwrap();

    f.serialize(plan)
}

pub fn emit(plan: &super::ReportPlan) -> Result<(),()>
{
    let basename = std::env::var("TESTSUITE_OUTPUT").unwrap_or_else(|_| "test-result".to_string());

    for o in std::env::var("TESTSUITE_FORMAT").unwrap_or_else(|_| "tap".to_string()).split(',') {
	if o.is_empty() {
	    continue;
	}

	match o {
	    "tap"		=> output_tap(&basename, plan)?,
	    "json"		=> output_json(&basename, plan)?,
	    "junit" |
	    "junit-flat"	=> output_junit(&basename, plan, false)?,
	    "junit-nested"	=> output_junit(&basename, plan, true)?,
	    _			=> panic!("unsupported format {}", o),
	};
    }

    Ok(())
}