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)]
143pub unsafe extern "C" fn patch_seq_test_set_name(stack: Stack) -> Stack {
144 unsafe {
145 let (stack, name_val) = pop(stack);
146 let name = match name_val {
147 Value::String(s) => s.as_str().to_string(),
148 _ => panic!("test.set-name: expected String (test name) on stack"),
149 };
150 let mut ctx = TEST_CONTEXT.lock().unwrap();
151 ctx.current_test = Some(name);
152 stack
153 }
154}
155
156#[unsafe(no_mangle)]
163pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
164 unsafe {
165 let (stack, name_val) = pop(stack);
166 let name = match name_val {
167 Value::String(s) => s.as_str().to_string(),
168 _ => panic!("test.init: expected String (test name) on stack"),
169 };
170
171 let mut ctx = TEST_CONTEXT.lock().unwrap();
172 ctx.reset(Some(name));
173 stack
174 }
175}
176
177#[unsafe(no_mangle)]
187pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
188 let ctx = TEST_CONTEXT.lock().unwrap();
189 let test_name = ctx.current_test.as_deref().unwrap_or("unknown");
190
191 if ctx.failures.is_empty() {
192 println!("{} ... ok", test_name);
194 } else {
195 println!("{} ... FAILED", test_name);
203 for failure in ctx.failures.iter().take(MAX_PRINTED_FAILURES_PER_TEST) {
204 let detail = match (&failure.expected, &failure.actual) {
205 (Some(e), Some(a)) => format!("expected {}, got {}", e, a),
206 _ => failure.message.clone(),
207 };
208 match failure.line {
209 Some(line) => println!(" at line {}: {}", line, detail),
210 None => println!(" {}", detail),
211 }
212 }
213 if ctx.failures.len() > MAX_PRINTED_FAILURES_PER_TEST {
214 let remaining = ctx.failures.len() - MAX_PRINTED_FAILURES_PER_TEST;
215 let s = if remaining == 1 { "" } else { "s" };
216 println!(" +{} more failure{}", remaining, s);
217 }
218 }
219
220 stack
221}
222
223#[unsafe(no_mangle)]
232pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
233 let ctx = TEST_CONTEXT.lock().unwrap();
234 let has_failures = ctx.has_failures();
235 unsafe { push(stack, Value::Bool(has_failures)) }
236}
237
238#[unsafe(no_mangle)]
247pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
248 unsafe {
249 let (stack, val) = pop(stack);
250 let condition = match val {
251 Value::Int(n) => n != 0,
252 Value::Bool(b) => b,
253 _ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
254 };
255
256 let mut ctx = TEST_CONTEXT.lock().unwrap();
257 if condition {
258 ctx.record_pass();
259 } else {
260 ctx.record_failure(
261 "assertion failed".to_string(),
262 Some("true".to_string()),
263 Some(display_value(&val)),
264 );
265 }
266
267 stack
268 }
269}
270
271#[unsafe(no_mangle)]
280pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
281 unsafe {
282 let (stack, val) = pop(stack);
283 let is_falsy = match val {
284 Value::Int(n) => n == 0,
285 Value::Bool(b) => !b,
286 _ => panic!(
287 "test.assert-not: expected Int or Bool on stack, got {:?}",
288 val
289 ),
290 };
291
292 let mut ctx = TEST_CONTEXT.lock().unwrap();
293 if is_falsy {
294 ctx.record_pass();
295 } else {
296 ctx.record_failure(
297 "assertion failed".to_string(),
298 Some("false".to_string()),
299 Some(display_value(&val)),
300 );
301 }
302
303 stack
304 }
305}
306
307#[unsafe(no_mangle)]
316pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
317 unsafe {
318 let (stack, actual_val) = pop(stack);
319 let (stack, expected_val) = pop(stack);
320
321 let (expected, actual) = match (&expected_val, &actual_val) {
322 (Value::Int(e), Value::Int(a)) => (*e, *a),
323 _ => panic!(
324 "test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
325 expected_val, actual_val
326 ),
327 };
328
329 let mut ctx = TEST_CONTEXT.lock().unwrap();
330 if expected == actual {
331 ctx.record_pass();
332 } else {
333 ctx.record_failure(
334 "assertion failed: values not equal".to_string(),
335 Some(expected.to_string()),
336 Some(actual.to_string()),
337 );
338 }
339
340 stack
341 }
342}
343
344#[unsafe(no_mangle)]
353pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
354 unsafe {
355 let (stack, actual_val) = pop(stack);
356 let (stack, expected_val) = pop(stack);
357
358 let (expected, actual) = match (&expected_val, &actual_val) {
359 (Value::String(e), Value::String(a)) => {
360 (e.as_str().to_string(), a.as_str().to_string())
361 }
362 _ => panic!(
363 "test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
364 expected_val, actual_val
365 ),
366 };
367
368 let mut ctx = TEST_CONTEXT.lock().unwrap();
369 if expected == actual {
370 ctx.record_pass();
371 } else {
372 ctx.record_failure(
373 "assertion failed: strings not equal".to_string(),
374 Some(format!("\"{}\"", expected)),
375 Some(format!("\"{}\"", actual)),
376 );
377 }
378
379 stack
380 }
381}
382
383#[unsafe(no_mangle)]
392pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
393 unsafe {
394 let (stack, msg_val) = pop(stack);
395 let message = match msg_val {
396 Value::String(s) => s.as_str().to_string(),
397 _ => panic!("test.fail: expected String (message) on stack"),
398 };
399
400 let mut ctx = TEST_CONTEXT.lock().unwrap();
401 ctx.record_failure(message, None, None);
402
403 stack
404 }
405}
406
407#[unsafe(no_mangle)]
414pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
415 let ctx = TEST_CONTEXT.lock().unwrap();
416 unsafe { push(stack, Value::Int(ctx.passes as i64)) }
417}
418
419#[unsafe(no_mangle)]
426pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
427 let ctx = TEST_CONTEXT.lock().unwrap();
428 unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
429}
430
431#[cfg(test)]
432mod tests;