From d4f74282a3157efb7d26f6f801332e165bd8c9e1 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 13 Dec 2024 10:31:01 -0500 Subject: [PATCH 01/13] Added breakpoints --- Cargo.toml | 1 + src/debug.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 113 insertions(+) create mode 100644 src/debug.rs diff --git a/Cargo.toml b/Cargo.toml index b6cd275..21cfcb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ctor = "0.2.8" once_cell = "1.20.2" clap = { version = "4.5.21", features = ["derive"] } assert_cmd = "2.0.16" +shlex = "1.3.0" [dev-dependencies] rand_distr = "0.4.3" diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..1d9b4de --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,111 @@ +use crate::Context; +use crate::ContextPeopleExt; +use clap::Command; +use std::io::Write; + +fn cli() -> Command { + // strip out usage + const PARSER_TEMPLATE: &str = "\ + {all-args} + "; + // strip out name/version + const APPLET_TEMPLATE: &str = "\ + {about-with-newline}\n\ + {usage-heading}\n {usage}\n\ + \n\ + {all-args}{after-help}\ + "; + + Command::new("repl") + .multicall(true) + .arg_required_else_help(true) + .subcommand_required(true) + .subcommand_value_name("DEBUGGER") + .subcommand_help_heading("DEBUGGER UTILS") + .help_template(PARSER_TEMPLATE) + .subcommand( + Command::new("population") + .about("Get the total number of people") + .help_template(APPLET_TEMPLATE), + ) + .subcommand( + Command::new("quit") + .alias("exit") + .about("Quit the debugger") + .help_template(APPLET_TEMPLATE), + ) +} + +fn readline() -> Result { + write!(std::io::stdout(), "$ ").map_err(|e| e.to_string())?; + std::io::stdout().flush().map_err(|e| e.to_string())?; + let mut buffer = String::new(); + std::io::stdin() + .read_line(&mut buffer) + .map_err(|e| e.to_string())?; + Ok(buffer) +} + +fn respond(line: &str, context: &Context) -> Result { + let args = shlex::split(line).ok_or("error: Invalid quoting")?; + let matches = cli() + .try_get_matches_from(args) + .map_err(|e| e.to_string())?; + match matches.subcommand() { + Some(("population", _matches)) => { + writeln!( + std::io::stdout(), + "The number of people is {}", + context.get_current_population() + ) + .map_err(|e| e.to_string())?; + std::io::stdout().flush().map_err(|e| e.to_string())?; + } + Some(("quit", _matches)) => { + write!(std::io::stdout(), "Exiting ...").map_err(|e| e.to_string())?; + std::io::stdout().flush().map_err(|e| e.to_string())?; + return Ok(true); + } + Some((name, _matches)) => unimplemented!("{name}"), + None => unreachable!("subcommand required"), + } + + Ok(false) +} + +fn breakpoint(context: &Context) -> Result<(), String> { + loop { + let line = readline()?; + let line = line.trim(); + if line.is_empty() { + continue; + } + + match respond(line, context) { + Ok(quit) => { + if quit { + break; + } + } + Err(err) => { + write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; + std::io::stdout().flush().map_err(|e| e.to_string())?; + } + } + } + + Ok(()) +} + +pub trait ContextDebugExt { + fn add_breakpoint(&mut self, t: f64); +} + +impl ContextDebugExt for Context { + fn add_breakpoint(&mut self, t: f64) { + self.add_plan(t, |context| { + println!("Debugging: t = {}", context.get_current_time()); + breakpoint(context).expect("Error in breakpoint"); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6f9663c..7180c65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,3 +51,4 @@ pub mod report; pub use report::{ConfigReportOptions, ContextReportExt, Report}; pub mod runner; pub use runner::{run_with_args, run_with_custom_args, BaseArgs}; +pub mod debug; From 1d01fd66ff6d982cd66c8a32295665c78945fece Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 13 Dec 2024 11:35:55 -0500 Subject: [PATCH 02/13] add cli option --- src/{debug.rs => debugger.rs} | 56 ++++++++++++++++++++++++----------- src/lib.rs | 4 ++- src/runner.rs | 15 +++++++++- 3 files changed, 55 insertions(+), 20 deletions(-) rename src/{debug.rs => debugger.rs} (60%) diff --git a/src/debug.rs b/src/debugger.rs similarity index 60% rename from src/debug.rs rename to src/debugger.rs index 1d9b4de..1684e1c 100644 --- a/src/debug.rs +++ b/src/debugger.rs @@ -4,12 +4,12 @@ use clap::Command; use std::io::Write; fn cli() -> Command { - // strip out usage - const PARSER_TEMPLATE: &str = "\ + // strip out "Usage: " in the default template + const MAIN_HELP_TEMPLATE: &str = "\ {all-args} "; // strip out name/version - const APPLET_TEMPLATE: &str = "\ + const COMMAND_TEMPLATE: &str = "\ {about-with-newline}\n\ {usage-heading}\n {usage}\n\ \n\ @@ -21,18 +21,24 @@ fn cli() -> Command { .arg_required_else_help(true) .subcommand_required(true) .subcommand_value_name("DEBUGGER") - .subcommand_help_heading("DEBUGGER UTILS") - .help_template(PARSER_TEMPLATE) + .subcommand_help_heading("IXA DEBUGGER") + .help_template(MAIN_HELP_TEMPLATE) .subcommand( Command::new("population") .about("Get the total number of people") - .help_template(APPLET_TEMPLATE), + .help_template(COMMAND_TEMPLATE), ) .subcommand( - Command::new("quit") + Command::new("step") + .about("Advance the simulation by 1.0 and break") + .help_template(COMMAND_TEMPLATE), + ) + .subcommand( + Command::new("continue") .alias("exit") - .about("Quit the debugger") - .help_template(APPLET_TEMPLATE), + .alias("quit") + .about("Continue the simulation and exit the debugger") + .help_template(COMMAND_TEMPLATE), ) } @@ -46,7 +52,11 @@ fn readline() -> Result { Ok(buffer) } -fn respond(line: &str, context: &Context) -> Result { +fn flush() -> Result<(), String> { + std::io::stdout().flush().map_err(|e| e.to_string()) +} + +fn respond(line: &str, context: &mut Context) -> Result { let args = shlex::split(line).ok_or("error: Invalid quoting")?; let matches = cli() .try_get_matches_from(args) @@ -59,11 +69,22 @@ fn respond(line: &str, context: &Context) -> Result { context.get_current_population() ) .map_err(|e| e.to_string())?; - std::io::stdout().flush().map_err(|e| e.to_string())?; + flush()?; } - Some(("quit", _matches)) => { - write!(std::io::stdout(), "Exiting ...").map_err(|e| e.to_string())?; - std::io::stdout().flush().map_err(|e| e.to_string())?; + Some(("step", _matches)) => { + let next_t = context.get_current_time() + 1.0; + context.add_breakpoint(next_t); + flush()?; + return Ok(true); + } + Some(("continue", _matches)) => { + writeln!( + std::io::stdout(), + "Continuing the simulation from t = {}", + context.get_current_time() + ) + .map_err(|e| e.to_string())?; + flush()?; return Ok(true); } Some((name, _matches)) => unimplemented!("{name}"), @@ -73,7 +94,7 @@ fn respond(line: &str, context: &Context) -> Result { Ok(false) } -fn breakpoint(context: &Context) -> Result<(), String> { +fn breakpoint(context: &mut Context) -> Result<(), String> { loop { let line = readline()?; let line = line.trim(); @@ -93,7 +114,6 @@ fn breakpoint(context: &Context) -> Result<(), String> { } } } - Ok(()) } @@ -104,8 +124,8 @@ pub trait ContextDebugExt { impl ContextDebugExt for Context { fn add_breakpoint(&mut self, t: f64) { self.add_plan(t, |context| { - println!("Debugging: t = {}", context.get_current_time()); - breakpoint(context).expect("Error in breakpoint"); + println!("Debugging simulation at t = {}", context.get_current_time()); + breakpoint(context).expect("Error in debugger"); }); } } diff --git a/src/lib.rs b/src/lib.rs index 7180c65..28ba27a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,8 @@ pub use random::{ContextRandomExt, RngId}; pub mod report; pub use report::{ConfigReportOptions, ContextReportExt, Report}; + pub mod runner; pub use runner::{run_with_args, run_with_custom_args, BaseArgs}; -pub mod debug; + +pub mod debugger; diff --git a/src/runner.rs b/src/runner.rs index 57360fe..5f9e1f6 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,10 +1,10 @@ use std::path::{Path, PathBuf}; -use crate::context::Context; use crate::error::IxaError; use crate::global_properties::ContextGlobalPropertiesExt; use crate::random::ContextRandomExt; use crate::report::ContextReportExt; +use crate::{context::Context, debugger::ContextDebugExt}; use clap::{Args, Command, FromArgMatches as _}; /// Default cli arguments for ixa runner @@ -21,6 +21,10 @@ pub struct BaseArgs { /// Optional path for report output #[arg(short, long, default_value = "")] pub output_dir: String, + + /// Set a breakpoint at a given time and start the debugger + #[arg(short, long)] + pub debugger: Option, } #[derive(Args)] @@ -104,6 +108,11 @@ where context.init_random(args.random_seed); + // If a breakpoint is provided, stop at that time + if let Some(t) = args.debugger { + context.add_breakpoint(t); + } + // Run the provided Fn setup_fn(&mut context, args, custom_args)?; @@ -155,6 +164,7 @@ mod tests { random_seed: 42, config: String::new(), output_dir: String::new(), + debugger: None, }; // Use a comparison context to verify the random seed was set @@ -183,6 +193,7 @@ mod tests { random_seed: 42, config: "tests/data/global_properties_runner.json".to_string(), output_dir: String::new(), + debugger: None, }; let result = run_with_args_internal(test_args, None, |ctx, _, _: Option<()>| { let p3 = ctx.get_global_property_value(RunnerProperty).unwrap(); @@ -198,6 +209,7 @@ mod tests { random_seed: 42, config: String::new(), output_dir: "data".to_string(), + debugger: None, }; let result = run_with_args_internal(test_args, None, |ctx, _, _: Option<()>| { let output_dir = &ctx.report_options().directory; @@ -213,6 +225,7 @@ mod tests { random_seed: 42, config: String::new(), output_dir: String::new(), + debugger: None, }; let custom = CustomArgs { field: 42 }; let result = run_with_args_internal(test_args, Some(custom), |_, _, c| { From 03ec2f5d5973cc5940a634f94a4bd1a2609a6c1a Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 13 Dec 2024 11:50:05 -0500 Subject: [PATCH 03/13] rename stuff --- src/debugger.rs | 60 ++++++++++++++++++++++++++++--------------------- src/runner.rs | 2 +- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 1684e1c..a05d391 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -73,7 +73,7 @@ fn respond(line: &str, context: &mut Context) -> Result { } Some(("step", _matches)) => { let next_t = context.get_current_time() + 1.0; - context.add_breakpoint(next_t); + context.schedule_breakpoint(next_t); flush()?; return Ok(true); } @@ -94,38 +94,46 @@ fn respond(line: &str, context: &mut Context) -> Result { Ok(false) } -fn breakpoint(context: &mut Context) -> Result<(), String> { - loop { - let line = readline()?; - let line = line.trim(); - if line.is_empty() { - continue; - } +pub trait ContextDebugExt { + /// Pause the simulation at the current time and start the debugger. + /// The debugger allows you to inspect the state of the simulation + /// + /// # Errors + /// Reading or writing to stdin/stdout, or some problem in the debugger + fn breakpoint(&mut self) -> Result<(), String>; - match respond(line, context) { - Ok(quit) => { - if quit { - break; - } + /// Schedule a breakpoint at a given time t + fn schedule_breakpoint(&mut self, t: f64); +} + +impl ContextDebugExt for Context { + fn breakpoint(&mut self) -> Result<(), String> { + println!("Debugging simulation at t = {}", self.get_current_time()); + loop { + let line = readline()?; + let line = line.trim(); + if line.is_empty() { + continue; } - Err(err) => { - write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; - std::io::stdout().flush().map_err(|e| e.to_string())?; + + match respond(line, self) { + Ok(quit) => { + if quit { + break; + } + } + Err(err) => { + write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; + flush()?; + } } } + Ok(()) } - Ok(()) -} -pub trait ContextDebugExt { - fn add_breakpoint(&mut self, t: f64); -} - -impl ContextDebugExt for Context { - fn add_breakpoint(&mut self, t: f64) { + fn schedule_breakpoint(&mut self, t: f64) { self.add_plan(t, |context| { - println!("Debugging simulation at t = {}", context.get_current_time()); - breakpoint(context).expect("Error in debugger"); + context.breakpoint().expect("Error in debugger"); }); } } diff --git a/src/runner.rs b/src/runner.rs index 5f9e1f6..32201b5 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -110,7 +110,7 @@ where // If a breakpoint is provided, stop at that time if let Some(t) = args.debugger { - context.add_breakpoint(t); + context.schedule_breakpoint(t); } // Run the provided Fn From d9a105660a15de297c0e67db24876c79781ff907 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Fri, 13 Dec 2024 18:03:55 -0500 Subject: [PATCH 04/13] tests wip --- Cargo.toml | 7 ++++- src/debugger.rs | 53 +++++++++++++++++++++++++--------- src/runner.rs | 9 +++--- tests/bin/runner_test_debug.rs | 12 ++++++++ 4 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 tests/bin/runner_test_debug.rs diff --git a/Cargo.toml b/Cargo.toml index 21cfcb3..d780807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,14 +22,19 @@ paste = "1.0.15" ctor = "0.2.8" once_cell = "1.20.2" clap = { version = "4.5.21", features = ["derive"] } -assert_cmd = "2.0.16" shlex = "1.3.0" [dev-dependencies] rand_distr = "0.4.3" tempfile = "3.3" ordered-float = "4.3.0" +predicates = "3.1.2" +assert_cmd = "2.0.16" [[bin]] name = "runner_test_custom_args" path = "tests/bin/runner_test_custom_args.rs" + +[[bin]] +name = "runner_test_debug" +path = "tests/bin/runner_test_debug.rs" diff --git a/src/debugger.rs b/src/debugger.rs index a05d391..9774fbd 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,5 +1,6 @@ use crate::Context; use crate::ContextPeopleExt; +use crate::IxaError; use clap::Command; use std::io::Write; @@ -28,11 +29,6 @@ fn cli() -> Command { .about("Get the total number of people") .help_template(COMMAND_TEMPLATE), ) - .subcommand( - Command::new("step") - .about("Advance the simulation by 1.0 and break") - .help_template(COMMAND_TEMPLATE), - ) .subcommand( Command::new("continue") .alias("exit") @@ -61,6 +57,7 @@ fn respond(line: &str, context: &mut Context) -> Result { let matches = cli() .try_get_matches_from(args) .map_err(|e| e.to_string())?; + match matches.subcommand() { Some(("population", _matches)) => { writeln!( @@ -71,12 +68,6 @@ fn respond(line: &str, context: &mut Context) -> Result { .map_err(|e| e.to_string())?; flush()?; } - Some(("step", _matches)) => { - let next_t = context.get_current_time() + 1.0; - context.schedule_breakpoint(next_t); - flush()?; - return Ok(true); - } Some(("continue", _matches)) => { writeln!( std::io::stdout(), @@ -100,14 +91,14 @@ pub trait ContextDebugExt { /// /// # Errors /// Reading or writing to stdin/stdout, or some problem in the debugger - fn breakpoint(&mut self) -> Result<(), String>; + fn breakpoint(&mut self) -> Result<(), IxaError>; /// Schedule a breakpoint at a given time t fn schedule_breakpoint(&mut self, t: f64); } impl ContextDebugExt for Context { - fn breakpoint(&mut self) -> Result<(), String> { + fn breakpoint(&mut self) -> Result<(), IxaError> { println!("Debugging simulation at t = {}", self.get_current_time()); loop { let line = readline()?; @@ -137,3 +128,39 @@ impl ContextDebugExt for Context { }); } } + +#[cfg(test)] +mod tests { + use assert_cmd::Command; + use predicates::str::contains; + + #[test] + fn test_cli_debugger_quits() { + let mut cmd = Command::cargo_bin("runner_test_debug").unwrap(); + let assert = cmd + .args(["--debugger", "1.0"]) + .write_stdin("continue\n") + .assert(); + + assert + .success() + .stdout(contains("Debugging simulation at t = 1")) + .stdout(contains("Continuing the simulation from t = 1")); + } + + #[test] + #[ignore] + fn test_cli_debugger_population() { + let assert = Command::cargo_bin("runner_test_debug") + .unwrap() + .args(["--debugger", "1.0"]) + .write_stdin("population\n") + .write_stdin("continue\n") + .assert(); + + assert + .success() + // This doesn't seem to work for some reason + .stdout(contains("The number of people is 3")); + } +} diff --git a/src/runner.rs b/src/runner.rs index 32201b5..a68ccd2 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -22,9 +22,9 @@ pub struct BaseArgs { #[arg(short, long, default_value = "")] pub output_dir: String, - /// Set a breakpoint at a given time and start the debugger + /// Set a breakpoint at a given time and start the debugger. Defaults to t=0.0 #[arg(short, long)] - pub debugger: Option, + pub debugger: Option>, } #[derive(Args)] @@ -110,7 +110,7 @@ where // If a breakpoint is provided, stop at that time if let Some(t) = args.debugger { - context.schedule_breakpoint(t); + context.schedule_breakpoint(t.unwrap_or(0.0)); } // Run the provided Fn @@ -125,7 +125,6 @@ where mod tests { use super::*; use crate::{define_global_property, define_rng}; - use assert_cmd::Command; use serde::Deserialize; #[derive(Args, Debug)] @@ -144,7 +143,7 @@ mod tests { fn test_cli_invocation_with_custom_args() { // Note this target is defined in the bin section of Cargo.toml // and the entry point is in tests/bin/runner_test_custom_args - Command::cargo_bin("runner_test_custom_args") + assert_cmd::Command::cargo_bin("runner_test_custom_args") .unwrap() .args(["--field", "42"]) .assert() diff --git a/tests/bin/runner_test_debug.rs b/tests/bin/runner_test_debug.rs new file mode 100644 index 0000000..d060d6f --- /dev/null +++ b/tests/bin/runner_test_debug.rs @@ -0,0 +1,12 @@ +use ixa::runner::run_with_args; +use ixa::ContextPeopleExt; +fn main() { + run_with_args(|context, _args, _| { + context.add_person(()).unwrap(); + context.add_person(()).unwrap(); + context.add_person(()).unwrap(); + + Ok(()) + }) + .unwrap(); +} From b532e66d6869f3e484fc2befaf21430c0e97aeaa Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Sat, 14 Dec 2024 13:34:08 -0500 Subject: [PATCH 05/13] restructured commands to impl trait --- src/debugger.rs | 239 +++++++++++++++++++++++++++++------------------- src/runner.rs | 2 +- 2 files changed, 146 insertions(+), 95 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 9774fbd..df004cf 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,41 +1,106 @@ use crate::Context; use crate::ContextPeopleExt; use crate::IxaError; -use clap::Command; +use clap::{ArgMatches, Command}; +use std::cell::RefCell; +use std::collections::HashMap; use std::io::Write; -fn cli() -> Command { - // strip out "Usage: " in the default template - const MAIN_HELP_TEMPLATE: &str = "\ - {all-args} - "; - // strip out name/version - const COMMAND_TEMPLATE: &str = "\ - {about-with-newline}\n\ - {usage-heading}\n {usage}\n\ - \n\ - {all-args}{after-help}\ - "; - - Command::new("repl") - .multicall(true) - .arg_required_else_help(true) - .subcommand_required(true) - .subcommand_value_name("DEBUGGER") - .subcommand_help_heading("IXA DEBUGGER") - .help_template(MAIN_HELP_TEMPLATE) - .subcommand( - Command::new("population") - .about("Get the total number of people") - .help_template(COMMAND_TEMPLATE), - ) - .subcommand( - Command::new("continue") - .alias("exit") - .alias("quit") - .about("Continue the simulation and exit the debugger") - .help_template(COMMAND_TEMPLATE), - ) +trait DebuggerCommand { + fn handle( + &self, + context: &mut Context, + cli: &DebuggerRepl, + matches: &ArgMatches, + ) -> Result; + fn about(&self) -> &'static str; +} + +struct DebuggerRepl { + commands: HashMap<&'static str, Box>, + output: RefCell>, +} + +fn flush() -> Result<(), String> { + std::io::stdout().flush().map_err(|e| e.to_string()) +} + +impl DebuggerRepl { + fn new() -> Self { + DebuggerRepl { + commands: HashMap::new(), + output: RefCell::new(Box::new(std::io::stdout())), + } + } + + fn register_command(&mut self, name: &'static str, handler: Box) { + self.commands.insert(name, handler); + } + + fn get_command(&self, name: &str) -> Option<&dyn DebuggerCommand> { + self.commands.get(name).map(|command| &**command) + } + + fn writeln(&self, formatted_string: &str) -> Result<(), String> { + writeln!(self.output.borrow_mut(), "{formatted_string}").map_err(|e| e.to_string())?; + flush()?; + Ok(()) + } + + fn build_cli(&self) -> Command { + let mut cli = Command::new("repl") + .multicall(true) + .arg_required_else_help(true) + .subcommand_required(true) + .subcommand_value_name("DEBUGGER") + .subcommand_help_heading("IXA DEBUGGER") + .help_template("{all-args}"); + + for (name, handler) in &self.commands { + cli = cli.subcommand(Command::new(*name).about(handler.about()).help_template( + "{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}{after-help}", + )); + } + + cli + } +} + +/// Returns the current population of the simulation +struct PopulationCommand; +impl DebuggerCommand for PopulationCommand { + fn about(&self) -> &'static str { + "Get the total number of people" + } + fn handle( + &self, + context: &mut Context, + cli: &DebuggerRepl, + _matches: &ArgMatches, + ) -> Result { + cli.writeln(&format!("{}", context.get_current_population()))?; + Ok(false) + } +} + +/// Exits the debugger and continues the simulation +struct ContinueCommand; +impl DebuggerCommand for ContinueCommand { + fn about(&self) -> &'static str { + "Continue the simulation and exit the debugger" + } + fn handle( + &self, + context: &mut Context, + cli: &DebuggerRepl, + _matches: &ArgMatches, + ) -> Result { + cli.writeln(&format!( + "Continuing the simulation from t = {}", + context.get_current_time() + ))?; + Ok(true) + } } fn readline() -> Result { @@ -48,83 +113,69 @@ fn readline() -> Result { Ok(buffer) } -fn flush() -> Result<(), String> { - std::io::stdout().flush().map_err(|e| e.to_string()) -} - -fn respond(line: &str, context: &mut Context) -> Result { +fn setup_repl(line: &str, context: &mut Context) -> Result { let args = shlex::split(line).ok_or("error: Invalid quoting")?; - let matches = cli() + + let mut repl = DebuggerRepl::new(); + repl.register_command("population", Box::new(PopulationCommand)); + repl.register_command("continue", Box::new(ContinueCommand)); + let matches = repl + .build_cli() .try_get_matches_from(args) .map_err(|e| e.to_string())?; - match matches.subcommand() { - Some(("population", _matches)) => { - writeln!( - std::io::stdout(), - "The number of people is {}", - context.get_current_population() - ) - .map_err(|e| e.to_string())?; - flush()?; + if let Some((command, sub_matches)) = matches.subcommand() { + // If the provided command is known, run its handler + if let Some(handler) = repl.get_command(command) { + return handler.handle(context, &repl, sub_matches); } - Some(("continue", _matches)) => { - writeln!( - std::io::stdout(), - "Continuing the simulation from t = {}", - context.get_current_time() - ) - .map_err(|e| e.to_string())?; - flush()?; - return Ok(true); - } - Some((name, _matches)) => unimplemented!("{name}"), - None => unreachable!("subcommand required"), + // Unexpected command: print an error + return Err(format!("Unknown command: {command}")); } - Ok(false) -} - -pub trait ContextDebugExt { - /// Pause the simulation at the current time and start the debugger. - /// The debugger allows you to inspect the state of the simulation - /// - /// # Errors - /// Reading or writing to stdin/stdout, or some problem in the debugger - fn breakpoint(&mut self) -> Result<(), IxaError>; - - /// Schedule a breakpoint at a given time t - fn schedule_breakpoint(&mut self, t: f64); + unreachable!("subcommand required"); } -impl ContextDebugExt for Context { - fn breakpoint(&mut self) -> Result<(), IxaError> { - println!("Debugging simulation at t = {}", self.get_current_time()); - loop { - let line = readline()?; - let line = line.trim(); - if line.is_empty() { - continue; - } +/// Starts the debugger and pauses execution +fn start_debugger(context: &mut Context) -> Result<(), IxaError> { + println!("Debugging simulation at t = {}", context.get_current_time()); + loop { + let line = readline()?; + let line = line.trim(); + if line.is_empty() { + continue; + } - match respond(line, self) { - Ok(quit) => { - if quit { - break; - } - } - Err(err) => { - write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; - flush()?; + match setup_repl(line, context) { + Ok(quit) => { + if quit { + break; } } + Err(err) => { + write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; + flush()?; + } } - Ok(()) } + Ok(()) +} - fn schedule_breakpoint(&mut self, t: f64) { +pub trait ContextDebugExt { + /// Schedule the simulation to pause at time t and start the debugger. + /// This will give you a REPL which allows you to inspect the state of + /// the simulation (type help to see a list of commands) + /// + /// # Errors + /// Internal debugger errors e.g., reading or writing to stdin/stdout; + /// errors in Ixa are printed to stdout + fn schedule_debugger(&mut self, t: f64); +} + +impl ContextDebugExt for Context { + fn schedule_debugger(&mut self, t: f64) { self.add_plan(t, |context| { - context.breakpoint().expect("Error in debugger"); + start_debugger(context).expect("Error in debugger"); }); } } diff --git a/src/runner.rs b/src/runner.rs index a68ccd2..85bf90e 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -110,7 +110,7 @@ where // If a breakpoint is provided, stop at that time if let Some(t) = args.debugger { - context.schedule_breakpoint(t.unwrap_or(0.0)); + context.schedule_debugger(t.unwrap_or(0.0)); } // Run the provided Fn From 7c1db7186af17a5440516d99166d4520893a37b1 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Sat, 14 Dec 2024 13:38:21 -0500 Subject: [PATCH 06/13] comment --- src/debugger.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/debugger.rs b/src/debugger.rs index df004cf..7469246 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::io::Write; trait DebuggerCommand { + /// Handle the command and any inputs; returning true will exit the debugger fn handle( &self, context: &mut Context, From f240f543264f8d2fc4982cf98cee51324f5b36a0 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Mon, 16 Dec 2024 09:49:37 -0500 Subject: [PATCH 07/13] review comments --- src/debugger.rs | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 7469246..ee139bd 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -22,8 +22,11 @@ struct DebuggerRepl { output: RefCell>, } -fn flush() -> Result<(), String> { - std::io::stdout().flush().map_err(|e| e.to_string()) +fn flush() { + std::io::stdout() + .flush() + .map_err(|e| e.to_string()) + .expect("Error flushing stdout"); } impl DebuggerRepl { @@ -42,10 +45,9 @@ impl DebuggerRepl { self.commands.get(name).map(|command| &**command) } - fn writeln(&self, formatted_string: &str) -> Result<(), String> { - writeln!(self.output.borrow_mut(), "{formatted_string}").map_err(|e| e.to_string())?; - flush()?; - Ok(()) + fn writeln(&self, formatted_string: &str) { + let _ = writeln!(self.output.borrow_mut(), "{formatted_string}").map_err(|e| e.to_string()); + flush(); } fn build_cli(&self) -> Command { @@ -79,7 +81,7 @@ impl DebuggerCommand for PopulationCommand { cli: &DebuggerRepl, _matches: &ArgMatches, ) -> Result { - cli.writeln(&format!("{}", context.get_current_population()))?; + cli.writeln(&format!("{}", context.get_current_population())); Ok(false) } } @@ -92,20 +94,16 @@ impl DebuggerCommand for ContinueCommand { } fn handle( &self, - context: &mut Context, - cli: &DebuggerRepl, + _context: &mut Context, + _cli: &DebuggerRepl, _matches: &ArgMatches, ) -> Result { - cli.writeln(&format!( - "Continuing the simulation from t = {}", - context.get_current_time() - ))?; Ok(true) } } -fn readline() -> Result { - write!(std::io::stdout(), "$ ").map_err(|e| e.to_string())?; +fn readline(t: f64) -> Result { + write!(std::io::stdout(), "t={t} $ ").map_err(|e| e.to_string())?; std::io::stdout().flush().map_err(|e| e.to_string())?; let mut buffer = String::new(); std::io::stdin() @@ -120,6 +118,7 @@ fn setup_repl(line: &str, context: &mut Context) -> Result { let mut repl = DebuggerRepl::new(); repl.register_command("population", Box::new(PopulationCommand)); repl.register_command("continue", Box::new(ContinueCommand)); + let matches = repl .build_cli() .try_get_matches_from(args) @@ -139,9 +138,10 @@ fn setup_repl(line: &str, context: &mut Context) -> Result { /// Starts the debugger and pauses execution fn start_debugger(context: &mut Context) -> Result<(), IxaError> { - println!("Debugging simulation at t = {}", context.get_current_time()); + let t = context.get_current_time(); + println!("Debugging simulation at t = {t}"); loop { - let line = readline()?; + let line = readline(t).expect("Error reading input"); let line = line.trim(); if line.is_empty() { continue; @@ -155,7 +155,7 @@ fn start_debugger(context: &mut Context) -> Result<(), IxaError> { } Err(err) => { write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; - flush()?; + flush(); } } } @@ -186,6 +186,9 @@ mod tests { use assert_cmd::Command; use predicates::str::contains; + #[test] + fn test_cli_debugger_command() {} + #[test] fn test_cli_debugger_quits() { let mut cmd = Command::cargo_bin("runner_test_debug").unwrap(); @@ -196,8 +199,7 @@ mod tests { assert .success() - .stdout(contains("Debugging simulation at t = 1")) - .stdout(contains("Continuing the simulation from t = 1")); + .stdout(contains("Debugging simulation at t = 1")); } #[test] From 787de85110d168765143754e8c1ff0fd0871ffe7 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Mon, 16 Dec 2024 09:49:50 -0500 Subject: [PATCH 08/13] update runner example --- examples/runner/main.rs | 37 +++++++++++++++++++++++++++---------- src/runner.rs | 8 ++++---- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/examples/runner/main.rs b/examples/runner/main.rs index ce08e16..84ec182 100644 --- a/examples/runner/main.rs +++ b/examples/runner/main.rs @@ -1,23 +1,40 @@ use clap::Args; use ixa::runner::run_with_custom_args; +use ixa::ContextPeopleExt; #[derive(Args, Debug)] -struct Extra { +struct CustomArgs { #[arg(short, long)] - foo: bool, + population: Option, } fn main() { - // Try running this with `cargo run --example runner -- --seed 42` - run_with_custom_args(|context, args, extra: Option| { - context.add_plan(1.0, |_| { - println!("Hello, world!"); - }); - println!("{}", args.random_seed); - if let Some(extra) = extra { - println!("{}", extra.foo); + // Try running the following: + // cargo run --example runner -- --seed 42 + // cargo run --example runner -- --population 5 + // cargo run --example runner -- -p 5 --debugger + let context = run_with_custom_args(|context, args, custom_args: Option| { + println!("Setting random seed to {}", args.random_seed); + + // If an initial population was provided, add each person + if let Some(custom_args) = custom_args { + if let Some(population) = custom_args.population { + for _ in 0..population { + context.add_person(()).unwrap(); + } + } } + + context.add_plan(2.0, |context| { + println!("Adding two people at t=2"); + context.add_person(()).unwrap(); + context.add_person(()).unwrap(); + }); + Ok(()) }) .unwrap(); + + let final_count = context.get_current_population(); + println!("Simulation complete. The number of people is: {final_count}"); } diff --git a/src/runner.rs b/src/runner.rs index 85bf90e..01f0b81 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -46,7 +46,7 @@ fn create_ixa_cli() -> Command { /// # Errors /// Returns an error if argument parsing or the setup function fails #[allow(clippy::missing_errors_doc)] -pub fn run_with_custom_args(setup_fn: F) -> Result<(), Box> +pub fn run_with_custom_args(setup_fn: F) -> Result> where A: Args, F: Fn(&mut Context, BaseArgs, Option) -> Result<(), IxaError>, @@ -70,7 +70,7 @@ where /// # Errors /// Returns an error if argument parsing or the setup function fails #[allow(clippy::missing_errors_doc)] -pub fn run_with_args(setup_fn: F) -> Result<(), Box> +pub fn run_with_args(setup_fn: F) -> Result> where F: Fn(&mut Context, BaseArgs, Option) -> Result<(), IxaError>, { @@ -85,7 +85,7 @@ fn run_with_args_internal( args: BaseArgs, custom_args: Option, setup_fn: F, -) -> Result<(), Box> +) -> Result> where F: Fn(&mut Context, BaseArgs, Option) -> Result<(), IxaError>, { @@ -118,7 +118,7 @@ where // Execute the context context.execute(); - Ok(()) + Ok(context) } #[cfg(test)] From e1f6afe30e3c055264e6eec06292a9d75db88e31 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Mon, 16 Dec 2024 14:52:25 -0500 Subject: [PATCH 09/13] add tests and mock stdout, next command --- src/context.rs | 6 ++ src/debugger.rs | 204 ++++++++++++++++++++++++++++++++++-------------- src/plan.rs | 6 ++ 3 files changed, 158 insertions(+), 58 deletions(-) diff --git a/src/context.rs b/src/context.rs index 14c8529..b847429 100644 --- a/src/context.rs +++ b/src/context.rs @@ -210,6 +210,12 @@ impl Context { self.plan_queue.cancel_plan(id); } + /// Retrieve the number of remaining plans + #[must_use] + pub fn remaining_plan_count(&self) -> usize { + self.plan_queue.remaining_plan_count() + } + /// Add a `Callback` to the queue to be executed before the next plan pub fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static) { self.callback_queue.push_back(Box::new(callback)); diff --git a/src/debugger.rs b/src/debugger.rs index ee139bd..03e857c 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -1,7 +1,8 @@ use crate::Context; use crate::ContextPeopleExt; use crate::IxaError; -use clap::{ArgMatches, Command}; +use clap::value_parser; +use clap::{Arg, ArgMatches, Command}; use std::cell::RefCell; use std::collections::HashMap; use std::io::Write; @@ -15,6 +16,9 @@ trait DebuggerCommand { matches: &ArgMatches, ) -> Result; fn about(&self) -> &'static str; + fn extend(&self, subcommand: Command) -> Command { + subcommand + } } struct DebuggerRepl { @@ -22,18 +26,11 @@ struct DebuggerRepl { output: RefCell>, } -fn flush() { - std::io::stdout() - .flush() - .map_err(|e| e.to_string()) - .expect("Error flushing stdout"); -} - impl DebuggerRepl { - fn new() -> Self { + fn new(output: Box) -> Self { DebuggerRepl { commands: HashMap::new(), - output: RefCell::new(Box::new(std::io::stdout())), + output: RefCell::new(output), } } @@ -46,8 +43,9 @@ impl DebuggerRepl { } fn writeln(&self, formatted_string: &str) { - let _ = writeln!(self.output.borrow_mut(), "{formatted_string}").map_err(|e| e.to_string()); - flush(); + let mut output = self.output.borrow_mut(); + let _ = writeln!(output, "{formatted_string}").map_err(|e| e.to_string()); + output.flush().unwrap(); } fn build_cli(&self) -> Command { @@ -60,13 +58,34 @@ impl DebuggerRepl { .help_template("{all-args}"); for (name, handler) in &self.commands { - cli = cli.subcommand(Command::new(*name).about(handler.about()).help_template( - "{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}{after-help}", - )); + let subcommand = + handler.extend(Command::new(*name).about(handler.about()).help_template( + "{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}{after-help}", + )); + cli = cli.subcommand(subcommand); } cli } + + fn process_line(&self, l: &str, context: &mut Context) -> Result { + let args = shlex::split(l).ok_or("error: Invalid quoting")?; + let matches = self + .build_cli() + .try_get_matches_from(args) + .map_err(|e| e.to_string())?; + + if let Some((command, sub_matches)) = matches.subcommand() { + // If the provided command is known, run its handler + if let Some(handler) = self.get_command(command) { + return handler.handle(context, self, sub_matches); + } + // Unexpected command: print an error + return Err(format!("Unknown command: {command}")); + } + + unreachable!("subcommand required"); + } } /// Returns the current population of the simulation @@ -86,6 +105,32 @@ impl DebuggerCommand for PopulationCommand { } } +/// Adds a new debugger breakpoint at t +struct NextCommand; +impl DebuggerCommand for NextCommand { + fn about(&self) -> &'static str { + "Continue until the given time and then pause again" + } + fn extend(&self, subcommand: Command) -> Command { + subcommand.arg( + Arg::new("t") + .help("The next breakpoint (e.g., 4.2)") + .value_parser(value_parser!(f64)) + .required(true), + ) + } + fn handle( + &self, + context: &mut Context, + _cli: &DebuggerRepl, + matches: &ArgMatches, + ) -> Result { + let t = *matches.get_one::("t").unwrap(); + context.schedule_debugger(t); + Ok(true) + } +} + /// Exits the debugger and continues the simulation struct ContinueCommand; impl DebuggerCommand for ContinueCommand { @@ -102,6 +147,18 @@ impl DebuggerCommand for ContinueCommand { } } +// Assemble all the commands +fn build_repl(output: W) -> DebuggerRepl { + let mut repl = DebuggerRepl::new(Box::new(output)); + + repl.register_command("population", Box::new(PopulationCommand)); + repl.register_command("next", Box::new(NextCommand)); + repl.register_command("continue", Box::new(ContinueCommand)); + + repl +} + +// Helper function to read a line from stdin fn readline(t: f64) -> Result { write!(std::io::stdout(), "t={t} $ ").map_err(|e| e.to_string())?; std::io::stdout().flush().map_err(|e| e.to_string())?; @@ -112,34 +169,10 @@ fn readline(t: f64) -> Result { Ok(buffer) } -fn setup_repl(line: &str, context: &mut Context) -> Result { - let args = shlex::split(line).ok_or("error: Invalid quoting")?; - - let mut repl = DebuggerRepl::new(); - repl.register_command("population", Box::new(PopulationCommand)); - repl.register_command("continue", Box::new(ContinueCommand)); - - let matches = repl - .build_cli() - .try_get_matches_from(args) - .map_err(|e| e.to_string())?; - - if let Some((command, sub_matches)) = matches.subcommand() { - // If the provided command is known, run its handler - if let Some(handler) = repl.get_command(command) { - return handler.handle(context, &repl, sub_matches); - } - // Unexpected command: print an error - return Err(format!("Unknown command: {command}")); - } - - unreachable!("subcommand required"); -} - /// Starts the debugger and pauses execution fn start_debugger(context: &mut Context) -> Result<(), IxaError> { let t = context.get_current_time(); - println!("Debugging simulation at t = {t}"); + println!("Debugging simulation at t={t}"); loop { let line = readline(t).expect("Error reading input"); let line = line.trim(); @@ -147,7 +180,9 @@ fn start_debugger(context: &mut Context) -> Result<(), IxaError> { continue; } - match setup_repl(line, context) { + let repl = build_repl(std::io::stdout()); + + match repl.process_line(line, context) { Ok(quit) => { if quit { break; @@ -155,7 +190,7 @@ fn start_debugger(context: &mut Context) -> Result<(), IxaError> { } Err(err) => { write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?; - flush(); + std::io::stdout().flush().unwrap(); } } } @@ -183,15 +218,44 @@ impl ContextDebugExt for Context { #[cfg(test)] mod tests { - use assert_cmd::Command; + use crate::{Context, ContextPeopleExt}; use predicates::str::contains; + use std::{cell::RefCell, io::Write, rc::Rc}; - #[test] - fn test_cli_debugger_command() {} + use super::build_repl; + + #[derive(Clone)] + struct StdoutMock { + storage: Rc>>, + } + + impl StdoutMock { + fn new() -> Self { + StdoutMock { + storage: Rc::new(RefCell::new(Vec::new())), + } + } + fn into_inner(self) -> Vec { + Rc::try_unwrap(self.storage) + .expect("Multiple references to storage") + .into_inner() + } + fn into_string(self) -> String { + String::from_utf8(self.into_inner()).unwrap() + } + } + impl Write for StdoutMock { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.storage.borrow_mut().write(buf) + } + fn flush(&mut self) -> std::io::Result<()> { + self.storage.borrow_mut().flush() + } + } #[test] - fn test_cli_debugger_quits() { - let mut cmd = Command::cargo_bin("runner_test_debug").unwrap(); + fn test_cli_debugger_integration() { + let mut cmd = assert_cmd::Command::cargo_bin("runner_test_debug").unwrap(); let assert = cmd .args(["--debugger", "1.0"]) .write_stdin("continue\n") @@ -203,18 +267,42 @@ mod tests { } #[test] - #[ignore] fn test_cli_debugger_population() { - let assert = Command::cargo_bin("runner_test_debug") - .unwrap() - .args(["--debugger", "1.0"]) - .write_stdin("population\n") - .write_stdin("continue\n") - .assert(); + let context = &mut Context::new(); + // Add 2 people + context.add_person(()).unwrap(); + context.add_person(()).unwrap(); - assert - .success() - // This doesn't seem to work for some reason - .stdout(contains("The number of people is 3")); + let output = StdoutMock::new(); + let repl = build_repl(output.clone()); + let quits = repl.process_line("population\n", context).unwrap(); + assert!(!quits, "should not exit"); + + drop(repl); + assert!(output.into_string().contains('2')); + } + + #[test] + fn test_cli_continue() { + let context = &mut Context::new(); + let output = StdoutMock::new(); + let repl = build_repl(output.clone()); + let quits = repl.process_line("continue\n", context).unwrap(); + assert!(quits, "should exit"); + } + + #[test] + fn test_cli_next() { + let context = &mut Context::new(); + assert_eq!(context.remaining_plan_count(), 0); + let output = StdoutMock::new(); + let repl = build_repl(output.clone()); + let quits = repl.process_line("next 2\n", context).unwrap(); + assert!(quits, "should exit"); + assert_eq!( + context.remaining_plan_count(), + 1, + "should schedule a plan for the debugger to pause" + ); } } diff --git a/src/plan.rs b/src/plan.rs index e27a173..c70f4a3 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -97,6 +97,12 @@ impl Queue { } } } + + /// Returns the number of remaining plans + #[must_use] + pub fn remaining_plan_count(&self) -> usize { + self.queue.len() + } } impl Default for Queue { From c50fe749d4fe45409f93b4285c290fa1d4e39e58 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Tue, 17 Dec 2024 15:40:28 -0500 Subject: [PATCH 10/13] Fix the test --- src/debugger.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/debugger.rs b/src/debugger.rs index 03e857c..cc7e586 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -219,7 +219,6 @@ impl ContextDebugExt for Context { #[cfg(test)] mod tests { use crate::{Context, ContextPeopleExt}; - use predicates::str::contains; use std::{cell::RefCell, io::Write, rc::Rc}; use super::build_repl; @@ -255,15 +254,13 @@ mod tests { #[test] fn test_cli_debugger_integration() { - let mut cmd = assert_cmd::Command::cargo_bin("runner_test_debug").unwrap(); - let assert = cmd + assert_cmd::Command::cargo_bin("runner_test_debug") + .unwrap() .args(["--debugger", "1.0"]) + .write_stdin("population\n") .write_stdin("continue\n") - .assert(); - - assert - .success() - .stdout(contains("Debugging simulation at t = 1")); + .assert() + .success(); } #[test] From 5e82a813694d8828732c544c49865c6248c88405 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Tue, 17 Dec 2024 16:22:42 -0500 Subject: [PATCH 11/13] Review fixes --- examples/runner/main.rs | 8 ++++---- src/context.rs | 6 ++---- src/debugger.rs | 35 +++++++++++++++++------------------ src/plan.rs | 4 +--- 4 files changed, 24 insertions(+), 29 deletions(-) diff --git a/examples/runner/main.rs b/examples/runner/main.rs index 84ec182..813a9c3 100644 --- a/examples/runner/main.rs +++ b/examples/runner/main.rs @@ -4,21 +4,21 @@ use ixa::ContextPeopleExt; #[derive(Args, Debug)] struct CustomArgs { - #[arg(short, long)] - population: Option, + #[arg(short = 'p', long)] + starting_population: Option, } fn main() { // Try running the following: // cargo run --example runner -- --seed 42 - // cargo run --example runner -- --population 5 + // cargo run --example runner -- --starting-population 5 // cargo run --example runner -- -p 5 --debugger let context = run_with_custom_args(|context, args, custom_args: Option| { println!("Setting random seed to {}", args.random_seed); // If an initial population was provided, add each person if let Some(custom_args) = custom_args { - if let Some(population) = custom_args.population { + if let Some(population) = custom_args.starting_population { for _ in 0..population { context.add_person(()).unwrap(); } diff --git a/src/context.rs b/src/context.rs index b847429..308d1d6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -210,10 +210,8 @@ impl Context { self.plan_queue.cancel_plan(id); } - /// Retrieve the number of remaining plans - #[must_use] - pub fn remaining_plan_count(&self) -> usize { - self.plan_queue.remaining_plan_count() + pub(crate) fn _remaining_plan_count(&self) -> usize { + self.plan_queue._remaining_plan_count() } /// Add a `Callback` to the queue to be executed before the next plan diff --git a/src/debugger.rs b/src/debugger.rs index cc7e586..2f871a7 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -12,9 +12,8 @@ trait DebuggerCommand { fn handle( &self, context: &mut Context, - cli: &DebuggerRepl, matches: &ArgMatches, - ) -> Result; + ) -> Result<(bool, Option), String>; fn about(&self) -> &'static str; fn extend(&self, subcommand: Command) -> Command { subcommand @@ -69,7 +68,7 @@ impl DebuggerRepl { } fn process_line(&self, l: &str, context: &mut Context) -> Result { - let args = shlex::split(l).ok_or("error: Invalid quoting")?; + let args = shlex::split(l).ok_or("Error splitting lines")?; let matches = self .build_cli() .try_get_matches_from(args) @@ -78,7 +77,11 @@ impl DebuggerRepl { if let Some((command, sub_matches)) = matches.subcommand() { // If the provided command is known, run its handler if let Some(handler) = self.get_command(command) { - return handler.handle(context, self, sub_matches); + let (quit, output) = handler.handle(context, sub_matches)?; + if let Some(output) = output { + self.writeln(&output); + } + return Ok(quit); } // Unexpected command: print an error return Err(format!("Unknown command: {command}")); @@ -97,11 +100,10 @@ impl DebuggerCommand for PopulationCommand { fn handle( &self, context: &mut Context, - cli: &DebuggerRepl, _matches: &ArgMatches, - ) -> Result { - cli.writeln(&format!("{}", context.get_current_population())); - Ok(false) + ) -> Result<(bool, Option), String> { + let output = format!("{}", context.get_current_population()); + Ok((false, Some(output))) } } @@ -122,12 +124,11 @@ impl DebuggerCommand for NextCommand { fn handle( &self, context: &mut Context, - _cli: &DebuggerRepl, matches: &ArgMatches, - ) -> Result { + ) -> Result<(bool, Option), String> { let t = *matches.get_one::("t").unwrap(); context.schedule_debugger(t); - Ok(true) + Ok((true, None)) } } @@ -140,10 +141,9 @@ impl DebuggerCommand for ContinueCommand { fn handle( &self, _context: &mut Context, - _cli: &DebuggerRepl, _matches: &ArgMatches, - ) -> Result { - Ok(true) + ) -> Result<(bool, Option), String> { + Ok((true, None)) } } @@ -172,6 +172,7 @@ fn readline(t: f64) -> Result { /// Starts the debugger and pauses execution fn start_debugger(context: &mut Context) -> Result<(), IxaError> { let t = context.get_current_time(); + let repl = build_repl(std::io::stdout()); println!("Debugging simulation at t={t}"); loop { let line = readline(t).expect("Error reading input"); @@ -180,8 +181,6 @@ fn start_debugger(context: &mut Context) -> Result<(), IxaError> { continue; } - let repl = build_repl(std::io::stdout()); - match repl.process_line(line, context) { Ok(quit) => { if quit { @@ -291,13 +290,13 @@ mod tests { #[test] fn test_cli_next() { let context = &mut Context::new(); - assert_eq!(context.remaining_plan_count(), 0); + assert_eq!(context._remaining_plan_count(), 0); let output = StdoutMock::new(); let repl = build_repl(output.clone()); let quits = repl.process_line("next 2\n", context).unwrap(); assert!(quits, "should exit"); assert_eq!( - context.remaining_plan_count(), + context._remaining_plan_count(), 1, "should schedule a plan for the debugger to pause" ); diff --git a/src/plan.rs b/src/plan.rs index c70f4a3..45d86ff 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -98,9 +98,7 @@ impl Queue { } } - /// Returns the number of remaining plans - #[must_use] - pub fn remaining_plan_count(&self) -> usize { + pub(crate) fn _remaining_plan_count(&self) -> usize { self.queue.len() } } From 3611b9c0b2a5ff7ba5439534270d91aa1b5d87f6 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Tue, 17 Dec 2024 16:25:15 -0500 Subject: [PATCH 12/13] review fixes --- src/debugger.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/debugger.rs b/src/debugger.rs index 2f871a7..75d032e 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -43,7 +43,9 @@ impl DebuggerRepl { fn writeln(&self, formatted_string: &str) { let mut output = self.output.borrow_mut(); - let _ = writeln!(output, "{formatted_string}").map_err(|e| e.to_string()); + writeln!(output, "{formatted_string}") + .map_err(|e| e.to_string()) + .unwrap(); output.flush().unwrap(); } From e6b7b632c6a16d201f8bd7aa37222ddeb139d414 Mon Sep 17 00:00:00 2001 From: k88hudson-cfa Date: Tue, 17 Dec 2024 16:29:12 -0500 Subject: [PATCH 13/13] fix --- src/context.rs | 6 ++++-- src/debugger.rs | 4 ++-- src/plan.rs | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/context.rs b/src/context.rs index 308d1d6..0009808 100644 --- a/src/context.rs +++ b/src/context.rs @@ -210,8 +210,10 @@ impl Context { self.plan_queue.cancel_plan(id); } - pub(crate) fn _remaining_plan_count(&self) -> usize { - self.plan_queue._remaining_plan_count() + #[doc(hidden)] + #[allow(dead_code)] + pub(crate) fn remaining_plan_count(&self) -> usize { + self.plan_queue.remaining_plan_count() } /// Add a `Callback` to the queue to be executed before the next plan diff --git a/src/debugger.rs b/src/debugger.rs index 75d032e..0c21848 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -292,13 +292,13 @@ mod tests { #[test] fn test_cli_next() { let context = &mut Context::new(); - assert_eq!(context._remaining_plan_count(), 0); + assert_eq!(context.remaining_plan_count(), 0); let output = StdoutMock::new(); let repl = build_repl(output.clone()); let quits = repl.process_line("next 2\n", context).unwrap(); assert!(quits, "should exit"); assert_eq!( - context._remaining_plan_count(), + context.remaining_plan_count(), 1, "should schedule a plan for the debugger to pause" ); diff --git a/src/plan.rs b/src/plan.rs index 45d86ff..7d9c70c 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -98,7 +98,8 @@ impl Queue { } } - pub(crate) fn _remaining_plan_count(&self) -> usize { + #[doc(hidden)] + pub(crate) fn remaining_plan_count(&self) -> usize { self.queue.len() } }