use crate::matcher::{MatchMaker, Matcher};
use crate::prelude::*;
use crate::util::{get_reader, Infile};
use fs_err as fs;
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
pub fn show_format() {
println!("#command CMD : the command to be run");
println!("#stdin : followed by the input to the program. Default empty.");
println!("#stdout : followed by the expected stdout output. Default empty.");
println!("#stderr : followed by the expected stderr output. Default empty.");
println!("#infile Filename : followed by the contents of the named file. '#infile foo' means that the file $TMP/foo is available to the command.");
println!("#outfile Filename : followed by the contents of the named file. '#outfile foo' means that the file $TMP/foo must be created by the command, with exactly that contents.");
println!("#nonewline : must follow the contents of one of the above four. Removes the final newline from the input or expected output.");
println!("#status Number : the expected exit status. Default zero.");
println!("'# ' : a line starting with # and a space is a comment, and is ignored");
}
#[derive(Debug, Clone, Default)]
struct InFile {
name: String,
content: Vec<u8>,
}
#[allow(missing_debug_implementations)]
struct OutFile {
name: String,
content: Vec<u8>,
matcher: Matcher,
}
impl Default for OutFile {
fn default() -> Self {
Self {
name: String::new(),
content: Vec::new(),
matcher: MatchMaker::make("empty").unwrap(),
}
}
}
#[allow(missing_debug_implementations)]
#[derive(Default)]
pub struct Test {
name: String,
cmd: Vec<u8>,
stdin: Vec<u8>,
stdout: OutFile,
stderr: OutFile,
code: i32,
in_files: Vec<InFile>,
out_files: Vec<OutFile>,
}
fn delete_file(s: &str, dir: &str) -> Result<()> {
let mut f = dir.to_string();
f.push_str(s);
std::fs::remove_file(f)?;
Ok(())
}
fn nuke(v: &mut Vec<String>, s: &str, dir: &str, keep_files: bool) -> Result<()> {
v.retain(|x| x != s);
if keep_files {
Ok(())
} else {
delete_file(s, dir)
}
}
pub fn read_dir(dir: &Path) -> Result<Vec<String>> {
let mut dirs = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
dirs.push(entry.file_name().to_string_lossy().to_string());
}
Ok(dirs)
}
fn grab(
tag: &[u8],
#[allow(clippy::ptr_arg)] buff: &mut Vec<u8>,
line: &mut Vec<u8>,
reader: &mut Infile,
need_read: &mut bool,
) -> Result<bool> {
if let Some(x) = line.strip_prefix(tag) {
if !x.is_empty() {
prerr(&[b"Unexpected stuff after ", tag, b" : ", line]);
return cdx_err(CdxError::Silent);
}
loop {
line.clear();
let sz = reader.read_until(b'\n', line)?;
if sz == 0 {
break;
}
if line.starts_with(b"#") {
if line.starts_with(b"#nonewline") {
if !buff.is_empty() {
buff.pop();
}
} else {
*need_read = false;
}
break;
} else {
buff.extend(&*line);
}
}
Ok(true)
} else {
Ok(false)
}
}
impl Test {
pub fn new() -> Self {
Self::default()
}
fn add_outfile(
&mut self,
tag: &str,
line: &mut Vec<u8>,
reader: &mut Infile,
need_read: &mut bool,
) -> Result<bool> {
if let Some(x) = line.strip_prefix(tag.as_bytes()) {
let out = if x.is_empty() {
let mut buf = Vec::new();
grab(tag.as_bytes(), &mut buf, line, reader, need_read)?;
OutFile {
name: tag[1..].to_string(),
matcher: MatchMaker::make2("exact", std::str::from_utf8(&buf)?)?,
content: buf,
}
} else {
let x = x.trimw_start();
OutFile {
name: tag[1..].to_string(),
matcher: MatchMaker::make(std::str::from_utf8(x)?)?,
content: x.to_vec(),
}
};
if tag == "#stderr" {
self.stderr = out;
} else {
self.stdout = out;
}
Ok(true)
} else {
Ok(false)
}
}
pub fn open(&mut self, file: &str) -> Result<()> {
self.name = file.to_string();
let mut reader = get_reader(file)?;
let mut line: Vec<u8> = Vec::new();
let sz = reader.read_until(b'\n', &mut line)?;
if sz == 0 {
return Ok(());
}
loop {
if line.last() == Some(&b'\n') {
line.pop();
}
let mut need_read = true;
if let Some(x) = line.strip_prefix(b"#command") {
let y = x.trimw_start();
self.cmd = y.to_vec();
} else if self.add_outfile("#stdout", &mut line, &mut reader, &mut need_read)?
|| self.add_outfile("#stderr", &mut line, &mut reader, &mut need_read)?
|| grab(
b"#stdin",
&mut self.stdin,
&mut line,
&mut reader,
&mut need_read,
)?
{
} else if let Some(x) = line.strip_prefix(b"#status") {
let y = x.trimw_start();
self.code = y.to_isize_whole(&line, "status")? as i32;
} else if let Some(x) = line.strip_prefix(b"#infile") {
let y = x.trimw_start();
let mut f = InFile {
name: String::from_utf8(y.to_vec())?,
..Default::default()
};
line.clear();
grab(b"", &mut f.content, &mut line, &mut reader, &mut need_read)?;
self.in_files.push(f);
} else if let Some(x) = line.strip_prefix(b"#outfile") {
let y = std::str::from_utf8(x)?;
let y = y.trimw_start();
if let Some((name, matcher)) = y.split_once(' ') {
let f = OutFile {
name: name.to_string(),
content: matcher.as_bytes().to_vec(),
matcher: MatchMaker::make(matcher)?,
};
self.out_files.push(f);
} else {
let name = y.to_string();
line.clear();
let mut buf = Vec::new();
grab(b"", &mut buf, &mut line, &mut reader, &mut need_read)?;
let f = OutFile {
name,
matcher: MatchMaker::make2("exact", std::str::from_utf8(&buf)?)?,
content: buf,
};
self.out_files.push(f);
}
} else if line.starts_with(b"# ") {
} else {
return err!(
"Unexpected line in test file : {}",
std::str::from_utf8(&line)?
);
}
if need_read {
line.clear();
let sz = reader.read_until(b'\n', &mut line)?;
if sz == 0 {
break;
}
}
}
Ok(())
}
pub fn run(&mut self, config: &Config) -> Result<bool> {
let mut keep_files = false;
let mut tmp: String = if config.tmpdir.is_empty() {
config.tmp.path().to_owned().to_string_lossy().to_string()
} else {
keep_files = true;
config.tmpdir.clone()
};
if !tmp.ends_with('/') {
tmp.push('/');
}
for x in &self.in_files {
let mut fname = tmp.clone();
fname.push_str(&x.name);
let mut w = get_writer(&fname)?;
w.write_all(&x.content)?;
}
let mut tmp_stdin = tmp.clone();
tmp_stdin.push_str("stdin");
{
let mut w = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open(&tmp_stdin)?;
w.write_all(&self.stdin)?;
}
let ncmd = String::from_utf8_lossy(&self.cmd)
.replace("$TMP", &tmp)
.as_bytes()
.to_vec();
let cmd: Vec<&[u8]> = ncmd.split(|num| num <= &b' ').collect();
if cmd.is_empty() {
return err!("command is empty");
}
let mut basecmd = config.bindir.as_bytes().to_vec();
basecmd.extend(cmd[0]);
let mut tmp_cmd = Command::new(OsStr::from_bytes(&basecmd));
for x in &cmd[1..] {
tmp_cmd.arg(OsStr::from_bytes(x));
}
let res = tmp_cmd
.stdin(std::fs::File::open(&tmp_stdin).unwrap())
.output();
let output = match res {
Err(x) => {
prerr(&[b"Error trying to execute : ", &basecmd]);
return Err(anyhow::Error::new(x));
}
Ok(x) => x,
};
let mut files = read_dir(Path::new(&tmp))?;
nuke(&mut files, "stdin", &tmp, keep_files)?;
for x in &self.in_files {
nuke(&mut files, &x.name, &tmp, keep_files)?;
}
let mut failed = false;
match output.status.code() {
Some(code) => {
if code != self.code {
failed = true;
eprintln!("Exited with status code: {} instead of {}", code, self.code);
}
}
None => {
failed = true;
eprintln!("Process terminated by signal");
}
}
if !self.stderr.matcher.do_match_safe(&output.stderr) {
failed = true;
prerr(&[
b"Stderr was\n",
&output.stderr,
b"\ninstead of\n",
&self.stderr.content,
]);
}
if !self.stdout.matcher.do_match_safe(&output.stdout) {
failed = true;
prerr(&[
b"Stdout was\n",
&output.stdout,
b"\ninstead of\n",
&self.stdout.content,
]);
}
for x in &self.out_files {
let mut fname = tmp.clone();
fname.push_str(&x.name);
match get_reader(&fname) {
Err(e) => {
failed = true;
eprintln!("{:?}", e);
}
Ok(mut f) => {
let mut body = Vec::new();
f.read_to_end(&mut body)?;
if !x.matcher.do_match_safe(&body) {
failed = true;
prerr(&[
b"File ",
x.name.as_bytes(),
b" was\n",
&body,
b" but should have been\n",
&x.content,
]);
}
}
}
nuke(&mut files, &x.name, &tmp, keep_files)?;
}
if !keep_files && !files.is_empty() {
failed = true;
eprintln!("Files left over in TMP dir :");
for x in &files {
eprintln!("{}", x);
delete_file(x, &tmp)?;
}
}
if keep_files {
let mut fname = tmp.clone();
fname.push_str("stdout");
let mut w = get_writer(&fname)?;
w.write_all(&output.stdout)?;
fname = tmp.clone();
fname.push_str("stderr");
w = get_writer(&fname)?;
w.write_all(&output.stderr)?;
}
if failed {
prerr(&[b"Test ", self.name.as_bytes(), b" failed ", &self.cmd]);
}
Ok(!failed)
}
}
#[derive(Debug)]
pub struct Config {
pass: usize,
fail: usize,
bindir: String,
tmpdir: String,
tmp: TempDir,
}
impl Config {
pub fn new() -> Result<Self> {
Ok(Self {
pass: 0,
fail: 0,
bindir: "./".to_string(),
tmpdir: String::new(),
tmp: TempDir::new()?,
})
}
pub fn tmp(&mut self, path: &str) -> Result<()> {
self.tmpdir = path.to_string();
if !self.tmpdir.ends_with('/') {
self.tmpdir.push('/');
}
fs::create_dir_all(&self.tmpdir)?;
Ok(())
}
pub fn bin(&mut self, path: &str) {
self.bindir = path.to_string();
if !self.bindir.ends_with('/') {
self.bindir.push('/');
}
}
pub fn report(&self) -> Result<()> {
println!(
"{} test run, {} pass, {} failed",
self.pass + self.fail,
self.pass,
self.fail
);
if self.fail > 0 {
cdx_err(CdxError::Silent)
} else {
Ok(())
}
}
fn do_run(&mut self, file: &str) -> Result<bool> {
let mut t = Test::new();
t.open(file)?;
t.run(self)
}
pub fn run(&mut self, file: &str) {
match self.do_run(file) {
Ok(x) => {
if x {
self.pass += 1;
} else {
self.fail += 1;
}
}
Err(x) => {
self.fail += 1;
eprintln!("Failure : {}", x);
prerr(&[b"Test ", file.as_bytes(), b" failed "]);
}
}
}
}
impl Default for Config {
fn default() -> Self {
Self::new().unwrap()
}
}