diff --git a/Cargo.lock b/Cargo.lock index 9e5ef450..663f2c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2367,15 +2367,15 @@ dependencies = [ name = "moorc" version = "0.9.0-alpha" dependencies = [ - "bincode", - "bytes", "clap", "clap_derive", "color-eyre", "eyre", + "moor-compiler", "moor-daemon", "moor-db", "moor-kernel", + "moor-moot", "moor-values", "semver", "tempfile", diff --git a/crates/compiler/src/objdef.rs b/crates/compiler/src/objdef.rs index 498e1885..1cb9517a 100644 --- a/crates/compiler/src/objdef.rs +++ b/crates/compiler/src/objdef.rs @@ -98,7 +98,7 @@ pub enum ObjDefParseError { BadAttributeType(VarType), } -fn parse_boolean_literal(pair: pest::iterators::Pair) -> Result { +fn parse_boolean_literal(pair: Pair) -> Result { let str = pair.as_str(); match str.to_lowercase().as_str() { "true" => Ok(true), @@ -110,7 +110,7 @@ fn parse_boolean_literal(pair: pest::iterators::Pair) -> Result, + pairs: Pairs, ) -> Result { let mut list = vec![]; for pair in pairs { @@ -122,7 +122,7 @@ fn parse_literal_list( fn parse_literal_map( context: &mut ObjFileContext, - pairs: pest::iterators::Pairs, + pairs: Pairs, ) -> Result { let mut elements = vec![]; for r in pairs { @@ -139,10 +139,7 @@ fn parse_literal_map( Ok(v_map(&pairs)) } -fn parse_literal( - context: &mut ObjFileContext, - pair: pest::iterators::Pair, -) -> Result { +fn parse_literal(context: &mut ObjFileContext, pair: Pair) -> Result { match pair.as_rule() { Rule::atom => { let pair = pair.into_inner().next().unwrap(); @@ -224,7 +221,7 @@ fn parse_literal( } } -fn parse_object_literal(pair: pest::iterators::Pair) -> Result { +fn parse_object_literal(pair: Pair) -> Result { match pair.as_rule() { Rule::object => { let ostr = &pair.as_str()[1..]; @@ -238,15 +235,15 @@ fn parse_object_literal(pair: pest::iterators::Pair) -> Result) -> Result { +fn parse_string_literal(pair: Pair) -> Result { let string = pair.as_str(); - let parsed = unquote_str(string).map_err(ObjDefParseError::VerbCompileError)?; + let parsed = unquote_str(string).map_err(VerbCompileError)?; Ok(parsed) } fn parse_literal_atom( context: &mut ObjFileContext, - pair: pest::iterators::Pair, + pair: Pair, ) -> Result { match pair.as_rule() { Rule::object => { @@ -262,7 +259,7 @@ fn parse_literal_atom( pair.as_str() )) }) - .map_err(ObjDefParseError::VerbCompileError)?, + .map_err(VerbCompileError)?, )), Rule::float => Ok(v_float( pair.as_str() @@ -273,7 +270,7 @@ fn parse_literal_atom( pair.as_str() )) }) - .map_err(ObjDefParseError::VerbCompileError)?, + .map_err(VerbCompileError)?, )), Rule::string => { let str = parse_string_literal(pair)?; @@ -329,15 +326,13 @@ pub fn compile_object_definitions( LineColLocation::Span(begin, end) => (begin, Some(end)), }; - return Err(ObjDefParseError::VerbCompileError( - CompileError::ParseError { - line, - column, - end_line_col, - context: e.line().to_string(), - message: e.variant.message().to_string(), - }, - )); + return Err(VerbCompileError(CompileError::ParseError { + line, + column, + end_line_col, + context: e.line().to_string(), + message: e.variant.message().to_string(), + })); } }; @@ -724,8 +719,7 @@ fn parse_verb_decl( let program = match verb_body.as_rule() { Rule::verb_statements => { let inner = verb_body.into_inner(); - compile_tree(inner, compile_options.clone()) - .map_err(ObjDefParseError::VerbCompileError)? + compile_tree(inner, compile_options.clone()).map_err(VerbCompileError)? } _ => { panic!("Expected verb body, got {:?}", verb_body); diff --git a/crates/kernel/testsuite/moot_suite.rs b/crates/kernel/testsuite/moot_suite.rs index cc396b58..b400efae 100644 --- a/crates/kernel/testsuite/moot_suite.rs +++ b/crates/kernel/testsuite/moot_suite.rs @@ -33,7 +33,7 @@ use moor_kernel::{ }, SchedulerClient, }; -use moor_moot::{execute_moot_test, MootRunner}; +use moor_moot::{execute_moot_test, MootOptions, MootRunner}; use moor_values::{v_none, Obj, Var}; mod common; @@ -111,7 +111,7 @@ impl MootRunner for SchedulerMootRunner { fn test_with_db(path: &Path) { test(create_db(), path); } -test_each_file::test_each_path! { in "./crates/kernel/testsuite/moot" as txdb => test_with_db } +test_each_file::test_each_path! { in "./crates/kernel/testsuite/moot" as moot_run => test_with_db } struct NoopSessionFactory {} impl SessionFactory for NoopSessionFactory { @@ -143,8 +143,10 @@ fn test(db: Box, path: &Path) { .spawn(move || scheduler.run(session_factory.clone())) .expect("Failed to spawn scheduler"); + let options = MootOptions::default(); execute_moot_test( SchedulerMootRunner::new(scheduler_client.clone(), Arc::new(NoopClientSession::new())), + &options, path, || Ok(()), ); diff --git a/crates/telnet-host/tests/integration_test.rs b/crates/telnet-host/tests/integration_test.rs index d16cb522..b7caf42a 100644 --- a/crates/telnet-host/tests/integration_test.rs +++ b/crates/telnet-host/tests/integration_test.rs @@ -11,7 +11,7 @@ // this program. If not, see . // -use moor_moot::{telnet::ManagedChild, test_db_path}; +use moor_moot::{telnet::ManagedChild, test_db_path, MootOptions}; use serial_test::serial; use std::net::TcpListener; use std::{ @@ -144,8 +144,10 @@ fn test_moot_with_telnet_host>(moot_file: P) { telnet_host_clone.lock().unwrap().assert_running() }; + let moot_options = MootOptions::default(); execute_moot_test( TelnetMootRunner::new(port), + &moot_options, &PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/moot") .join(moot_file) diff --git a/crates/testing/moot/src/lib.rs b/crates/testing/moot/src/lib.rs index 027a47b6..70b879ea 100644 --- a/crates/testing/moot/src/lib.rs +++ b/crates/testing/moot/src/lib.rs @@ -35,7 +35,10 @@ pub const NONPROGRAMMER: Obj = Obj::mk_id(5); #[allow(dead_code)] static LOGGING_INIT: Once = Once::new(); #[allow(dead_code)] -fn init_logging() { +fn init_logging(options: &MootOptions) { + if !options.init_logging { + return; + } LOGGING_INIT.call_once(|| { let main_subscriber = tracing_subscriber::fmt() .compact() @@ -46,8 +49,7 @@ fn init_logging() { .with_max_level(tracing::Level::WARN) .with_test_writer() .finish(); - tracing::subscriber::set_global_default(main_subscriber) - .expect("Unable to configure logging"); + tracing::subscriber::set_global_default(main_subscriber).ok(); }); } /// Look up the path to Test.db from any crate under the `moor` workspace @@ -67,24 +69,69 @@ pub trait MootRunner { fn none(&self) -> Self::Value; } +pub struct MootOptions { + /// Whether logging needs to be initialized, or if the host process has already initialized + /// logging + init_logging: bool, + wizard_object: Obj, + programmer_object: Obj, + nonprogrammer_object: Obj, +} + +impl MootOptions { + pub fn default() -> Self { + Self { + init_logging: false, + wizard_object: WIZARD, + programmer_object: PROGRAMMER, + nonprogrammer_object: NONPROGRAMMER, + } + } + + pub fn new() -> Self { + Self::default() + } + + pub fn init_logging(mut self, yesno: bool) -> Self { + self.init_logging = yesno; + self + } + + pub fn wizard_object(mut self, wizard_object: Obj) -> Self { + self.wizard_object = wizard_object; + self + } + + pub fn programmer_object(mut self, programmer_object: Obj) -> Self { + self.programmer_object = programmer_object; + self + } + + pub fn nonprogrammer_object(mut self, nonprogrammer_object: Obj) -> Self { + self.nonprogrammer_object = nonprogrammer_object; + self + } +} + pub fn execute_moot_test eyre::Result<()>>( mut runner: R, + options: &MootOptions, path: &Path, validate_state: F, ) { - init_logging(); + init_logging(&options); eprintln!("Test definition: {}", path.display()); let test = std::fs::read_to_string(path) .wrap_err(format!("{}", path.display())) .unwrap(); - let mut player = WIZARD; + let mut player = options.wizard_object.clone(); for span in parser::parse(&test).context("parse").unwrap() { eprintln!("{:?}", span); match &span.expr { MootBlock::ChangePlayer(change) => { - player = handle_change_player(change.name) + player = handle_change_player(&options, change.name) .context("handle_change_player") .unwrap(); } @@ -104,11 +151,11 @@ pub fn execute_moot_test eyre::Result<()>>( } } -fn handle_change_player(name: &str) -> eyre::Result { +fn handle_change_player(options: &MootOptions, name: &str) -> eyre::Result { Ok(match name { - "wizard" => WIZARD, - "programmer" => PROGRAMMER, - "nonprogrammer" => NONPROGRAMMER, + "wizard" => options.wizard_object.clone(), + "programmer" => options.programmer_object.clone(), + "nonprogrammer" => options.nonprogrammer_object.clone(), _ => return Err(eyre!("Unknown player: {}", name)), }) } diff --git a/crates/testing/moot/tests/moot_lmoo.rs b/crates/testing/moot/tests/moot_lmoo.rs index bb1b2f1a..ce6a26e1 100644 --- a/crates/testing/moot/tests/moot_lmoo.rs +++ b/crates/testing/moot/tests/moot_lmoo.rs @@ -24,7 +24,9 @@ use std::{ sync::{Arc, Mutex}, }; -use moor_moot::{execute_moot_test, telnet::ManagedChild, telnet::TelnetMootRunner, test_db_path}; +use moor_moot::{ + execute_moot_test, telnet::ManagedChild, telnet::TelnetMootRunner, test_db_path, MootOptions, +}; fn moo_path() -> PathBuf { env::var("MOOT_MOO_PATH") @@ -67,7 +69,13 @@ fn test_moo(path: &Path) { let moo_clone = moo.clone(); let validate_state = move || moo_clone.lock().unwrap().assert_running(); - execute_moot_test(TelnetMootRunner::new(moo_port()), path, validate_state); + let moot_options = MootOptions::default(); + execute_moot_test( + TelnetMootRunner::new(moo_port()), + &moot_options, + path, + validate_state, + ); drop(moo); } diff --git a/tools/moorc/Cargo.toml b/tools/moorc/Cargo.toml index 67759ded..024e1f02 100644 --- a/tools/moorc/Cargo.toml +++ b/tools/moorc/Cargo.toml @@ -12,18 +12,18 @@ rust-version.workspace = true description = "A tool for importing, compiling, and exporting mooR cores without running the full daemon" [dependencies] +moor-compiler = { path = "../../crates/compiler" } moor-db = { path = "../../crates/db" } moor-kernel = { path = "../../crates/kernel" } moor-values = { path = "../../crates/common" } moor-daemon = { path = "../../crates/daemon" } +moor-moot = { path = "../../crates/testing/moot" } ## Command line arguments parsing. clap.workspace = true clap_derive.workspace = true # General. -bincode.workspace = true -bytes.workspace = true color-eyre.workspace = true eyre.workspace = true semver.workspace = true diff --git a/tools/moorc/src/main.rs b/tools/moorc/src/main.rs index 1c5729be..8be58341 100644 --- a/tools/moorc/src/main.rs +++ b/tools/moorc/src/main.rs @@ -11,19 +11,28 @@ // this program. If not, see . // +mod testrun; + +use crate::testrun::run_test; use bincommon::FeatureArgs; use clap::Parser; use clap_derive::Parser; use moor_db::{Database, DatabaseConfig, TxDB}; -use moor_kernel::config::{FeaturesConfig, TextdumpConfig}; +use moor_kernel::config::{Config, FeaturesConfig, TextdumpConfig}; use moor_kernel::objdef::{ collect_object_definitions, dump_object_definitions, ObjectDefinitionLoader, }; +use moor_kernel::tasks::scheduler::Scheduler; +use moor_kernel::tasks::sessions::NoopSystemControl; +use moor_kernel::tasks::NoopTasksDb; use moor_kernel::textdump::{make_textdump, textdump_load, EncodingMode, TextdumpWriter}; -use moor_values::build; +use moor_moot::MootOptions; +use moor_values::model::{PropFlag, WorldStateSource}; +use moor_values::{build, Obj, Symbol, SYSTEM_OBJECT}; use std::fs::File; use std::path::PathBuf; -use tracing::{debug, error, info, trace}; +use std::sync::Arc; +use tracing::{debug, error, info, trace, warn}; #[derive(Parser, Debug)] // requires `derive` feature pub struct Args { @@ -54,6 +63,30 @@ pub struct Args { #[command(flatten)] feature_args: Option, + #[clap( + long, + help = "Run the set of unit tests defined in the defined directory" + )] + test_directory: Option, + + #[clap( + long, + help = "The hardcoded object number to use for the wizard character in tests." + )] + test_wizard: Option, + + #[clap( + long, + help = "The hardcoded object number to use for the programmer character in tests." + )] + test_programmer: Option, + + #[clap( + long, + help = "The hardcoded object number to use for the non-programmer player character in tests." + )] + test_player: Option, + #[clap(long, help = "Enable debug logging")] debug: bool, } @@ -193,4 +226,78 @@ fn main() { info!(?dirdump_path, "Objdefdump written."); } + + if let Some(test_directory) = args.test_directory { + let tasks_db = Box::new(NoopTasksDb {}); + let moot_version = semver::Version::new(0, 1, 0); + let db = Box::new(database); + + let wizard = Obj::mk_id(args.test_wizard.expect("Must specify wizard object")); + let player = Obj::mk_id(args.test_player.expect("Must specify player object")); + let programmer = Obj::mk_id( + args.test_programmer + .expect("Must specify programmer object"), + ); + let moot_options = MootOptions::default() + .wizard_object(wizard.clone()) + .nonprogrammer_object(player.clone()) + .programmer_object(programmer) + .init_logging(false); + + { + // We need to create a scratch property on #0 that is used for tests to stick transient + // values in + let mut tx = db.new_world_state().unwrap(); + tx.define_property( + &wizard, + &SYSTEM_OBJECT, + &SYSTEM_OBJECT, + Symbol::mk("scratch"), + &player, + PropFlag::rw(), + None, + ) + .unwrap(); + tx.commit().unwrap(); + } + let scheduler = Scheduler::new( + moot_version, + db, + tasks_db, + Arc::new(Config::default()), + Arc::new(NoopSystemControl::default()), + ); + let scheduler_client = scheduler.client().unwrap(); + let session_factory = Arc::new(crate::testrun::NoopSessionFactory {}); + let scheduler_loop_jh = std::thread::Builder::new() + .name("moor-scheduler".to_string()) + .spawn(move || scheduler.run(session_factory.clone())) + .expect("Failed to spawn scheduler"); + + // Iterate all the .moot tests and run them in the context of the current database. + warn!("Running tests in {}", test_directory.display()); + for entry in std::fs::read_dir(test_directory).unwrap() { + let Ok(entry) = entry else { + continue; + }; + + let path = entry.path(); + let Some(extension) = path.extension() else { + continue; + }; + + if extension != "moot" { + continue; + } + + run_test(&moot_options, scheduler_client.clone(), &path); + } + + scheduler_client + .submit_shutdown("Test runs are done") + .expect("Failed to shut down scheduler"); + scheduler_loop_jh + .join() + .expect("Failed to join() scheduler"); + } } diff --git a/tools/moorc/src/testrun.rs b/tools/moorc/src/testrun.rs new file mode 100644 index 00000000..03df6e1d --- /dev/null +++ b/tools/moorc/src/testrun.rs @@ -0,0 +1,112 @@ +// Copyright (C) 2025 Ryan Daum This program is free +// software: you can redistribute it and/or modify it under the terms of the GNU +// General Public License as published by the Free Software Foundation, version +// 3. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . +// + +use eyre::Context; +use moor_compiler::to_literal; +use moor_kernel::tasks::scheduler_test_utils; +use moor_kernel::tasks::sessions::{NoopClientSession, Session, SessionError, SessionFactory}; +use moor_kernel::SchedulerClient; +use moor_moot::{execute_moot_test, MootOptions, MootRunner}; +use moor_values::{v_none, Obj, Var}; +use std::path::Path; +use std::sync::Arc; +// TODO: consolidate with what's in kernel/testsuite/moo_suite.rs? + +#[derive(Clone)] +struct SchedulerMootRunner { + scheduler: SchedulerClient, + session: Arc, + eval_result: Option, +} +impl SchedulerMootRunner { + fn new(scheduler: SchedulerClient, session: Arc) -> Self { + Self { + scheduler, + session, + eval_result: None, + } + } +} +impl MootRunner for SchedulerMootRunner { + type Value = Var; + + fn eval>(&mut self, player: &Obj, command: S) -> eyre::Result<()> { + let command = command.into(); + eprintln!("{player} >> ; {command}"); + self.eval_result = Some( + scheduler_test_utils::call_eval( + self.scheduler.clone(), + self.session.clone(), + player, + command.clone(), + ) + .wrap_err(format!( + "SchedulerMootRunner::eval({player}, {:?})", + command + ))?, + ); + Ok(()) + } + + fn command>(&mut self, player: &Obj, command: S) -> eyre::Result<()> { + let command: &str = command.as_ref(); + eprintln!("{player} >> ; {}", command); + self.eval_result = Some( + scheduler_test_utils::call_command( + self.scheduler.clone(), + self.session.clone(), + player, + command, + ) + .wrap_err(format!( + "SchedulerMootRunner::command({player}, {:?})", + command + ))?, + ); + Ok(()) + } + + fn read_line(&mut self, _player: &Obj) -> eyre::Result> { + unimplemented!("Not supported on SchedulerMootRunner"); + } + + fn read_eval_result(&mut self, player: &Obj) -> eyre::Result> { + Ok(self + .eval_result + .take() + .inspect(|var| eprintln!("{player} << {}", to_literal(var)))) + } + + fn none(&self) -> Var { + v_none() + } +} + +pub(crate) struct NoopSessionFactory {} +impl SessionFactory for NoopSessionFactory { + fn mk_background_session( + self: Arc, + _player: &Obj, + ) -> Result, SessionError> { + Ok(Arc::new(NoopClientSession::new())) + } +} + +pub(crate) fn run_test(options: &MootOptions, scheduler_client: SchedulerClient, path: &Path) { + execute_moot_test( + SchedulerMootRunner::new(scheduler_client.clone(), Arc::new(NoopClientSession::new())), + options, + path, + || Ok(()), + ); +}