Refactor: Move tester module to utils

This commit is contained in:
Michael Schimmel
2026-02-18 18:09:33 +01:00
parent b6d1d41c8b
commit 193414c1da
6 changed files with 12 additions and 9 deletions
+1
View File
@@ -0,0 +1 @@
pub mod tester;
+172
View File
@@ -0,0 +1,172 @@
use crate::ast::environment::Environment;
use std::fs;
use std::time::{Duration, Instant};
use regex::Regex;
pub struct TestResult {
pub name: String,
pub success: bool,
pub message: String,
}
pub struct BenchmarkResult {
pub name: String,
pub median: Duration,
pub baseline: Option<Duration>,
pub diff_pct: Option<f64>,
pub status: String,
}
pub fn run_functional_tests() -> Vec<TestResult> {
let mut results = Vec::new();
let entries = fs::read_dir("examples").unwrap();
let output_re = Regex::new(r";; Output: (.*)").unwrap();
for entry in entries.filter_map(|e| e.ok()) {
let env = Environment::new(); // Fresh environment per test file
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "myc") {
let content = fs::read_to_string(&path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let expected_output = output_re
.captures(&content)
.map(|m| m.get(1).unwrap().as_str().trim().to_string());
if let Some(expected) = expected_output {
match env.run_script(&content) {
Ok(val) => {
let val_str = format!("{}", val);
if val_str == expected {
results.push(TestResult { name, success: true, message: format!("OK: {}", val_str) });
} else {
results.push(TestResult { name, success: false, message: format!("Expected {}, got {}", expected, val_str) });
}
}
Err(e) => results.push(TestResult { name, success: false, message: format!("Error: {}", e) }),
}
}
}
}
results
}
pub fn run_benchmarks(update: bool) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
let entries = fs::read_dir("examples").unwrap();
let env = Environment::new();
let is_release = !cfg!(debug_assertions);
let baseline_re = Regex::new(r";; Benchmark: ([\d\.]+\w+)").unwrap();
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "myc") {
let content = fs::read_to_string(&path).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let baseline_match = baseline_re.captures(&content);
// Prepare: Compile and Link once outside the measurement loop
let linked_node = match env.compile(&content).map(|c| env.link(c)) {
Ok(node) => node,
Err(_) => continue, // Skip scripts that don't compile
};
// Measure: Only the VM execution
let mut runs = Vec::new();
for _ in 0..100 {
let start = Instant::now();
let _ = env.run(&linked_node);
runs.push(start.elapsed());
}
runs.sort();
let median = runs[runs.len() / 2];
if update {
let new_val = format_duration(median);
let updated_content = if let Some(m) = baseline_match {
content.replace(m.get(0).unwrap().as_str(), &format!(";; Benchmark: {}", new_val))
} else {
format!(";; Benchmark: {}
{}", new_val, content)
};
fs::write(&path, updated_content).unwrap();
results.push(BenchmarkResult { name, median, baseline: None, diff_pct: None, status: format!("UPDATED: {}", new_val) });
} else if let Some(m) = baseline_match {
let baseline_str = m.get(1).unwrap().as_str();
let baseline = parse_duration(baseline_str).unwrap();
let diff = (median.as_nanos() as f64 / baseline.as_nanos() as f64) - 1.0;
let threshold = if is_release { 0.10 } else { 0.25 };
let status = if median > baseline && diff > threshold { "FAILED" } else { "OK" };
results.push(BenchmarkResult { name, median, baseline: Some(baseline), diff_pct: Some(diff * 100.0), status: status.to_string() });
} else {
results.push(BenchmarkResult { name, median, baseline: None, diff_pct: None, status: "MISSING BASELINE".to_string() });
}
}
}
results
}
fn format_duration(d: Duration) -> String {
if d.as_nanos() < 1000 { format!("{}ns", d.as_nanos()) }
else if d.as_micros() < 1000 { format!("{:.1}us", d.as_nanos() as f64 / 1000.0) }
else if d.as_millis() < 1000 { format!("{:.1}ms", d.as_micros() as f64 / 1000.0) }
else { format!("{:.1}s", d.as_millis() as f64 / 1000.0) }
}
fn parse_duration(s: &str) -> Option<Duration> {
use std::sync::OnceLock;
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| Regex::new(r"([\d\.]+)(\w+)").unwrap());
let caps = re.captures(s)?;
let val: f64 = caps[1].parse().ok()?;
let unit = &caps[2];
match unit {
"ns" => Some(Duration::from_nanos(val as u64)),
"us" => Some(Duration::from_nanos((val * 1000.0) as u64)),
"ms" => Some(Duration::from_nanos((val * 1_000_000.0) as u64)),
"s" => Some(Duration::from_nanos((val * 1_000_000_000.0) as u64)),
_ => None,
}
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(not(debug_assertions))]
fn benchmark_regression_test() {
use super::*;
let mut success = false;
let mut last_failures = String::new();
for _ in 0..5 {
let results = run_benchmarks(false);
let failures: Vec<_> = results.iter()
.filter(|r| r.status == "FAILED")
.collect();
if failures.is_empty() {
success = true;
break;
}
last_failures = failures.iter()
.map(|r| format!("{}: Median {:?}, Baseline {:?}, Diff {:.2}%",
r.name, r.median, r.baseline.unwrap_or_default(), r.diff_pct.unwrap_or(0.0)))
.collect::<Vec<_>>()
.join("
");
// Give the system a tiny bit of breath between retries
std::thread::sleep(std::time::Duration::from_millis(50));
}
if !success {
panic!("Performance regression detected after 5 attempts:
{}", last_failures);
}
}
}