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),
}
}
fn escape_for_display(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'\\' => out.push_str("\\\\"),
b'"' => out.push_str("\\\""),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0x20..=0x7E => out.push(b as char),
0x00..=0x1F | 0x7F => {
use std::fmt::Write;
let _ = write!(out, "\\x{:02X}", b);
}
_ => out.push(b as char),
}
}
out
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FailureKind {
#[default]
Other,
StringEq,
}
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>,
pub kind: FailureKind,
}
#[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,
kind: FailureKind::Other,
});
self.current_line = None;
}
pub fn record_string_eq_failure(&mut self, expected: String, actual: String) {
self.failures.push(TestFailure {
line: self.current_line,
message: "assertion failed: strings not equal".to_string(),
expected: Some(expected),
actual: Some(actual),
kind: FailureKind::StringEq,
});
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_or_empty().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_or_empty().to_string(),
_ => panic!("test.init: expected String (test name) on stack"),
};
let mut ctx = TEST_CONTEXT.lock().unwrap();
ctx.reset(Some(name));
stack
}
}
pub fn format_failure_detail(failure: &TestFailure) -> String {
use std::fmt::Write;
let mut out = String::new();
match (failure.kind, &failure.expected, &failure.actual) {
(FailureKind::StringEq, Some(e), Some(a)) => {
match failure.line {
Some(line) => writeln!(out, " at line {}: {}", line, failure.message).unwrap(),
None => writeln!(out, " {}", failure.message).unwrap(),
}
writeln!(
out,
" expected ({} bytes): \"{}\"",
e.len(),
escape_for_display(e)
)
.unwrap();
writeln!(
out,
" actual ({} bytes): \"{}\"",
a.len(),
escape_for_display(a)
)
.unwrap();
if e != a {
let common = e
.as_bytes()
.iter()
.zip(a.as_bytes().iter())
.take_while(|(x, y)| x == y)
.count();
writeln!(out, " first differ at byte {}", common).unwrap();
}
}
(_, Some(e), Some(a)) => {
let detail = format!("expected {}, got {}", e, a);
match failure.line {
Some(line) => writeln!(out, " at line {}: {}", line, detail).unwrap(),
None => writeln!(out, " {}", detail).unwrap(),
}
}
_ => match failure.line {
Some(line) => writeln!(out, " at line {}: {}", line, failure.message).unwrap(),
None => writeln!(out, " {}", failure.message).unwrap(),
},
}
out
}
#[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) {
print!("{}", format_failure_detail(failure));
}
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, expected_val) = pop(stack);
let (stack, actual_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, expected_val) = pop(stack);
let (stack, actual_val) = pop(stack);
let (expected, actual) = match (&expected_val, &actual_val) {
(Value::String(e), Value::String(a)) => (
e.as_str_or_empty().to_string(),
a.as_str_or_empty().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_string_eq_failure(expected, 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_or_empty().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;