use crate::check_args;
use crate::molt_ok;
use crate::types::ContextID;
use crate::Interp;
use crate::MoltResult;
use crate::ResultCode;
use crate::Value;
use std::env;
use std::fs;
use std::path::PathBuf;
pub fn test_harness(interp: &mut Interp, args: &[String]) -> Result<(), ()> {
println!("Molt {} -- Test Harness", env!("CARGO_PKG_VERSION"));
if args.is_empty() {
eprintln!("missing test script");
return Err(());
}
let path = PathBuf::from(&args[0]);
let context_id = interp.save_context(TestContext::new());
interp.add_context_command("test", test_cmd, context_id);
match fs::read_to_string(&args[0]) {
Ok(script) => {
if let Some(parent) = path.parent() {
let _ = env::set_current_dir(parent);
}
if let Err(exception) = interp.eval(&script) {
if exception.code() == ResultCode::Error {
eprintln!("{}", exception.value());
return Err(());
} else {
eprintln!("Unexpected eval return: {:?}", exception);
return Err(());
}
}
}
Err(e) => {
println!("{}", e);
return Err(());
}
}
let ctx = interp.context::<TestContext>(context_id);
println!(
"\n{} tests, {} passed, {} failed, {} errors",
ctx.num_tests, ctx.num_passed, ctx.num_failed, ctx.num_errors
);
if ctx.num_failed + ctx.num_errors == 0 {
Ok(())
} else {
Err(())
}
}
struct TestContext {
num_tests: usize,
num_passed: usize,
num_failed: usize,
num_errors: usize,
}
impl TestContext {
fn new() -> Self {
Self {
num_tests: 0,
num_passed: 0,
num_failed: 0,
num_errors: 0,
}
}
}
#[derive(Eq, PartialEq, Debug)]
enum Code {
Ok,
Error,
}
impl std::fmt::Display for Code {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Code::Ok => write!(f, "-ok"),
Code::Error => write!(f, "-error"),
}
}
}
#[derive(Debug)]
struct TestInfo {
name: String,
description: String,
setup: String,
body: String,
cleanup: String,
code: Code,
expect: String,
}
impl TestInfo {
fn new(name: &str, description: &str) -> Self {
Self {
name: name.into(),
description: description.into(),
setup: String::new(),
body: String::new(),
cleanup: String::new(),
code: Code::Ok,
expect: String::new(),
}
}
fn print_failure(&self, got_code: &str, received: &str) {
println!("\n*** FAILED {} {}", self.name, self.description);
println!("Expected {} <{}>", self.code.to_string(), self.expect);
println!("Received {} <{}>", got_code, received);
}
fn print_error(&self, result: &MoltResult) {
println!("\n*** ERROR {} {}", self.name, self.description);
println!("Expected {} <{}>", self.code.to_string(), self.expect);
match result {
Ok(val) => println!("Received -ok <{}>", val),
Err(exception) => match exception.code() {
ResultCode::Error => println!("Received -error <{}>", exception.value()),
ResultCode::Return => println!("Received -return <{}>", exception.value()),
ResultCode::Break => println!("Received -break <>"),
ResultCode::Continue => println!("Received -continue <>"),
_ => unimplemented!(),
},
}
}
fn print_helper_error(&self, part: &str, msg: &str) {
println!(
"\n*** ERROR (in {}) {} {}",
part, self.name, self.description
);
println!(" {}", msg);
}
}
fn test_cmd(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
check_args(1, argv, 4, 0, "name description args...")?;
let arg = argv[3].as_str();
if arg.starts_with('-') {
fancy_test(interp, context_id, argv)
} else {
simple_test(interp, context_id, argv)
}
}
fn simple_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
check_args(1, argv, 6, 6, "name description script -ok|-error result")?;
let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
info.body = argv[3].to_string();
info.expect = argv[5].to_string();
let code = argv[4].as_str();
info.code = if code == "-ok" {
Code::Ok
} else if code == "-error" {
Code::Error
} else {
incr_errors(interp, context_id);
info.print_helper_error("test command", &format!("invalid option: \"{}\"", code));
return molt_ok!();
};
run_test(interp, context_id, &info);
molt_ok!()
}
fn fancy_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
check_args(
1,
argv,
4,
0,
"name description option value ?option value...?",
)?;
let mut info = TestInfo::new(argv[1].as_str(), argv[2].as_str());
let mut iter = argv[3..].iter();
loop {
let opt = iter.next();
if opt.is_none() {
break;
}
let opt = opt.unwrap().as_str();
let val = iter.next();
if val.is_none() {
incr_errors(interp, context_id);
info.print_helper_error("test command", &format!("missing value for {}", opt));
return molt_ok!();
}
let val = val.unwrap().as_str();
match opt {
"-setup" => info.setup = val.to_string(),
"-body" => info.body = val.to_string(),
"-cleanup" => info.cleanup = val.to_string(),
"-ok" => {
info.code = Code::Ok;
info.expect = val.to_string();
}
"-error" => {
info.code = Code::Error;
info.expect = val.to_string();
}
_ => {
incr_errors(interp, context_id);
info.print_helper_error("test command", &format!("invalid option: \"{}\"", val));
return molt_ok!();
}
}
}
run_test(interp, context_id, &info);
molt_ok!()
}
fn run_test(interp: &mut Interp, context_id: ContextID, info: &TestInfo) {
interp.push_scope();
if let Err(exception) = interp.eval(&info.setup) {
if exception.code() == ResultCode::Error {
info.print_helper_error("-setup", exception.value().as_str());
}
}
let body = Value::from(&info.body);
let result = interp.eval_value(&body);
if let Err(exception) = interp.eval(&info.cleanup) {
if exception.code() == ResultCode::Error {
info.print_helper_error("-cleanup", exception.value().as_str());
}
}
interp.pop_scope();
let ctx = interp.context::<TestContext>(context_id);
ctx.num_tests += 1;
match &result {
Ok(out) => {
if info.code == Code::Ok {
if *out == Value::from(&info.expect) {
ctx.num_passed += 1;
} else {
ctx.num_failed += 1;
info.print_failure("-ok", &out.to_string());
}
return;
}
}
Err(exception) => {
if info.code == Code::Error {
if exception.value() == Value::from(&info.expect) {
ctx.num_passed += 1;
} else {
ctx.num_failed += 1;
info.print_failure("-error", exception.value().as_str());
}
return;
}
}
}
ctx.num_errors += 1;
info.print_error(&result);
}
fn incr_errors(interp: &mut Interp, context_id: ContextID) {
interp.context::<TestContext>(context_id).num_errors += 1;
}