Skip to main content

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/// Render a stack `Value` for a failure message — prefers the natural
14/// form for `Int` / `Bool`, falls back to debug for anything else.
15fn 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
23/// Maximum number of per-test assertion failures to print in the run
24/// summary. Additional failures are rolled up into a `+N more failure(s)`
25/// footer so noisy tests (loop-like assertions over lists) don't drown
26/// the overall report. Tune here if feedback suggests a different value.
27const MAX_PRINTED_FAILURES_PER_TEST: usize = 5;
28
29/// A single test failure with context
30#[derive(Debug, Clone)]
31pub struct TestFailure {
32    /// Source line of the assertion (1-indexed), if codegen set one.
33    pub line: Option<u32>,
34    pub message: String,
35    pub expected: Option<String>,
36    pub actual: Option<String>,
37}
38
39/// Test context that tracks assertion results
40#[derive(Debug, Default)]
41pub struct TestContext {
42    /// Current test name being executed
43    pub current_test: Option<String>,
44    /// Source line of the assertion most recently announced by codegen.
45    /// Set by `patch_seq_test_set_line` just before each `test.assert*`
46    /// call; captured into a `TestFailure` if the assertion fails.
47    pub current_line: Option<u32>,
48    /// Number of passed assertions
49    pub passes: usize,
50    /// Collected failures
51    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        // Consume the line so a following assertion without a `set_line`
69        // hook (defensive — span-less `WordCall`s don't emit one) can't
70        // inherit this one's attribution.
71        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        // Same rationale as `record_pass`: don't let this line bleed into
87        // the next assertion's record.
88        self.current_line = None;
89    }
90
91    pub fn has_failures(&self) -> bool {
92        !self.failures.is_empty()
93    }
94}
95
96/// Global test context protected by mutex
97static 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/// Announce the source line of the next `test.assert*` call.
105///
106/// Called by generated code immediately before each assertion so the
107/// runtime can attribute a failure to its source position. `line` is
108/// 1-indexed; pass 0 to clear.
109///
110/// This helper takes a raw `i64` rather than a stack argument because it
111/// is a compiler-emitted diagnostic, not a user-callable Seq builtin.
112///
113/// # Safety
114///
115/// Safe to call from any thread. Acquires the global test-context
116/// mutex; no other preconditions.
117#[unsafe(no_mangle)]
118pub unsafe extern "C" fn patch_seq_test_set_line(line: i64) {
119    let mut ctx = TEST_CONTEXT.lock().unwrap();
120    // Reject 0 (the agreed "clear" sentinel) and any value that can't
121    // fit in a u32 (no real source file has 4B lines, but be explicit
122    // about truncation intent rather than silently wrapping).
123    ctx.current_line = if line > 0 {
124        u32::try_from(line).ok()
125    } else {
126        None
127    };
128}
129
130/// Initialize test context for a new test
131///
132/// Stack effect: ( name -- )
133///
134/// # Safety
135/// Stack must have a String (test name) on top
136#[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/// Finalize test and print results
152///
153/// Stack effect: ( -- )
154///
155/// Prints pass/fail summary for the current test in a format parseable by the test runner.
156/// Output format: "test-name ... ok" or "test-name ... FAILED"
157///
158/// # Safety
159/// Stack pointer must be valid
160#[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        // Output pass in parseable format
167        println!("{} ... ok", test_name);
168    } else {
169        // Output failure in parseable format. Detail lines are emitted on
170        // STDOUT, indented, so the test runner can associate them with the
171        // preceding FAILED header on the same stream.
172        // Cap the per-test output so a flood of failures (e.g. a loop-like
173        // test walking a list) doesn't drown the summary. The first
174        // `MAX_PRINTED_FAILURES_PER_TEST` are printed in full; a footer
175        // counts anything suppressed.
176        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/// Check if any assertions failed
198///
199/// Stack effect: ( -- Int )
200///
201/// Returns 1 if there are failures, 0 if all passed.
202///
203/// # Safety
204/// Stack pointer must be valid
205#[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/// Assert that a value is truthy (non-zero)
213///
214/// Stack effect: ( Int -- )
215///
216/// Records failure if value is 0, records pass otherwise.
217///
218/// # Safety
219/// Stack must have an Int on top
220#[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/// Assert that a value is falsy (zero)
246///
247/// Stack effect: ( Int -- )
248///
249/// Records failure if value is non-zero, records pass otherwise.
250///
251/// # Safety
252/// Stack must have an Int on top
253#[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/// Assert that two integers are equal
282///
283/// Stack effect: ( expected actual -- )
284///
285/// Records failure if values differ, records pass otherwise.
286///
287/// # Safety
288/// Stack must have two Ints on top
289#[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/// Assert that two strings are equal
319///
320/// Stack effect: ( expected actual -- )
321///
322/// Records failure if strings differ, records pass otherwise.
323///
324/// # Safety
325/// Stack must have two Strings on top
326#[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/// Explicitly fail a test with a message
358///
359/// Stack effect: ( message -- )
360///
361/// Always records a failure with the given message.
362///
363/// # Safety
364/// Stack must have a String on top
365#[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/// Get the number of passed assertions
382///
383/// Stack effect: ( -- Int )
384///
385/// # Safety
386/// Stack pointer must be valid
387#[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/// Get the number of failed assertions
394///
395/// Stack effect: ( -- Int )
396///
397/// # Safety
398/// Stack pointer must be valid
399#[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;