seq_runtime/
test.rs

1//! Test framework support for Seq
2//!
3//! Provides assertion primitives and test context management for the `seqc test` runner.
4//! Assertions collect failures instead of panicking, allowing all tests to run and
5//! report comprehensive results.
6//!
7//! These functions are exported with C ABI for LLVM codegen to call.
8
9use crate::stack::{Stack, pop, push};
10use crate::value::Value;
11use std::sync::Mutex;
12
13/// A single test failure with context
14#[derive(Debug, Clone)]
15pub struct TestFailure {
16    pub message: String,
17    pub expected: Option<String>,
18    pub actual: Option<String>,
19}
20
21/// Test context that tracks assertion results
22#[derive(Debug, Default)]
23pub struct TestContext {
24    /// Current test name being executed
25    pub current_test: Option<String>,
26    /// Number of passed assertions
27    pub passes: usize,
28    /// Collected failures
29    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
65/// Global test context protected by mutex
66static TEST_CONTEXT: Mutex<TestContext> = Mutex::new(TestContext {
67    current_test: None,
68    passes: 0,
69    failures: Vec::new(),
70});
71
72/// Initialize test context for a new test
73///
74/// Stack effect: ( name -- )
75///
76/// # Safety
77/// Stack must have a String (test name) on top
78#[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/// Finalize test and print results
94///
95/// Stack effect: ( -- )
96///
97/// Prints pass/fail summary for the current test in a format parseable by the test runner.
98/// Output format: "test-name ... ok" or "test-name ... FAILED"
99///
100/// # Safety
101/// Stack pointer must be valid
102#[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        // Output pass in parseable format
109        println!("{} ... ok", test_name);
110    } else {
111        // Output failure in parseable format
112        println!("{} ... FAILED", test_name);
113        // Print failure details to stderr
114        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/// Check if any assertions failed
129///
130/// Stack effect: ( -- Int )
131///
132/// Returns 1 if there are failures, 0 if all passed.
133///
134/// # Safety
135/// Stack pointer must be valid
136#[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/// Assert that a value is truthy (non-zero)
144///
145/// Stack effect: ( Int -- )
146///
147/// Records failure if value is 0, records pass otherwise.
148///
149/// # Safety
150/// Stack must have an Int on top
151#[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/// Assert that a value is falsy (zero)
177///
178/// Stack effect: ( Int -- )
179///
180/// Records failure if value is non-zero, records pass otherwise.
181///
182/// # Safety
183/// Stack must have an Int on top
184#[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/// Assert that two integers are equal
213///
214/// Stack effect: ( expected actual -- )
215///
216/// Records failure if values differ, records pass otherwise.
217///
218/// # Safety
219/// Stack must have two Ints on top
220#[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/// Assert that two strings are equal
250///
251/// Stack effect: ( expected actual -- )
252///
253/// Records failure if strings differ, records pass otherwise.
254///
255/// # Safety
256/// Stack must have two Strings on top
257#[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/// Explicitly fail a test with a message
289///
290/// Stack effect: ( message -- )
291///
292/// Always records a failure with the given message.
293///
294/// # Safety
295/// Stack must have a String on top
296#[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/// Get the number of passed assertions
313///
314/// Stack effect: ( -- Int )
315///
316/// # Safety
317/// Stack pointer must be valid
318#[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/// Get the number of failed assertions
325///
326/// Stack effect: ( -- Int )
327///
328/// # Safety
329/// Stack pointer must be valid
330#[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}