use crate::stack::{Stack, pop, push};
use crate::value::Value;
use std::sync::Mutex;
fn display_value(val: &Value) -> String {
match val {
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
other => format!("{:?}", other),
}
}
const MAX_PRINTED_FAILURES_PER_TEST: usize = 5;
#[derive(Debug, Clone)]
pub struct TestFailure {
pub line: Option<u32>,
pub message: String,
pub expected: Option<String>,
pub actual: Option<String>,
}
#[derive(Debug, Default)]
pub struct TestContext {
pub current_test: Option<String>,
pub current_line: Option<u32>,
pub passes: usize,
pub failures: Vec<TestFailure>,
}
impl TestContext {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self, test_name: Option<String>) {
self.current_test = test_name;
self.current_line = None;
self.passes = 0;
self.failures.clear();
}
pub fn record_pass(&mut self) {
self.passes += 1;
self.current_line = None;
}
pub fn record_failure(
&mut self,
message: String,
expected: Option<String>,
actual: Option<String>,
) {
self.failures.push(TestFailure {
line: self.current_line,
message,
expected,
actual,
});
self.current_line = None;
}
pub fn has_failures(&self) -> bool {
!self.failures.is_empty()
}
}
static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
current_test: None,
current_line: None,
passes: 0,
failures: Vec::new(),
});
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_set_line(line: i64) {
let mut ctx = TEST_CONTEXT.lock().unwrap();
ctx.current_line = if line > 0 {
u32::try_from(line).ok()
} else {
None
};
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_set_name(stack: Stack) -> Stack {
unsafe {
let (stack, name_val) = pop(stack);
let name = match name_val {
Value::String(s) => s.as_str().to_string(),
_ => panic!("test.set-name: expected String (test name) on stack"),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
ctx.current_test = Some(name);
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
unsafe {
let (stack, name_val) = pop(stack);
let name = match name_val {
Value::String(s) => s.as_str().to_string(),
_ => panic!("test.init: expected String (test name) on stack"),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
ctx.reset(Some(name));
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
let ctx = TEST_CONTEXT.lock().unwrap();
let test_name = ctx.current_test.as_deref().unwrap_or("unknown");
if ctx.failures.is_empty() {
println!("{} ... ok", test_name);
} else {
println!("{} ... FAILED", test_name);
for failure in ctx.failures.iter().take(MAX_PRINTED_FAILURES_PER_TEST) {
let detail = match (&failure.expected, &failure.actual) {
(Some(e), Some(a)) => format!("expected {}, got {}", e, a),
_ => failure.message.clone(),
};
match failure.line {
Some(line) => println!(" at line {}: {}", line, detail),
None => println!(" {}", detail),
}
}
if ctx.failures.len() > MAX_PRINTED_FAILURES_PER_TEST {
let remaining = ctx.failures.len() - MAX_PRINTED_FAILURES_PER_TEST;
let s = if remaining == 1 { "" } else { "s" };
println!(" +{} more failure{}", remaining, s);
}
}
stack
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
let ctx = TEST_CONTEXT.lock().unwrap();
let has_failures = ctx.has_failures();
unsafe { push(stack, Value::Bool(has_failures)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
unsafe {
let (stack, val) = pop(stack);
let condition = match val {
Value::Int(n) => n != 0,
Value::Bool(b) => b,
_ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
if condition {
ctx.record_pass();
} else {
ctx.record_failure(
"assertion failed".to_string(),
Some("true".to_string()),
Some(display_value(&val)),
);
}
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
unsafe {
let (stack, val) = pop(stack);
let is_falsy = match val {
Value::Int(n) => n == 0,
Value::Bool(b) => !b,
_ => panic!(
"test.assert-not: expected Int or Bool on stack, got {:?}",
val
),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
if is_falsy {
ctx.record_pass();
} else {
ctx.record_failure(
"assertion failed".to_string(),
Some("false".to_string()),
Some(display_value(&val)),
);
}
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
unsafe {
let (stack, actual_val) = pop(stack);
let (stack, expected_val) = pop(stack);
let (expected, actual) = match (&expected_val, &actual_val) {
(Value::Int(e), Value::Int(a)) => (*e, *a),
_ => panic!(
"test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
expected_val, actual_val
),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
if expected == actual {
ctx.record_pass();
} else {
ctx.record_failure(
"assertion failed: values not equal".to_string(),
Some(expected.to_string()),
Some(actual.to_string()),
);
}
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
unsafe {
let (stack, actual_val) = pop(stack);
let (stack, expected_val) = pop(stack);
let (expected, actual) = match (&expected_val, &actual_val) {
(Value::String(e), Value::String(a)) => {
(e.as_str().to_string(), a.as_str().to_string())
}
_ => panic!(
"test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
expected_val, actual_val
),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
if expected == actual {
ctx.record_pass();
} else {
ctx.record_failure(
"assertion failed: strings not equal".to_string(),
Some(format!("\"{}\"", expected)),
Some(format!("\"{}\"", actual)),
);
}
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
unsafe {
let (stack, msg_val) = pop(stack);
let message = match msg_val {
Value::String(s) => s.as_str().to_string(),
_ => panic!("test.fail: expected String (message) on stack"),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
ctx.record_failure(message, None, None);
stack
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
let ctx = TEST_CONTEXT.lock().unwrap();
unsafe { push(stack, Value::Int(ctx.passes as i64)) }
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
let ctx = TEST_CONTEXT.lock().unwrap();
unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
}
#[cfg(test)]
mod tests;