1use crate::error::{EvalError, EvalResult};
8use crate::space::SpaceInstance;
9use pepl_stdlib::Value;
10use pepl_types::ast::*;
11
12#[derive(Debug, Clone)]
14pub struct TestResult {
15 pub description: String,
17 pub passed: bool,
19 pub error: Option<String>,
21}
22
23impl std::fmt::Display for TestResult {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 if self.passed {
26 write!(f, " ✓ {}", self.description)
27 } else {
28 write!(
29 f,
30 " ✗ {} — {}",
31 self.description,
32 self.error.as_deref().unwrap_or("unknown error")
33 )
34 }
35 }
36}
37
38#[derive(Debug)]
40pub struct TestRunSummary {
41 pub results: Vec<TestResult>,
42 pub passed: usize,
43 pub failed: usize,
44}
45
46impl std::fmt::Display for TestRunSummary {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 for r in &self.results {
49 writeln!(f, "{r}")?;
50 }
51 writeln!(f, "\n{} passed, {} failed", self.passed, self.failed)
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct MockResponse {
58 pub module: String,
59 pub function: String,
60 pub response: Value,
61}
62
63pub fn run_tests(program: &Program) -> EvalResult<TestRunSummary> {
68 let mut results = Vec::new();
69
70 for test_block in &program.tests {
71 for case in &test_block.cases {
72 let result = run_single_test(program, case)?;
73 results.push(result);
74 }
75 }
76
77 let passed = results.iter().filter(|r| r.passed).count();
78 let failed = results.iter().filter(|r| !r.passed).count();
79
80 Ok(TestRunSummary {
81 results,
82 passed,
83 failed,
84 })
85}
86
87fn run_single_test(program: &Program, case: &TestCase) -> EvalResult<TestResult> {
89 let mocks = resolve_mocks(program, case)?;
91
92 let mut instance = SpaceInstance::new(program)?;
94
95 if !mocks.is_empty() {
97 instance.set_mock_responses(mocks);
98 }
99
100 let exec_result = execute_test_body(&mut instance, &case.body, &program.space.body);
102
103 match exec_result {
104 Ok(()) => Ok(TestResult {
105 description: case.description.clone(),
106 passed: true,
107 error: None,
108 }),
109 Err(EvalError::AssertionFailed(msg)) => Ok(TestResult {
110 description: case.description.clone(),
111 passed: false,
112 error: Some(msg),
113 }),
114 Err(e) => Ok(TestResult {
115 description: case.description.clone(),
116 passed: false,
117 error: Some(format!("{e}")),
118 }),
119 }
120}
121
122fn resolve_mocks(program: &Program, case: &TestCase) -> EvalResult<Vec<MockResponse>> {
124 let mut mocks = Vec::new();
125
126 if let Some(with_responses) = &case.with_responses {
127 let mut temp_instance = SpaceInstance::new(program)?;
129
130 for mapping in &with_responses.mappings {
131 let response = temp_instance.eval_expr_public(&mapping.response)?;
132 mocks.push(MockResponse {
133 module: mapping.module.name.clone(),
134 function: mapping.function.name.clone(),
135 response,
136 });
137 }
138 }
139
140 Ok(mocks)
141}
142
143fn execute_test_body(
151 instance: &mut SpaceInstance,
152 body: &Block,
153 space_body: &SpaceBody,
154) -> EvalResult<()> {
155 for stmt in &body.stmts {
156 execute_test_stmt(instance, stmt, space_body)?;
157 }
158 Ok(())
159}
160
161fn execute_test_stmt(
163 instance: &mut SpaceInstance,
164 stmt: &Stmt,
165 space_body: &SpaceBody,
166) -> EvalResult<()> {
167 match stmt {
168 Stmt::Expr(expr_stmt) => {
169 execute_test_expr(instance, &expr_stmt.expr, space_body)?;
170 Ok(())
171 }
172 Stmt::Assert(assert) => {
173 let val = instance.eval_expr_public(&assert.condition)?;
175 if !val.is_truthy() {
176 let msg = assert
177 .message
178 .clone()
179 .unwrap_or_else(|| "assertion failed".into());
180 return Err(EvalError::AssertionFailed(msg));
181 }
182 Ok(())
183 }
184 Stmt::Let(binding) => {
185 let value = instance.eval_expr_public(&binding.value)?;
186 if let Some(name) = &binding.name {
187 instance.define_in_env(&name.name, value);
188 }
189 Ok(())
190 }
191 Stmt::If(if_expr) => {
192 let cond = instance.eval_expr_public(&if_expr.condition)?;
193 if cond.is_truthy() {
194 execute_test_body(instance, &if_expr.then_block, space_body)?;
195 } else if let Some(else_branch) = &if_expr.else_branch {
196 match else_branch {
197 ElseBranch::ElseIf(elif) => {
198 let cond = instance.eval_expr_public(&elif.condition)?;
199 if cond.is_truthy() {
200 execute_test_body(instance, &elif.then_block, space_body)?;
201 }
202 }
203 ElseBranch::Block(block) => {
204 execute_test_body(instance, block, space_body)?;
205 }
206 }
207 }
208 Ok(())
209 }
210 Stmt::For(for_expr) => {
211 let iterable = instance.eval_expr_public(&for_expr.iterable)?;
212 if let Value::List(items) = iterable {
213 for (i, item) in items.iter().enumerate() {
214 instance.push_scope();
215 instance.define_in_env(&for_expr.item.name, item.clone());
216 if let Some(idx) = &for_expr.index {
217 instance.define_in_env(&idx.name, Value::Number(i as f64));
218 }
219 execute_test_body(instance, &for_expr.body, space_body)?;
220 instance.pop_scope();
221 }
222 }
223 Ok(())
224 }
225 _ => {
226 instance.eval_stmt_public(stmt)?;
228 Ok(())
229 }
230 }
231}
232
233fn execute_test_expr(
237 instance: &mut SpaceInstance,
238 expr: &Expr,
239 space_body: &SpaceBody,
240) -> EvalResult<Value> {
241 match &expr.kind {
242 ExprKind::Call { name, args } => {
243 let is_action = space_body.actions.iter().any(|a| a.name.name == name.name);
245
246 if is_action {
247 let mut arg_vals = Vec::new();
248 for arg in args {
249 arg_vals.push(instance.eval_expr_public(arg)?);
250 }
251 let result = instance.dispatch(&name.name, arg_vals)?;
252 if !result.committed {
253 if let Some(err) = result.invariant_error {
254 return Err(EvalError::InvariantViolation(err));
255 }
256 }
257 Ok(Value::Nil)
258 } else {
259 instance.eval_expr_public(expr)
260 }
261 }
262 _ => instance.eval_expr_public(expr),
263 }
264}