use crate::error::{EvalError, EvalResult};
use crate::space::SpaceInstance;
use pepl_stdlib::Value;
use pepl_types::ast::*;
#[derive(Debug, Clone)]
pub struct TestResult {
pub description: String,
pub passed: bool,
pub error: Option<String>,
}
impl std::fmt::Display for TestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.passed {
write!(f, " ✓ {}", self.description)
} else {
write!(
f,
" ✗ {} — {}",
self.description,
self.error.as_deref().unwrap_or("unknown error")
)
}
}
}
#[derive(Debug)]
pub struct TestRunSummary {
pub results: Vec<TestResult>,
pub passed: usize,
pub failed: usize,
}
impl std::fmt::Display for TestRunSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for r in &self.results {
writeln!(f, "{r}")?;
}
writeln!(f, "\n{} passed, {} failed", self.passed, self.failed)
}
}
#[derive(Debug, Clone)]
pub struct MockResponse {
pub module: String,
pub function: String,
pub response: Value,
}
pub fn run_tests(program: &Program) -> EvalResult<TestRunSummary> {
let mut results = Vec::new();
for test_block in &program.tests {
for case in &test_block.cases {
let result = run_single_test(program, case)?;
results.push(result);
}
}
let passed = results.iter().filter(|r| r.passed).count();
let failed = results.iter().filter(|r| !r.passed).count();
Ok(TestRunSummary {
results,
passed,
failed,
})
}
fn run_single_test(program: &Program, case: &TestCase) -> EvalResult<TestResult> {
let mocks = resolve_mocks(program, case)?;
let mut instance = SpaceInstance::new(program)?;
if !mocks.is_empty() {
instance.set_mock_responses(mocks);
}
let exec_result = execute_test_body(&mut instance, &case.body, &program.space.body);
match exec_result {
Ok(()) => Ok(TestResult {
description: case.description.clone(),
passed: true,
error: None,
}),
Err(EvalError::AssertionFailed(msg)) => Ok(TestResult {
description: case.description.clone(),
passed: false,
error: Some(msg),
}),
Err(e) => Ok(TestResult {
description: case.description.clone(),
passed: false,
error: Some(format!("{e}")),
}),
}
}
fn resolve_mocks(program: &Program, case: &TestCase) -> EvalResult<Vec<MockResponse>> {
let mut mocks = Vec::new();
if let Some(with_responses) = &case.with_responses {
let mut temp_instance = SpaceInstance::new(program)?;
for mapping in &with_responses.mappings {
let response = temp_instance.eval_expr_public(&mapping.response)?;
mocks.push(MockResponse {
module: mapping.module.name.clone(),
function: mapping.function.name.clone(),
response,
});
}
}
Ok(mocks)
}
fn execute_test_body(
instance: &mut SpaceInstance,
body: &Block,
space_body: &SpaceBody,
) -> EvalResult<()> {
for stmt in &body.stmts {
execute_test_stmt(instance, stmt, space_body)?;
}
Ok(())
}
fn execute_test_stmt(
instance: &mut SpaceInstance,
stmt: &Stmt,
space_body: &SpaceBody,
) -> EvalResult<()> {
match stmt {
Stmt::Expr(expr_stmt) => {
execute_test_expr(instance, &expr_stmt.expr, space_body)?;
Ok(())
}
Stmt::Assert(assert) => {
let val = instance.eval_expr_public(&assert.condition)?;
if !val.is_truthy() {
let msg = assert
.message
.clone()
.unwrap_or_else(|| "assertion failed".into());
return Err(EvalError::AssertionFailed(msg));
}
Ok(())
}
Stmt::Let(binding) => {
let value = instance.eval_expr_public(&binding.value)?;
if let Some(name) = &binding.name {
instance.define_in_env(&name.name, value);
}
Ok(())
}
Stmt::If(if_expr) => {
let cond = instance.eval_expr_public(&if_expr.condition)?;
if cond.is_truthy() {
execute_test_body(instance, &if_expr.then_block, space_body)?;
} else if let Some(else_branch) = &if_expr.else_branch {
match else_branch {
ElseBranch::ElseIf(elif) => {
let cond = instance.eval_expr_public(&elif.condition)?;
if cond.is_truthy() {
execute_test_body(instance, &elif.then_block, space_body)?;
}
}
ElseBranch::Block(block) => {
execute_test_body(instance, block, space_body)?;
}
}
}
Ok(())
}
Stmt::For(for_expr) => {
let iterable = instance.eval_expr_public(&for_expr.iterable)?;
if let Value::List(items) = iterable {
for (i, item) in items.iter().enumerate() {
instance.push_scope();
instance.define_in_env(&for_expr.item.name, item.clone());
if let Some(idx) = &for_expr.index {
instance.define_in_env(&idx.name, Value::Number(i as f64));
}
execute_test_body(instance, &for_expr.body, space_body)?;
instance.pop_scope();
}
}
Ok(())
}
_ => {
instance.eval_stmt_public(stmt)?;
Ok(())
}
}
}
fn execute_test_expr(
instance: &mut SpaceInstance,
expr: &Expr,
space_body: &SpaceBody,
) -> EvalResult<Value> {
match &expr.kind {
ExprKind::Call { name, args } => {
let is_action = space_body.actions.iter().any(|a| a.name.name == name.name);
if is_action {
let mut arg_vals = Vec::new();
for arg in args {
arg_vals.push(instance.eval_expr_public(arg)?);
}
let result = instance.dispatch(&name.name, arg_vals)?;
if !result.committed {
if let Some(err) = result.invariant_error {
return Err(EvalError::InvariantViolation(err));
}
}
Ok(Value::Nil)
} else {
instance.eval_expr_public(expr)
}
}
_ => instance.eval_expr_public(expr),
}
}