1use crate::stack::{Stack, pop, push};
10use crate::value::Value;
11use std::sync::Mutex;
12
13fn display_value(val: &Value) -> String {
16 match val {
17 Value::Bool(b) => b.to_string(),
18 Value::Int(n) => n.to_string(),
19 other => format!("{:?}", other),
20 }
21}
22
23const MAX_PRINTED_FAILURES_PER_TEST: usize = 5;
28
29#[derive(Debug, Clone)]
31pub struct TestFailure {
32 pub line: Option<u32>,
34 pub message: String,
35 pub expected: Option<String>,
36 pub actual: Option<String>,
37}
38
39#[derive(Debug, Default)]
41pub struct TestContext {
42 pub current_test: Option<String>,
44 pub current_line: Option<u32>,
48 pub passes: usize,
50 pub failures: Vec<TestFailure>,
52}
53
54impl TestContext {
55 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn reset(&mut self, test_name: Option<String>) {
60 self.current_test = test_name;
61 self.current_line = None;
62 self.passes = 0;
63 self.failures.clear();
64 }
65
66 pub fn record_pass(&mut self) {
67 self.passes += 1;
68 self.current_line = None;
72 }
73
74 pub fn record_failure(
75 &mut self,
76 message: String,
77 expected: Option<String>,
78 actual: Option<String>,
79 ) {
80 self.failures.push(TestFailure {
81 line: self.current_line,
82 message,
83 expected,
84 actual,
85 });
86 self.current_line = None;
89 }
90
91 pub fn has_failures(&self) -> bool {
92 !self.failures.is_empty()
93 }
94}
95
96static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
98 current_test: None,
99 current_line: None,
100 passes: 0,
101 failures: Vec::new(),
102});
103
104#[unsafe(no_mangle)]
118pub unsafe extern "C" fn patch_seq_test_set_line(line: i64) {
119 let mut ctx = TEST_CONTEXT.lock().unwrap();
120 ctx.current_line = if line > 0 {
124 u32::try_from(line).ok()
125 } else {
126 None
127 };
128}
129
130#[unsafe(no_mangle)]
137pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
138 unsafe {
139 let (stack, name_val) = pop(stack);
140 let name = match name_val {
141 Value::String(s) => s.as_str().to_string(),
142 _ => panic!("test.init: expected String (test name) on stack"),
143 };
144
145 let mut ctx = TEST_CONTEXT.lock().unwrap();
146 ctx.reset(Some(name));
147 stack
148 }
149}
150
151#[unsafe(no_mangle)]
161pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
162 let ctx = TEST_CONTEXT.lock().unwrap();
163 let test_name = ctx.current_test.as_deref().unwrap_or("unknown");
164
165 if ctx.failures.is_empty() {
166 println!("{} ... ok", test_name);
168 } else {
169 println!("{} ... FAILED", test_name);
177 for failure in ctx.failures.iter().take(MAX_PRINTED_FAILURES_PER_TEST) {
178 let detail = match (&failure.expected, &failure.actual) {
179 (Some(e), Some(a)) => format!("expected {}, got {}", e, a),
180 _ => failure.message.clone(),
181 };
182 match failure.line {
183 Some(line) => println!(" at line {}: {}", line, detail),
184 None => println!(" {}", detail),
185 }
186 }
187 if ctx.failures.len() > MAX_PRINTED_FAILURES_PER_TEST {
188 let remaining = ctx.failures.len() - MAX_PRINTED_FAILURES_PER_TEST;
189 let s = if remaining == 1 { "" } else { "s" };
190 println!(" +{} more failure{}", remaining, s);
191 }
192 }
193
194 stack
195}
196
197#[unsafe(no_mangle)]
206pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
207 let ctx = TEST_CONTEXT.lock().unwrap();
208 let has_failures = ctx.has_failures();
209 unsafe { push(stack, Value::Bool(has_failures)) }
210}
211
212#[unsafe(no_mangle)]
221pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
222 unsafe {
223 let (stack, val) = pop(stack);
224 let condition = match val {
225 Value::Int(n) => n != 0,
226 Value::Bool(b) => b,
227 _ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
228 };
229
230 let mut ctx = TEST_CONTEXT.lock().unwrap();
231 if condition {
232 ctx.record_pass();
233 } else {
234 ctx.record_failure(
235 "assertion failed".to_string(),
236 Some("true".to_string()),
237 Some(display_value(&val)),
238 );
239 }
240
241 stack
242 }
243}
244
245#[unsafe(no_mangle)]
254pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
255 unsafe {
256 let (stack, val) = pop(stack);
257 let is_falsy = match val {
258 Value::Int(n) => n == 0,
259 Value::Bool(b) => !b,
260 _ => panic!(
261 "test.assert-not: expected Int or Bool on stack, got {:?}",
262 val
263 ),
264 };
265
266 let mut ctx = TEST_CONTEXT.lock().unwrap();
267 if is_falsy {
268 ctx.record_pass();
269 } else {
270 ctx.record_failure(
271 "assertion failed".to_string(),
272 Some("false".to_string()),
273 Some(display_value(&val)),
274 );
275 }
276
277 stack
278 }
279}
280
281#[unsafe(no_mangle)]
290pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
291 unsafe {
292 let (stack, actual_val) = pop(stack);
293 let (stack, expected_val) = pop(stack);
294
295 let (expected, actual) = match (&expected_val, &actual_val) {
296 (Value::Int(e), Value::Int(a)) => (*e, *a),
297 _ => panic!(
298 "test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
299 expected_val, actual_val
300 ),
301 };
302
303 let mut ctx = TEST_CONTEXT.lock().unwrap();
304 if expected == actual {
305 ctx.record_pass();
306 } else {
307 ctx.record_failure(
308 "assertion failed: values not equal".to_string(),
309 Some(expected.to_string()),
310 Some(actual.to_string()),
311 );
312 }
313
314 stack
315 }
316}
317
318#[unsafe(no_mangle)]
327pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
328 unsafe {
329 let (stack, actual_val) = pop(stack);
330 let (stack, expected_val) = pop(stack);
331
332 let (expected, actual) = match (&expected_val, &actual_val) {
333 (Value::String(e), Value::String(a)) => {
334 (e.as_str().to_string(), a.as_str().to_string())
335 }
336 _ => panic!(
337 "test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
338 expected_val, actual_val
339 ),
340 };
341
342 let mut ctx = TEST_CONTEXT.lock().unwrap();
343 if expected == actual {
344 ctx.record_pass();
345 } else {
346 ctx.record_failure(
347 "assertion failed: strings not equal".to_string(),
348 Some(format!("\"{}\"", expected)),
349 Some(format!("\"{}\"", actual)),
350 );
351 }
352
353 stack
354 }
355}
356
357#[unsafe(no_mangle)]
366pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
367 unsafe {
368 let (stack, msg_val) = pop(stack);
369 let message = match msg_val {
370 Value::String(s) => s.as_str().to_string(),
371 _ => panic!("test.fail: expected String (message) on stack"),
372 };
373
374 let mut ctx = TEST_CONTEXT.lock().unwrap();
375 ctx.record_failure(message, None, None);
376
377 stack
378 }
379}
380
381#[unsafe(no_mangle)]
388pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
389 let ctx = TEST_CONTEXT.lock().unwrap();
390 unsafe { push(stack, Value::Int(ctx.passes as i64)) }
391}
392
393#[unsafe(no_mangle)]
400pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
401 let ctx = TEST_CONTEXT.lock().unwrap();
402 unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
403}
404
405#[cfg(test)]
406mod tests;