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:
223
src/main.rs
223
src/main.rs
@@ -1,6 +1,7 @@
|
||||
//! Lux - A functional programming language with first-class effects
|
||||
|
||||
mod ast;
|
||||
mod codegen;
|
||||
mod compiler;
|
||||
mod debugger;
|
||||
mod diagnostics;
|
||||
@@ -364,80 +365,208 @@ fn run_tests(args: &[String]) {
|
||||
// Find test files
|
||||
let pattern = args.first().map(|s| s.as_str());
|
||||
|
||||
// Look for test files in current directory and tests/ subdirectory
|
||||
// Look for test files in multiple locations
|
||||
let mut test_files = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(".").into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||
test_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Current directory
|
||||
collect_test_files(".", pattern, &mut test_files);
|
||||
|
||||
// tests/ subdirectory
|
||||
if Path::new("tests").is_dir() {
|
||||
collect_test_files("tests", pattern, &mut test_files);
|
||||
}
|
||||
|
||||
if Path::new("tests").is_dir() {
|
||||
for entry in fs::read_dir("tests").into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||
test_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
// examples/ subdirectory (for example tests)
|
||||
if Path::new("examples").is_dir() {
|
||||
collect_test_files("examples", pattern, &mut test_files);
|
||||
}
|
||||
|
||||
// If a specific file is given, use that
|
||||
if let Some(p) = pattern {
|
||||
if Path::new(p).is_file() {
|
||||
test_files.clear();
|
||||
test_files.push(std::path::PathBuf::from(p));
|
||||
}
|
||||
}
|
||||
|
||||
if test_files.is_empty() {
|
||||
println!("No test files found.");
|
||||
println!("Test files should be named test_*.lux or *_test.lux");
|
||||
println!("Or contain functions named test_*");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut passed = 0;
|
||||
let mut failed = 0;
|
||||
println!("Running tests...\n");
|
||||
|
||||
let mut total_passed = 0;
|
||||
let mut total_failed = 0;
|
||||
let mut all_failures = Vec::new();
|
||||
|
||||
for test_file in &test_files {
|
||||
let path_str = test_file.to_string_lossy().to_string();
|
||||
print!("Testing {}... ", path_str);
|
||||
|
||||
// Run the test file
|
||||
let result = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.arg(&path_str)
|
||||
.output();
|
||||
|
||||
match result {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("OK");
|
||||
passed += 1;
|
||||
}
|
||||
Ok(output) => {
|
||||
println!("FAILED");
|
||||
if !output.stderr.is_empty() {
|
||||
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
failed += 1;
|
||||
}
|
||||
// Read and parse the file
|
||||
let source = match fs::read_to_string(test_file) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
println!("ERROR: {}", e);
|
||||
failed += 1;
|
||||
println!(" {} ... ERROR: {}", path_str, e);
|
||||
total_failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let program = match Parser::parse_source(&source) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
println!(" {} ... PARSE ERROR: {}", path_str, e);
|
||||
total_failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Type check
|
||||
let mut checker = typechecker::TypeChecker::new();
|
||||
if let Err(errors) = checker.check_program(&program) {
|
||||
println!(" {} ... TYPE ERROR", path_str);
|
||||
for err in errors {
|
||||
eprintln!(" {}", err);
|
||||
}
|
||||
total_failed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find test functions (functions starting with test_)
|
||||
let test_funcs: Vec<_> = program.declarations.iter().filter_map(|d| {
|
||||
if let ast::Declaration::Function(f) = d {
|
||||
if f.name.name.starts_with("test_") {
|
||||
return Some(f.name.name.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}).collect();
|
||||
|
||||
if test_funcs.is_empty() {
|
||||
// No test functions, run the whole file
|
||||
let mut interp = Interpreter::new();
|
||||
interp.reset_test_results();
|
||||
|
||||
match interp.run(&program) {
|
||||
Ok(_) => {
|
||||
let results = interp.get_test_results();
|
||||
if results.failed == 0 && results.passed == 0 {
|
||||
// No Test assertions, just check it runs
|
||||
println!(" {} ... OK (no assertions)", path_str);
|
||||
total_passed += 1;
|
||||
} else if results.failed == 0 {
|
||||
println!(" {} ... OK ({} assertions)", path_str, results.passed);
|
||||
total_passed += results.passed;
|
||||
} else {
|
||||
println!(" {} ... FAILED ({} passed, {} failed)", path_str, results.passed, results.failed);
|
||||
total_passed += results.passed;
|
||||
total_failed += results.failed;
|
||||
for failure in &results.failures {
|
||||
all_failures.push((path_str.clone(), "".to_string(), failure.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" {} ... RUNTIME ERROR: {}", path_str, e);
|
||||
total_failed += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Run individual test functions
|
||||
println!(" {}:", path_str);
|
||||
|
||||
for test_name in &test_funcs {
|
||||
let mut interp = Interpreter::new();
|
||||
interp.reset_test_results();
|
||||
|
||||
// First run the file to define all functions
|
||||
if let Err(e) = interp.run(&program) {
|
||||
println!(" {} ... ERROR: {}", test_name, e);
|
||||
total_failed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Call the test function
|
||||
let call_source = format!("let testResult = run {}() with {{}}", test_name);
|
||||
let call_program = match Parser::parse_source(&call_source) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
println!(" {} ... ERROR: {}", test_name, e);
|
||||
total_failed += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match interp.run(&call_program) {
|
||||
Ok(_) => {
|
||||
let results = interp.get_test_results();
|
||||
if results.failed == 0 {
|
||||
println!(" {} ... OK", test_name);
|
||||
total_passed += 1;
|
||||
} else {
|
||||
println!(" {} ... FAILED", test_name);
|
||||
total_failed += 1;
|
||||
for failure in &results.failures {
|
||||
all_failures.push((path_str.clone(), test_name.clone(), failure.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" {} ... ERROR: {}", test_name, e);
|
||||
total_failed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Results: {} passed, {} failed", passed, failed);
|
||||
// Print failure details
|
||||
if !all_failures.is_empty() {
|
||||
println!("\n--- Failures ---\n");
|
||||
for (file, test, failure) in &all_failures {
|
||||
if test.is_empty() {
|
||||
println!("{}:", file);
|
||||
} else {
|
||||
println!("{} - {}:", file, test);
|
||||
}
|
||||
println!(" {}", failure.message);
|
||||
if let Some(expected) = &failure.expected {
|
||||
println!(" Expected: {}", expected);
|
||||
}
|
||||
if let Some(actual) = &failure.actual {
|
||||
println!(" Actual: {}", actual);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
println!("Results: {} passed, {} failed", total_passed, total_failed);
|
||||
|
||||
if total_failed > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_test_files(dir: &str, pattern: Option<&str>, files: &mut Vec<std::path::PathBuf>) {
|
||||
use std::fs;
|
||||
|
||||
for entry in fs::read_dir(dir).into_iter().flatten().flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "lux").unwrap_or(false) {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with("test_") || name.ends_with("_test.lux") {
|
||||
if pattern.map(|p| name.contains(p)).unwrap_or(true) {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn watch_file(path: &str) {
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::path::Path;
|
||||
|
||||
Reference in New Issue
Block a user