use crate::discovered_test::DiscoveredTest;
use crate::failed_test_result::FailedTestResult;
use crate::test_run_summary::TestRunSummary;
use ocelot_ast::item_kind::ItemKind;
use ocelot_base::compilation_context::CompilationContext;
use ocelot_base::compilation_stage::CompilationStage;
use ocelot_base::error::OcelotError;
use ocelot_base::file_path::FilePath;
use ocelot_base::render_source_diagnostics::render_source_diagnostics;
use ocelot_base::result::OcelotResult;
use ocelot_base::result::OptionExt;
use ocelot_base::result::ResultExt;
use ocelot_base::source_file::SourceFile;
use ocelot_pal::pal::PalHandle;
#[derive(Debug, Clone)]
pub struct Engine {
pal: PalHandle,
}
impl Engine {
pub fn new(pal: PalHandle) -> Self {
Self { pal }
}
pub fn run_script(&self, path: impl Into<FilePath>) -> OcelotResult<()> {
let source_file = self.load_source_file(path.into())?;
let script = self.parse_script(&source_file)?;
ocelot_interpreter::interpret_script::interpret_script(&script, &*self.pal)?;
Ok(())
}
pub fn discover_tests(&self, path: impl Into<FilePath>) -> OcelotResult<Vec<DiscoveredTest>> {
let source_file = self.load_source_file(path.into())?;
let script = self.parse_script(&source_file)?;
Ok(script
.items
.into_iter()
.filter_map(|item| match item.kind {
ItemKind::Test(test_item) => {
Some(DiscoveredTest::new(test_item.name, test_item.span))
}
ItemKind::Statement(_) => None,
})
.collect())
}
pub fn run_test(&self, path: impl Into<FilePath>, test_name: &str) -> OcelotResult<()> {
let source_file = self.load_source_file(path.into())?;
let script = self.parse_script(&source_file)?;
let test_item = script
.items
.iter()
.find_map(|item| match &item.kind {
ItemKind::Test(test_item) if test_item.name == test_name => Some(test_item),
_ => None,
})
.context(format!("unknown test `{test_name}`"))?;
ocelot_interpreter::interpreter::Interpreter::new(&*self.pal)
.interpret_statements(&test_item.body)
.context(format!("test `{}` failed", test_item.name))?;
Ok(())
}
pub fn run_tests(&self, path: impl Into<FilePath>) -> OcelotResult<TestRunSummary> {
let source_file = self.load_source_file(path.into())?;
let script = self.parse_script(&source_file)?;
let interpreter = ocelot_interpreter::interpreter::Interpreter::new(&*self.pal);
let mut summary = TestRunSummary::new();
for item in &script.items {
let ItemKind::Test(test_item) = &item.kind else {
continue;
};
match interpreter.interpret_statements(&test_item.body) {
Ok(()) => summary.passed.push(test_item.name.clone()),
Err(error) => summary.failed.push(FailedTestResult::new(
test_item.name.clone(),
OcelotError::message(format!("test `{}` failed", test_item.name))
.with_source(error)
.to_test_string(),
)),
}
}
Ok(summary)
}
fn load_source_file(&self, path: FilePath) -> OcelotResult<SourceFile> {
let source = self.pal.read_file_to_string(&path)?;
Ok(SourceFile::new(path, source))
}
fn parse_script(&self, source_file: &SourceFile) -> OcelotResult<ocelot_ast::script::Script> {
let mut compilation_context = CompilationContext::default();
let script =
ocelot_parser::parse_script::parse_script(source_file, &mut compilation_context)?;
match script {
Some(script) => Ok(script),
None if compilation_context.has_errors() => Err(OcelotError::compilation_error(
CompilationStage::Parser,
)
.with_source(OcelotError::message(render_source_diagnostics(
&compilation_context.source_diagnostics.diagnostics,
)))),
None => Err(OcelotError::message(
"parser returned no script without reporting diagnostics",
)),
}
}
}
#[cfg(test)]
mod tests {
use super::Engine;
use expect_test::expect;
use ocelot_base::compilation_stage::CompilationStage;
use ocelot_base::error::ErrorKind;
use ocelot_pal::pal::PalHandle;
use ocelot_pal::pal_mock::PalMock;
#[test]
fn run_script_reads_and_executes_a_file() {
let pal = PalMock::new();
pal.set_file("examples/hello_world.ocelot", "println(\"hello, world\");");
let engine = Engine::new(PalHandle::new(pal.clone()));
engine.run_script("examples/hello_world.ocelot").unwrap();
expect![[r#"
READ FILE: examples/hello_world.ocelot
PRINT: hello, world
"#]]
.assert_eq(&pal.get_effects());
assert_eq!(pal.take_printed_output(), "hello, world\n");
}
#[test]
fn run_script_ignores_test_items() {
let pal = PalMock::new();
pal.set_file(
"examples/mixed.ocelot",
"println(\"setup\"); test \"prints hello\" { println(\"hello\"); }",
);
let engine = Engine::new(PalHandle::new(pal.clone()));
engine.run_script("examples/mixed.ocelot").unwrap();
assert_eq!(pal.take_printed_output(), "setup\n");
}
#[test]
fn discover_tests_returns_names_in_source_order() {
let pal = PalMock::new();
pal.set_file(
"examples/tests.ocelot",
"test \"first\" { println(\"one\"); } test \"second\" { println(\"two\"); }",
);
let engine = Engine::new(PalHandle::new(pal));
let tests = engine.discover_tests("examples/tests.ocelot").unwrap();
assert_eq!(tests.len(), 2);
assert_eq!(tests[0].name, "first");
assert_eq!(tests[1].name, "second");
}
#[test]
fn run_test_executes_only_the_named_test() {
let pal = PalMock::new();
pal.set_file(
"examples/tests.ocelot",
"test \"first\" { println(\"one\"); } test \"second\" { println(\"two\"); }",
);
let engine = Engine::new(PalHandle::new(pal.clone()));
engine.run_test("examples/tests.ocelot", "second").unwrap();
assert_eq!(pal.take_printed_output(), "two\n");
}
#[test]
fn run_test_reports_the_failing_test_name() {
let pal = PalMock::new();
pal.set_file(
"examples/tests.ocelot",
"test \"broken\" { println(missing_value); }",
);
let engine = Engine::new(PalHandle::new(pal));
let error = engine
.run_test("examples/tests.ocelot", "broken")
.unwrap_err();
assert!(error.to_test_string().contains("test `broken` failed"));
}
#[test]
fn run_tests_reports_unknown_test_names() {
let pal = PalMock::new();
pal.set_file(
"examples/tests.ocelot",
"test \"first\" { println(\"one\"); }",
);
let engine = Engine::new(PalHandle::new(pal));
let error = engine
.run_test("examples/tests.ocelot", "missing")
.unwrap_err();
assert!(error.to_test_string().contains("unknown test `missing`"));
}
#[test]
fn run_script_tags_parser_failures_as_compilation_errors() {
let pal = PalMock::new();
pal.set_file("examples/broken.ocelot", "println();");
let engine = Engine::new(PalHandle::new(pal));
let error = engine.run_script("examples/broken.ocelot").unwrap_err();
assert!(matches!(
error.kind(),
ErrorKind::CompilationError(CompilationStage::Parser)
));
assert!(
error
.to_test_string()
.contains("type error: `println` expects exactly one argument")
);
}
#[test]
fn run_tests_reports_passes_and_failures_for_all_tests() {
let pal = PalMock::new();
pal.set_file(
"examples/tests.ocelot",
"test \"first\" { println(\"one\"); } \
test \"broken\" { println(missing_value); } \
test \"third\" { println(\"three\"); }",
);
let engine = Engine::new(PalHandle::new(pal.clone()));
let summary = engine.run_tests("examples/tests.ocelot").unwrap();
assert_eq!(summary.passed, vec!["first", "third"]);
assert_eq!(summary.failed.len(), 1);
assert_eq!(summary.failed[0].name, "broken");
assert!(summary.failed[0].message.contains("test `broken` failed"));
assert!(
summary.failed[0]
.message
.contains("unresolved identifier `missing_value`")
);
assert_eq!(pal.take_printed_output(), "one\nthree\n");
}
}