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:
@@ -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: {}.{}",
|
||||
|
||||
Reference in New Issue
Block a user