ensc-testsuite 0.1.6

Tool to generate TAP or JUnit reports
Documentation
use crate::TestStatus;
use super::generic::Generic;

// when temporary file is smaller than this size, read its content and close the file
const MAX_CONTENT_DATA_SIZE: u64 = 512 * 1_024;

#[derive(Debug)]
enum IoFileContent {
    File(std::fs::File),
    Data(Vec<u8>),
}

#[derive(Debug)]
pub struct IoFile {
    pub key: &'static str,
    pub ftype: IoFileType,

    content: IoFileContent,
}

impl IoFile {
    pub fn new(ftype: IoFileType, key: &'static str) -> std::io::Result<Self>
    {
	Ok(Self {
	    key: key,
	    ftype: ftype,
	    content: IoFileContent::File(tempfile::tempfile()?),
	})
    }

    fn flush(&mut self) -> std::io::Result<()>
    {
	use std::io::Write;

	match &mut self.content {
	    IoFileContent::File(f) => f.flush(),
	    _ => Ok(()),
	}
    }

    fn try_clone(&self) -> std::io::Result<std::fs::File>
    {
	match &self.content {
	    IoFileContent::File(f) => f.try_clone(),
	    _ => Err(std::io::ErrorKind::BrokenPipe.into()),
	}
    }

    fn finish(&mut self) -> std::io::Result<()> {
	if let IoFileContent::File(f) = &self.content {
	    if let Ok(m) = f.metadata() {
		if m.len() > MAX_CONTENT_DATA_SIZE {
		    // keep the temporary file when it is too large
		    return Ok(());
		}
	    }

	    let data = Self::read_internal(f)?;

	    self.content = IoFileContent::Data(data);
	}

	Ok(())
    }

    fn read_internal(file: &std::fs::File) -> std::io::Result<Vec<u8>>
    {
	use std::io::Read;
	use std::io::Seek;
	use std::io::SeekFrom;

	let mut buf = Vec::new();
	let mut file = file.try_clone()?;

	file.seek(SeekFrom::Start(0))?;
	file.read_to_end(&mut buf)?;

	let mut pos_s = 0;
	while pos_s < buf.len() && buf[pos_s] == b'\n' {
	    pos_s += 1;
	}

	let mut pos_e = buf.len();
	while pos_e > 0 && buf[pos_e - 1] == b'\n' {
	    pos_e -= 1;
	}

	Ok(buf[pos_s..pos_e].to_vec())
    }

    pub fn read(&self) -> std::io::Result<Vec<u8>>
    {
	match &self.content {
	    IoFileContent::File(f) => Self::read_internal(f),
	    IoFileContent::Data(d) => Ok(d.clone()),
	}
    }

    pub fn read_string(&self) -> std::io::Result<Vec<String>>
    {
	let mut data: Vec<_> = self.read()?
	    .split(|c| *c == b'\n')
	    .map(|v| String::from_utf8_lossy(v).to_string())
	    .collect();

	while matches!(data.last(), Some(data) if data.is_empty()) {
	    data.pop();
	}

	Ok(data)
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IoFileType {
    Stderr,
    Stdout,
}

trait IoFileAccessor {
    fn find_or_insert<'a>(&'a mut self, key: &'static str, ftype: IoFileType) -> std::io::Result<&'a mut IoFile>;
}

impl IoFileAccessor for Vec<IoFile>
{
    fn find_or_insert<'a>(&'a mut self, key: &'static str, ftype: IoFileType) -> std::io::Result<&'a mut IoFile>
    {
	let idx = match self.iter().position(|e| e.ftype == ftype && e.key == key) {
	    Some(p) => p,
	    None => {
		self.push(IoFile::new(ftype, key)?);
		self.len() - 1
	    }
	};

	Ok(&mut self[idx])
    }
}

#[derive(Debug, Default)]
pub struct Case {
    pub(crate) status:		TestStatus,
    pub(crate) generic:		Generic,
    pub(crate) backtrace:	Option<String>,
    pub(crate) reason:		Option<String>,

    pub(crate) val_expect:	Option<String>,
    pub(crate) val_got:		Option<String>,

    pub(crate) stdio:		Vec<IoFile>,
}

impl Case {
    pub fn new() -> Self
    {
	Self::default()
    }

    pub fn record_duration(&mut self, duration: std::time::Duration)
    {
	self.generic.record_duration(duration)
    }

    pub fn record_expect(&mut self, s: &str)
    {
	assert!(self.val_expect.is_none());

	self.val_expect = Some(s.to_string());
    }

    pub fn record_got(&mut self, s: &str)
    {
	assert!(self.val_got.is_none());

	self.val_got = Some(s.to_string());
    }

    #[allow(dead_code)]
    fn read_file(f: &Option<std::fs::File>) -> std::io::Result<Option<String>>
    {
	fn read(mut f: std::fs::File) -> std::io::Result<String>
	{
	    use std::io::Read;
	    use std::io::Seek;
	    use std::io::SeekFrom;

	    let mut buf = String::new();

	    f.seek(SeekFrom::Start(0))?;
	    f.read_to_string(&mut buf)?;
	    Ok(buf)
	}

	Ok(if let Some(f) = f {
	    Some(read(f.try_clone()?)?)
	} else {
	    None
	})
    }

    pub(crate) fn stderr_iter(&self) -> Vec<&IoFile>
    {
	self.stdio
	    .iter()
	    .filter(|v| v.ftype == IoFileType::Stderr)
	    .collect()
    }

    pub(crate) fn stdout_iter(&self) -> Vec<&IoFile>
    {
	self.stdio
	    .iter()
	    .filter(|v| v.ftype == IoFileType::Stdout)
	    .collect()
    }

    pub fn get_stderr(&mut self) -> std::io::Result<std::fs::File>
    {
	self.get_stderr_named("default")
    }

    pub fn get_stderr_named(&mut self, key: &'static str)
			    -> std::io::Result<std::fs::File>
    {
	let stdio = self.stdio.find_or_insert(key, IoFileType::Stderr)?;

	stdio.flush()?;
	stdio.try_clone()
    }

    pub fn get_stdout(&mut self) -> std::io::Result<std::fs::File>
    {
	self.get_stdout_named("default")
    }

    pub fn get_stdout_named(&mut self, key: &'static str)
			    -> std::io::Result<std::fs::File>
    {
	let stdio = self.stdio.find_or_insert(key, IoFileType::Stdout)?;

	stdio.flush()?;
	stdio.try_clone()
    }

    #[allow(dead_code)]
    pub fn set_backtrace(&mut self, s: &str)
    {
	self.backtrace = Some(s.to_string());
    }

    pub fn set_reason(&mut self, s: &str)
    {
	self.reason = Some(s.to_string());
    }

    pub fn is_ok(&self) -> bool {
	self.status.is_ok()
    }

    pub(crate) fn finish_io(&mut self) -> std::io::Result<()> {
	for f in &mut self.stdio {
	    f.finish()?;
	}

	Ok(())
    }
}