1use crate::discovered_test::DiscoveredTest;
2use crate::failed_test_result::FailedTestResult;
3use crate::test_run_summary::TestRunSummary;
4use ocelot_ast::item_kind::ItemKind;
5use ocelot_base::compilation_context::CompilationContext;
6use ocelot_base::compilation_stage::CompilationStage;
7use ocelot_base::error::OcelotError;
8use ocelot_base::file_path::FilePath;
9use ocelot_base::render_source_diagnostics::render_source_diagnostics;
10use ocelot_base::result::OcelotResult;
11use ocelot_base::result::OptionExt;
12use ocelot_base::result::ResultExt;
13use ocelot_base::source_file::SourceFile;
14use ocelot_pal::pal::PalHandle;
15
16#[derive(Debug, Clone)]
17pub struct Engine {
18 pal: PalHandle,
19}
20
21impl Engine {
22 pub fn new(pal: PalHandle) -> Self {
23 Self { pal }
24 }
25
26 pub fn run_script(&self, path: impl Into<FilePath>) -> OcelotResult<()> {
27 let source_file = self.load_source_file(path.into())?;
28 let script = self.parse_script(&source_file)?;
29 ocelot_interpreter::interpret_script::interpret_script(&script, &*self.pal)?;
30 Ok(())
31 }
32
33 pub fn discover_tests(&self, path: impl Into<FilePath>) -> OcelotResult<Vec<DiscoveredTest>> {
34 let source_file = self.load_source_file(path.into())?;
35 let script = self.parse_script(&source_file)?;
36
37 Ok(script
38 .items
39 .into_iter()
40 .filter_map(|item| match item.kind {
41 ItemKind::Test(test_item) => {
42 Some(DiscoveredTest::new(test_item.name, test_item.span))
43 }
44 ItemKind::Statement(_) => None,
45 })
46 .collect())
47 }
48
49 pub fn run_test(&self, path: impl Into<FilePath>, test_name: &str) -> OcelotResult<()> {
50 let source_file = self.load_source_file(path.into())?;
51 let script = self.parse_script(&source_file)?;
52 let test_item = script
53 .items
54 .iter()
55 .find_map(|item| match &item.kind {
56 ItemKind::Test(test_item) if test_item.name == test_name => Some(test_item),
57 _ => None,
58 })
59 .context(format!("unknown test `{test_name}`"))?;
60
61 ocelot_interpreter::interpreter::Interpreter::new(&*self.pal)
62 .interpret_statements(&test_item.body)
63 .context(format!("test `{}` failed", test_item.name))?;
64
65 Ok(())
66 }
67
68 pub fn run_tests(&self, path: impl Into<FilePath>) -> OcelotResult<TestRunSummary> {
69 let source_file = self.load_source_file(path.into())?;
70 let script = self.parse_script(&source_file)?;
71 let interpreter = ocelot_interpreter::interpreter::Interpreter::new(&*self.pal);
72 let mut summary = TestRunSummary::new();
73
74 for item in &script.items {
75 let ItemKind::Test(test_item) = &item.kind else {
76 continue;
77 };
78
79 match interpreter.interpret_statements(&test_item.body) {
80 Ok(()) => summary.passed.push(test_item.name.clone()),
81 Err(error) => summary.failed.push(FailedTestResult::new(
82 test_item.name.clone(),
83 OcelotError::message(format!("test `{}` failed", test_item.name))
84 .with_source(error)
85 .to_test_string(),
86 )),
87 }
88 }
89
90 Ok(summary)
91 }
92
93 fn load_source_file(&self, path: FilePath) -> OcelotResult<SourceFile> {
94 let source = self.pal.read_file_to_string(&path)?;
95 Ok(SourceFile::new(path, source))
96 }
97
98 fn parse_script(&self, source_file: &SourceFile) -> OcelotResult<ocelot_ast::script::Script> {
99 let mut compilation_context = CompilationContext::default();
100 let script =
101 ocelot_parser::parse_script::parse_script(source_file, &mut compilation_context)?;
102
103 match script {
104 Some(script) => Ok(script),
105 None if compilation_context.has_errors() => Err(OcelotError::compilation_error(
106 CompilationStage::Parser,
107 )
108 .with_source(OcelotError::message(render_source_diagnostics(
109 &compilation_context.source_diagnostics.diagnostics,
110 )))),
111 None => Err(OcelotError::message(
112 "parser returned no script without reporting diagnostics",
113 )),
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::Engine;
121 use expect_test::expect;
122 use ocelot_base::compilation_stage::CompilationStage;
123 use ocelot_base::error::ErrorKind;
124 use ocelot_pal::pal::PalHandle;
125 use ocelot_pal::pal_mock::PalMock;
126
127 #[test]
128 fn run_script_reads_and_executes_a_file() {
129 let pal = PalMock::new();
130 pal.set_file("examples/hello_world.ocelot", "println(\"hello, world\");");
131
132 let engine = Engine::new(PalHandle::new(pal.clone()));
133
134 engine.run_script("examples/hello_world.ocelot").unwrap();
135
136 expect![[r#"
137 READ FILE: examples/hello_world.ocelot
138 PRINT: hello, world
139
140 "#]]
141 .assert_eq(&pal.get_effects());
142 assert_eq!(pal.take_printed_output(), "hello, world\n");
143 }
144
145 #[test]
146 fn run_script_ignores_test_items() {
147 let pal = PalMock::new();
148 pal.set_file(
149 "examples/mixed.ocelot",
150 "println(\"setup\"); test \"prints hello\" { println(\"hello\"); }",
151 );
152
153 let engine = Engine::new(PalHandle::new(pal.clone()));
154
155 engine.run_script("examples/mixed.ocelot").unwrap();
156
157 assert_eq!(pal.take_printed_output(), "setup\n");
158 }
159
160 #[test]
161 fn discover_tests_returns_names_in_source_order() {
162 let pal = PalMock::new();
163 pal.set_file(
164 "examples/tests.ocelot",
165 "test \"first\" { println(\"one\"); } test \"second\" { println(\"two\"); }",
166 );
167
168 let engine = Engine::new(PalHandle::new(pal));
169
170 let tests = engine.discover_tests("examples/tests.ocelot").unwrap();
171
172 assert_eq!(tests.len(), 2);
173 assert_eq!(tests[0].name, "first");
174 assert_eq!(tests[1].name, "second");
175 }
176
177 #[test]
178 fn run_test_executes_only_the_named_test() {
179 let pal = PalMock::new();
180 pal.set_file(
181 "examples/tests.ocelot",
182 "test \"first\" { println(\"one\"); } test \"second\" { println(\"two\"); }",
183 );
184
185 let engine = Engine::new(PalHandle::new(pal.clone()));
186
187 engine.run_test("examples/tests.ocelot", "second").unwrap();
188
189 assert_eq!(pal.take_printed_output(), "two\n");
190 }
191
192 #[test]
193 fn run_test_reports_the_failing_test_name() {
194 let pal = PalMock::new();
195 pal.set_file(
196 "examples/tests.ocelot",
197 "test \"broken\" { println(missing_value); }",
198 );
199
200 let engine = Engine::new(PalHandle::new(pal));
201
202 let error = engine
203 .run_test("examples/tests.ocelot", "broken")
204 .unwrap_err();
205
206 assert!(error.to_test_string().contains("test `broken` failed"));
207 }
208
209 #[test]
210 fn run_tests_reports_unknown_test_names() {
211 let pal = PalMock::new();
212 pal.set_file(
213 "examples/tests.ocelot",
214 "test \"first\" { println(\"one\"); }",
215 );
216
217 let engine = Engine::new(PalHandle::new(pal));
218
219 let error = engine
220 .run_test("examples/tests.ocelot", "missing")
221 .unwrap_err();
222
223 assert!(error.to_test_string().contains("unknown test `missing`"));
224 }
225
226 #[test]
227 fn run_script_tags_parser_failures_as_compilation_errors() {
228 let pal = PalMock::new();
229 pal.set_file("examples/broken.ocelot", "println();");
230
231 let engine = Engine::new(PalHandle::new(pal));
232
233 let error = engine.run_script("examples/broken.ocelot").unwrap_err();
234
235 assert!(matches!(
236 error.kind(),
237 ErrorKind::CompilationError(CompilationStage::Parser)
238 ));
239 assert!(
240 error
241 .to_test_string()
242 .contains("type error: `println` expects exactly one argument")
243 );
244 }
245
246 #[test]
247 fn run_tests_reports_passes_and_failures_for_all_tests() {
248 let pal = PalMock::new();
249 pal.set_file(
250 "examples/tests.ocelot",
251 "test \"first\" { println(\"one\"); } \
252 test \"broken\" { println(missing_value); } \
253 test \"third\" { println(\"three\"); }",
254 );
255
256 let engine = Engine::new(PalHandle::new(pal.clone()));
257
258 let summary = engine.run_tests("examples/tests.ocelot").unwrap();
259
260 assert_eq!(summary.passed, vec!["first", "third"]);
261 assert_eq!(summary.failed.len(), 1);
262 assert_eq!(summary.failed[0].name, "broken");
263 assert!(summary.failed[0].message.contains("test `broken` failed"));
264 assert!(
265 summary.failed[0]
266 .message
267 .contains("unresolved identifier `missing_value`")
268 );
269 assert_eq!(pal.take_printed_output(), "one\nthree\n");
270 }
271}