feat: add Test effect and native testing framework

- Add Test effect with operations: assert, assertEqual, assertNotEqual,
  assertTrue, assertFalse, fail
- Implement Test effect handlers in interpreter with TestResults tracking
- Add values_equal method for comparing Value types in tests
- Update lux test command to discover and run test_* functions
- Create example test files: test_math.lux, test_lists.lux
- Add TESTING_DESIGN.md documentation
- Fix AST mismatches in C backend and compiler.rs for compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 01:20:30 -05:00
parent 9a42a7f540
commit ee9acce6ec
10 changed files with 866 additions and 107 deletions

View File

@@ -177,6 +177,40 @@ impl Value {
_ => None,
}
}
/// Compare two values for equality (for testing)
/// Returns true if the values are structurally equal
pub fn values_equal(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Int(x), Value::Int(y)) => x == y,
(Value::Float(x), Value::Float(y)) => (x - y).abs() < f64::EPSILON,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::String(x), Value::String(y)) => x == y,
(Value::Char(x), Value::Char(y)) => x == y,
(Value::Unit, Value::Unit) => true,
(Value::List(xs), Value::List(ys)) => {
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
}
(Value::Tuple(xs), Value::Tuple(ys)) => {
xs.len() == ys.len() && xs.iter().zip(ys.iter()).all(|(x, y)| Value::values_equal(x, y))
}
(Value::Record(xs), Value::Record(ys)) => {
xs.len() == ys.len() && xs.iter().all(|(k, v)| {
ys.get(k).map(|yv| Value::values_equal(v, yv)).unwrap_or(false)
})
}
(Value::Constructor { name: n1, fields: f1 }, Value::Constructor { name: n2, fields: f2 }) => {
n1 == n2 && f1.len() == f2.len() && f1.iter().zip(f2.iter()).all(|(x, y)| Value::values_equal(x, y))
}
(Value::Versioned { type_name: t1, version: v1, value: val1 },
Value::Versioned { type_name: t2, version: v2, value: val2 }) => {
t1 == t2 && v1 == v2 && Value::values_equal(val1, val2)
}
(Value::Json(j1), Value::Json(j2)) => j1 == j2,
// Functions and handlers cannot be compared for equality
_ => false,
}
}
}
/// Trait for extracting typed values from Value
@@ -545,6 +579,24 @@ pub struct Interpreter {
http_server: Arc<Mutex<Option<tiny_http::Server>>>,
/// Current HTTP request being handled (stored for respond operation)
current_http_request: Arc<Mutex<Option<tiny_http::Request>>>,
/// Test results for the Test effect
test_results: RefCell<TestResults>,
}
/// Results from running tests
#[derive(Debug, Clone, Default)]
pub struct TestResults {
pub passed: usize,
pub failed: usize,
pub failures: Vec<TestFailure>,
}
/// A single test failure
#[derive(Debug, Clone)]
pub struct TestFailure {
pub message: String,
pub expected: Option<String>,
pub actual: Option<String>,
}
impl Interpreter {
@@ -567,9 +619,20 @@ impl Interpreter {
in_handler_depth: 0,
http_server: Arc::new(Mutex::new(None)),
current_http_request: Arc::new(Mutex::new(None)),
test_results: RefCell::new(TestResults::default()),
}
}
/// Get the test results
pub fn get_test_results(&self) -> TestResults {
self.test_results.borrow().clone()
}
/// Reset test results for a new test run
pub fn reset_test_results(&self) {
*self.test_results.borrow_mut() = TestResults::default();
}
/// Set the initial value for the built-in State effect
pub fn set_state(&self, value: Value) {
*self.builtin_state.borrow_mut() = value;
@@ -3466,6 +3529,111 @@ impl Interpreter {
Ok(Value::Unit)
}
// Test effect for testing framework
("Test", "assert") => {
let condition = match request.args.first() {
Some(Value::Bool(b)) => *b,
_ => false,
};
let message = match request.args.get(1) {
Some(Value::String(s)) => s.clone(),
_ => "Assertion failed".to_string(),
};
if condition {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message,
expected: None,
actual: None,
});
}
Ok(Value::Unit)
}
("Test", "assertEqual") => {
let expected = request.args.first().cloned().unwrap_or(Value::Unit);
let actual = request.args.get(1).cloned().unwrap_or(Value::Unit);
if Value::values_equal(&expected, &actual) {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message: "Values not equal".to_string(),
expected: Some(format!("{}", expected)),
actual: Some(format!("{}", actual)),
});
}
Ok(Value::Unit)
}
("Test", "assertNotEqual") => {
let a = request.args.first().cloned().unwrap_or(Value::Unit);
let b = request.args.get(1).cloned().unwrap_or(Value::Unit);
if !Value::values_equal(&a, &b) {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message: "Values should not be equal".to_string(),
expected: Some(format!("not {}", a)),
actual: Some(format!("{}", b)),
});
}
Ok(Value::Unit)
}
("Test", "assertTrue") => {
let condition = match request.args.first() {
Some(Value::Bool(b)) => *b,
_ => false,
};
if condition {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message: "Expected true".to_string(),
expected: Some("true".to_string()),
actual: Some("false".to_string()),
});
}
Ok(Value::Unit)
}
("Test", "assertFalse") => {
let condition = match request.args.first() {
Some(Value::Bool(b)) => *b,
_ => true,
};
if !condition {
self.test_results.borrow_mut().passed += 1;
} else {
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message: "Expected false".to_string(),
expected: Some("false".to_string()),
actual: Some("true".to_string()),
});
}
Ok(Value::Unit)
}
("Test", "fail") => {
let message = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => "Test failed".to_string(),
};
self.test_results.borrow_mut().failed += 1;
self.test_results.borrow_mut().failures.push(TestFailure {
message,
expected: None,
actual: None,
});
Ok(Value::Unit)
}
_ => Err(RuntimeError {
message: format!(
"Unhandled effect operation: {}.{}",