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
23fn escape_for_display(s: &str) -> String {
34 let mut out = String::with_capacity(s.len());
35 for b in s.bytes() {
36 match b {
37 b'\\' => out.push_str("\\\\"),
38 b'"' => out.push_str("\\\""),
39 b'\n' => out.push_str("\\n"),
40 b'\r' => out.push_str("\\r"),
41 b'\t' => out.push_str("\\t"),
42 0x20..=0x7E => out.push(b as char),
43 0x00..=0x1F | 0x7F => {
44 use std::fmt::Write;
45 let _ = write!(out, "\\x{:02X}", b);
46 }
47 _ => out.push(b as char),
48 }
49 }
50 out
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum FailureKind {
59 #[default]
60 Other,
61 StringEq,
62}
63
64const MAX_PRINTED_FAILURES_PER_TEST: usize = 5;
69
70#[derive(Debug, Clone)]
72pub struct TestFailure {
73 pub line: Option<u32>,
75 pub message: String,
76 pub expected: Option<String>,
79 pub actual: Option<String>,
80 pub kind: FailureKind,
81}
82
83#[derive(Debug, Default)]
85pub struct TestContext {
86 pub current_test: Option<String>,
88 pub current_line: Option<u32>,
92 pub passes: usize,
94 pub failures: Vec<TestFailure>,
96}
97
98impl TestContext {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn reset(&mut self, test_name: Option<String>) {
104 self.current_test = test_name;
105 self.current_line = None;
106 self.passes = 0;
107 self.failures.clear();
108 }
109
110 pub fn record_pass(&mut self) {
111 self.passes += 1;
112 self.current_line = None;
116 }
117
118 pub fn record_failure(
119 &mut self,
120 message: String,
121 expected: Option<String>,
122 actual: Option<String>,
123 ) {
124 self.failures.push(TestFailure {
125 line: self.current_line,
126 message,
127 expected,
128 actual,
129 kind: FailureKind::Other,
130 });
131 self.current_line = None;
134 }
135
136 pub fn record_string_eq_failure(&mut self, expected: String, actual: String) {
140 self.failures.push(TestFailure {
141 line: self.current_line,
142 message: "assertion failed: strings not equal".to_string(),
143 expected: Some(expected),
144 actual: Some(actual),
145 kind: FailureKind::StringEq,
146 });
147 self.current_line = None;
148 }
149
150 pub fn has_failures(&self) -> bool {
151 !self.failures.is_empty()
152 }
153}
154
155static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
157 current_test: None,
158 current_line: None,
159 passes: 0,
160 failures: Vec::new(),
161});
162
163#[unsafe(no_mangle)]
177pub unsafe extern "C" fn patch_seq_test_set_line(line: i64) {
178 let mut ctx = TEST_CONTEXT.lock().unwrap();
179 ctx.current_line = if line > 0 {
183 u32::try_from(line).ok()
184 } else {
185 None
186 };
187}
188
189#[unsafe(no_mangle)]
202pub unsafe extern "C" fn patch_seq_test_set_name(stack: Stack) -> Stack {
203 unsafe {
204 let (stack, name_val) = pop(stack);
205 let name = match name_val {
206 Value::String(s) => s.as_str_or_empty().to_string(),
207 _ => panic!("test.set-name: expected String (test name) on stack"),
208 };
209 let mut ctx = TEST_CONTEXT.lock().unwrap();
210 ctx.current_test = Some(name);
211 stack
212 }
213}
214
215#[unsafe(no_mangle)]
222pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
223 unsafe {
224 let (stack, name_val) = pop(stack);
225 let name = match name_val {
226 Value::String(s) => s.as_str_or_empty().to_string(),
227 _ => panic!("test.init: expected String (test name) on stack"),
228 };
229
230 let mut ctx = TEST_CONTEXT.lock().unwrap();
231 ctx.reset(Some(name));
232 stack
233 }
234}
235
236pub fn format_failure_detail(failure: &TestFailure) -> String {
243 use std::fmt::Write;
244 let mut out = String::new();
245
246 match (failure.kind, &failure.expected, &failure.actual) {
247 (FailureKind::StringEq, Some(e), Some(a)) => {
248 match failure.line {
249 Some(line) => writeln!(out, " at line {}: {}", line, failure.message).unwrap(),
250 None => writeln!(out, " {}", failure.message).unwrap(),
251 }
252 writeln!(
253 out,
254 " expected ({} bytes): \"{}\"",
255 e.len(),
256 escape_for_display(e)
257 )
258 .unwrap();
259 writeln!(
260 out,
261 " actual ({} bytes): \"{}\"",
262 a.len(),
263 escape_for_display(a)
264 )
265 .unwrap();
266 if e != a {
267 let common = e
268 .as_bytes()
269 .iter()
270 .zip(a.as_bytes().iter())
271 .take_while(|(x, y)| x == y)
272 .count();
273 writeln!(out, " first differ at byte {}", common).unwrap();
274 }
275 }
276 (_, Some(e), Some(a)) => {
277 let detail = format!("expected {}, got {}", e, a);
278 match failure.line {
279 Some(line) => writeln!(out, " at line {}: {}", line, detail).unwrap(),
280 None => writeln!(out, " {}", detail).unwrap(),
281 }
282 }
283 _ => match failure.line {
284 Some(line) => writeln!(out, " at line {}: {}", line, failure.message).unwrap(),
285 None => writeln!(out, " {}", failure.message).unwrap(),
286 },
287 }
288
289 out
290}
291
292#[unsafe(no_mangle)]
302pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
303 let ctx = TEST_CONTEXT.lock().unwrap();
304 let test_name = ctx.current_test.as_deref().unwrap_or("unknown");
305
306 if ctx.failures.is_empty() {
307 println!("{} ... ok", test_name);
309 } else {
310 println!("{} ... FAILED", test_name);
318 for failure in ctx.failures.iter().take(MAX_PRINTED_FAILURES_PER_TEST) {
319 print!("{}", format_failure_detail(failure));
320 }
321 if ctx.failures.len() > MAX_PRINTED_FAILURES_PER_TEST {
322 let remaining = ctx.failures.len() - MAX_PRINTED_FAILURES_PER_TEST;
323 let s = if remaining == 1 { "" } else { "s" };
324 println!(" +{} more failure{}", remaining, s);
325 }
326 }
327
328 stack
329}
330
331#[unsafe(no_mangle)]
340pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
341 let ctx = TEST_CONTEXT.lock().unwrap();
342 let has_failures = ctx.has_failures();
343 unsafe { push(stack, Value::Bool(has_failures)) }
344}
345
346#[unsafe(no_mangle)]
355pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
356 unsafe {
357 let (stack, val) = pop(stack);
358 let condition = match val {
359 Value::Int(n) => n != 0,
360 Value::Bool(b) => b,
361 _ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
362 };
363
364 let mut ctx = TEST_CONTEXT.lock().unwrap();
365 if condition {
366 ctx.record_pass();
367 } else {
368 ctx.record_failure(
369 "assertion failed".to_string(),
370 Some("true".to_string()),
371 Some(display_value(&val)),
372 );
373 }
374
375 stack
376 }
377}
378
379#[unsafe(no_mangle)]
388pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
389 unsafe {
390 let (stack, val) = pop(stack);
391 let is_falsy = match val {
392 Value::Int(n) => n == 0,
393 Value::Bool(b) => !b,
394 _ => panic!(
395 "test.assert-not: expected Int or Bool on stack, got {:?}",
396 val
397 ),
398 };
399
400 let mut ctx = TEST_CONTEXT.lock().unwrap();
401 if is_falsy {
402 ctx.record_pass();
403 } else {
404 ctx.record_failure(
405 "assertion failed".to_string(),
406 Some("false".to_string()),
407 Some(display_value(&val)),
408 );
409 }
410
411 stack
412 }
413}
414
415#[unsafe(no_mangle)]
424pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
425 unsafe {
426 let (stack, expected_val) = pop(stack);
427 let (stack, actual_val) = pop(stack);
428
429 let (expected, actual) = match (&expected_val, &actual_val) {
430 (Value::Int(e), Value::Int(a)) => (*e, *a),
431 _ => panic!(
432 "test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
433 expected_val, actual_val
434 ),
435 };
436
437 let mut ctx = TEST_CONTEXT.lock().unwrap();
438 if expected == actual {
439 ctx.record_pass();
440 } else {
441 ctx.record_failure(
442 "assertion failed: values not equal".to_string(),
443 Some(expected.to_string()),
444 Some(actual.to_string()),
445 );
446 }
447
448 stack
449 }
450}
451
452#[unsafe(no_mangle)]
461pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
462 unsafe {
463 let (stack, expected_val) = pop(stack);
464 let (stack, actual_val) = pop(stack);
465
466 let (expected, actual) = match (&expected_val, &actual_val) {
467 (Value::String(e), Value::String(a)) => (
468 e.as_str_or_empty().to_string(),
469 a.as_str_or_empty().to_string(),
470 ),
471 _ => panic!(
472 "test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
473 expected_val, actual_val
474 ),
475 };
476
477 let mut ctx = TEST_CONTEXT.lock().unwrap();
478 if expected == actual {
479 ctx.record_pass();
480 } else {
481 ctx.record_string_eq_failure(expected, actual);
482 }
483
484 stack
485 }
486}
487
488#[unsafe(no_mangle)]
497pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
498 unsafe {
499 let (stack, msg_val) = pop(stack);
500 let message = match msg_val {
501 Value::String(s) => s.as_str_or_empty().to_string(),
502 _ => panic!("test.fail: expected String (message) on stack"),
503 };
504
505 let mut ctx = TEST_CONTEXT.lock().unwrap();
506 ctx.record_failure(message, None, None);
507
508 stack
509 }
510}
511
512#[unsafe(no_mangle)]
519pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
520 let ctx = TEST_CONTEXT.lock().unwrap();
521 unsafe { push(stack, Value::Int(ctx.passes as i64)) }
522}
523
524#[unsafe(no_mangle)]
531pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
532 let ctx = TEST_CONTEXT.lock().unwrap();
533 unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
534}
535
536#[cfg(test)]
537mod tests;