1use crate::stack::{Stack, pop, push};
10use crate::value::Value;
11use std::sync::Mutex;
12
13#[derive(Debug, Clone)]
15pub struct TestFailure {
16 pub message: String,
17 pub expected: Option<String>,
18 pub actual: Option<String>,
19}
20
21#[derive(Debug, Default)]
23pub struct TestContext {
24 pub current_test: Option<String>,
26 pub passes: usize,
28 pub failures: Vec<TestFailure>,
30}
31
32impl TestContext {
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn reset(&mut self, test_name: Option<String>) {
38 self.current_test = test_name;
39 self.passes = 0;
40 self.failures.clear();
41 }
42
43 pub fn record_pass(&mut self) {
44 self.passes += 1;
45 }
46
47 pub fn record_failure(
48 &mut self,
49 message: String,
50 expected: Option<String>,
51 actual: Option<String>,
52 ) {
53 self.failures.push(TestFailure {
54 message,
55 expected,
56 actual,
57 });
58 }
59
60 pub fn has_failures(&self) -> bool {
61 !self.failures.is_empty()
62 }
63}
64
65static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
67 current_test: None,
68 passes: 0,
69 failures: Vec::new(),
70});
71
72#[unsafe(no_mangle)]
79pub unsafe extern "C" fn patch_seq_test_init(stack: Stack) -> Stack {
80 unsafe {
81 let (stack, name_val) = pop(stack);
82 let name = match name_val {
83 Value::String(s) => s.as_str().to_string(),
84 _ => panic!("test.init: expected String (test name) on stack"),
85 };
86
87 let mut ctx = TEST_CONTEXT.lock().unwrap();
88 ctx.reset(Some(name));
89 stack
90 }
91}
92
93#[unsafe(no_mangle)]
103pub unsafe extern "C" fn patch_seq_test_finish(stack: Stack) -> Stack {
104 let ctx = TEST_CONTEXT.lock().unwrap();
105 let test_name = ctx.current_test.as_deref().unwrap_or("unknown");
106
107 if ctx.failures.is_empty() {
108 println!("{} ... ok", test_name);
110 } else {
111 println!("{} ... FAILED", test_name);
113 for failure in &ctx.failures {
115 eprintln!(" {}", failure.message);
116 if let Some(ref expected) = failure.expected {
117 eprintln!(" expected: {}", expected);
118 }
119 if let Some(ref actual) = failure.actual {
120 eprintln!(" actual: {}", actual);
121 }
122 }
123 }
124
125 stack
126}
127
128#[unsafe(no_mangle)]
137pub unsafe extern "C" fn patch_seq_test_has_failures(stack: Stack) -> Stack {
138 let ctx = TEST_CONTEXT.lock().unwrap();
139 let has_failures = if ctx.has_failures() { 1 } else { 0 };
140 unsafe { push(stack, Value::Int(has_failures)) }
141}
142
143#[unsafe(no_mangle)]
152pub unsafe extern "C" fn patch_seq_test_assert(stack: Stack) -> Stack {
153 unsafe {
154 let (stack, val) = pop(stack);
155 let condition = match val {
156 Value::Int(n) => n != 0,
157 Value::Bool(b) => b,
158 _ => panic!("test.assert: expected Int or Bool on stack, got {:?}", val),
159 };
160
161 let mut ctx = TEST_CONTEXT.lock().unwrap();
162 if condition {
163 ctx.record_pass();
164 } else {
165 ctx.record_failure(
166 "assertion failed: expected truthy value".to_string(),
167 Some("non-zero".to_string()),
168 Some("0".to_string()),
169 );
170 }
171
172 stack
173 }
174}
175
176#[unsafe(no_mangle)]
185pub unsafe extern "C" fn patch_seq_test_assert_not(stack: Stack) -> Stack {
186 unsafe {
187 let (stack, val) = pop(stack);
188 let is_falsy = match val {
189 Value::Int(n) => n == 0,
190 Value::Bool(b) => !b,
191 _ => panic!(
192 "test.assert-not: expected Int or Bool on stack, got {:?}",
193 val
194 ),
195 };
196
197 let mut ctx = TEST_CONTEXT.lock().unwrap();
198 if is_falsy {
199 ctx.record_pass();
200 } else {
201 ctx.record_failure(
202 "assertion failed: expected falsy value".to_string(),
203 Some("0".to_string()),
204 Some(format!("{:?}", val)),
205 );
206 }
207
208 stack
209 }
210}
211
212#[unsafe(no_mangle)]
221pub unsafe extern "C" fn patch_seq_test_assert_eq(stack: Stack) -> Stack {
222 unsafe {
223 let (stack, actual_val) = pop(stack);
224 let (stack, expected_val) = pop(stack);
225
226 let (expected, actual) = match (&expected_val, &actual_val) {
227 (Value::Int(e), Value::Int(a)) => (*e, *a),
228 _ => panic!(
229 "test.assert-eq: expected two Ints on stack, got {:?} and {:?}",
230 expected_val, actual_val
231 ),
232 };
233
234 let mut ctx = TEST_CONTEXT.lock().unwrap();
235 if expected == actual {
236 ctx.record_pass();
237 } else {
238 ctx.record_failure(
239 "assertion failed: values not equal".to_string(),
240 Some(expected.to_string()),
241 Some(actual.to_string()),
242 );
243 }
244
245 stack
246 }
247}
248
249#[unsafe(no_mangle)]
258pub unsafe extern "C" fn patch_seq_test_assert_eq_str(stack: Stack) -> Stack {
259 unsafe {
260 let (stack, actual_val) = pop(stack);
261 let (stack, expected_val) = pop(stack);
262
263 let (expected, actual) = match (&expected_val, &actual_val) {
264 (Value::String(e), Value::String(a)) => {
265 (e.as_str().to_string(), a.as_str().to_string())
266 }
267 _ => panic!(
268 "test.assert-eq-str: expected two Strings on stack, got {:?} and {:?}",
269 expected_val, actual_val
270 ),
271 };
272
273 let mut ctx = TEST_CONTEXT.lock().unwrap();
274 if expected == actual {
275 ctx.record_pass();
276 } else {
277 ctx.record_failure(
278 "assertion failed: strings not equal".to_string(),
279 Some(format!("\"{}\"", expected)),
280 Some(format!("\"{}\"", actual)),
281 );
282 }
283
284 stack
285 }
286}
287
288#[unsafe(no_mangle)]
297pub unsafe extern "C" fn patch_seq_test_fail(stack: Stack) -> Stack {
298 unsafe {
299 let (stack, msg_val) = pop(stack);
300 let message = match msg_val {
301 Value::String(s) => s.as_str().to_string(),
302 _ => panic!("test.fail: expected String (message) on stack"),
303 };
304
305 let mut ctx = TEST_CONTEXT.lock().unwrap();
306 ctx.record_failure(message, None, None);
307
308 stack
309 }
310}
311
312#[unsafe(no_mangle)]
319pub unsafe extern "C" fn patch_seq_test_pass_count(stack: Stack) -> Stack {
320 let ctx = TEST_CONTEXT.lock().unwrap();
321 unsafe { push(stack, Value::Int(ctx.passes as i64)) }
322}
323
324#[unsafe(no_mangle)]
331pub unsafe extern "C" fn patch_seq_test_fail_count(stack: Stack) -> Stack {
332 let ctx = TEST_CONTEXT.lock().unwrap();
333 unsafe { push(stack, Value::Int(ctx.failures.len() as i64)) }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_context_reset() {
342 let mut ctx = TestContext::new();
343 ctx.record_pass();
344 ctx.record_failure("test".to_string(), None, None);
345
346 assert_eq!(ctx.passes, 1);
347 assert_eq!(ctx.failures.len(), 1);
348
349 ctx.reset(Some("new-test".to_string()));
350
351 assert_eq!(ctx.passes, 0);
352 assert!(ctx.failures.is_empty());
353 assert_eq!(ctx.current_test, Some("new-test".to_string()));
354 }
355
356 #[test]
357 fn test_context_has_failures() {
358 let mut ctx = TestContext::new();
359 assert!(!ctx.has_failures());
360
361 ctx.record_failure("error".to_string(), None, None);
362 assert!(ctx.has_failures());
363 }
364}