1use crate::define_data_plugin;
2use crate::external_api::{
3 breakpoint, global_properties, halt, next, people, population, run_ext_api, EmptyArgs,
4};
5use crate::{trace, Context, IxaError};
6use crate::{HashMap, HashMapExt};
7use clap::{ArgMatches, Command, FromArgMatches, Parser, Subcommand};
8use rustyline;
9
10use std::fmt::Write;
11
12trait DebuggerCommand {
13 fn handle(
15 &self,
16 context: &mut Context,
17 matches: &ArgMatches,
18 ) -> Result<(bool, Option<String>), String>;
19 fn extend(&self, command: Command) -> Command;
20}
21
22struct Debugger {
23 rl: rustyline::DefaultEditor,
24 cli: Command,
25 commands: HashMap<&'static str, Box<dyn DebuggerCommand>>,
26}
27define_data_plugin!(DebuggerPlugin, Option<Debugger>, |_context| {
28 trace!("initializing debugger");
30 let mut commands: HashMap<&'static str, Box<dyn DebuggerCommand>> = HashMap::new();
31 commands.insert("breakpoint", Box::new(BreakpointCommand));
32 commands.insert("continue", Box::new(ContinueCommand));
33 commands.insert("global", Box::new(GlobalPropertyCommand));
34 commands.insert("halt", Box::new(HaltCommand));
35 commands.insert("next", Box::new(NextCommand));
36 commands.insert("people", Box::new(PeopleCommand));
37 commands.insert("population", Box::new(PopulationCommand));
38
39 let mut cli = Command::new("repl")
40 .multicall(true)
41 .arg_required_else_help(true)
42 .subcommand_required(true)
43 .subcommand_value_name("DEBUGGER")
44 .subcommand_help_heading("IXA DEBUGGER")
45 .help_template("{all-args}");
46
47 for handler in commands.values() {
48 cli = handler.extend(cli);
49 }
50
51 Some(Debugger {
52 rl: rustyline::DefaultEditor::new().unwrap(),
53 cli,
54 commands,
55 })
56});
57
58impl Debugger {
59 fn get_command(&self, name: &str) -> Option<&dyn DebuggerCommand> {
60 self.commands.get(name).map(|command| &**command)
61 }
62
63 fn process_command(
64 &self,
65 l: &str,
66 context: &mut Context,
67 ) -> Result<(bool, Option<String>), String> {
68 let args = shlex::split(l).ok_or("Error splitting lines")?;
69 let matches = self
70 .cli
71 .clone() .try_get_matches_from(args)
73 .map_err(|e| e.to_string())?;
74
75 if let Some((command, _)) = matches.subcommand() {
76 if let Some(handler) = self.get_command(command) {
79 return handler.handle(context, &matches);
80 }
81 return Err(format!("error: Unknown command: {command}"));
83 }
84
85 unreachable!("subcommand required");
86 }
87}
88
89struct PopulationCommand;
90impl DebuggerCommand for PopulationCommand {
91 fn handle(
92 &self,
93 context: &mut Context,
94 _matches: &ArgMatches,
95 ) -> Result<(bool, Option<String>), String> {
96 let output = format!(
97 "{}",
98 run_ext_api::<population::Api>(context, &EmptyArgs {})
99 .unwrap()
100 .population
101 );
102 Ok((false, Some(output)))
103 }
104 fn extend(&self, command: Command) -> Command {
105 population::Args::augment_subcommands(command)
106 }
107}
108
109struct PeopleCommand;
110impl DebuggerCommand for PeopleCommand {
111 fn extend(&self, command: Command) -> Command {
112 people::Args::augment_subcommands(command)
113 }
114 fn handle(
115 &self,
116 context: &mut Context,
117 matches: &ArgMatches,
118 ) -> Result<(bool, Option<String>), String> {
119 let args = people::Args::from_arg_matches(matches).unwrap();
120 match run_ext_api::<people::Api>(context, &args) {
121 Ok(people::Retval::Properties(props)) => Ok((
122 false,
123 Some(
124 props
125 .into_iter()
126 .map(|(k, v)| format!("{k}: {v}"))
127 .collect::<Vec<_>>()
128 .join("\n"),
129 ),
130 )),
131 Ok(people::Retval::PropertyNames(names)) => Ok((
132 false,
133 Some(format!("Available properties:\n{}", names.join("\n"))),
134 )),
135 Ok(people::Retval::Tabulated(rows)) => Ok((
136 false,
137 Some(
138 rows.into_iter()
139 .map(|(props, count)| {
140 format!(
141 "{}: {}",
142 count,
143 props
144 .into_iter()
145 .map(|(k, v)| format!("{k}={v}"))
146 .collect::<Vec<_>>()
147 .join(", ")
148 )
149 })
150 .collect::<Vec<_>>()
151 .join("\n"),
152 ),
153 )),
154 Err(e) => Ok((false, Some(format!("error: {e}")))),
155 }
156 }
157}
158
159struct GlobalPropertyCommand;
160impl DebuggerCommand for GlobalPropertyCommand {
161 fn extend(&self, command: Command) -> Command {
162 global_properties::Args::augment_subcommands(command)
163 }
164 fn handle(
165 &self,
166 context: &mut Context,
167 matches: &ArgMatches,
168 ) -> Result<(bool, Option<String>), String> {
169 let args = global_properties::Args::from_arg_matches(matches).unwrap();
170 let ret = run_ext_api::<global_properties::Api>(context, &args);
171 match ret {
172 Err(IxaError::IxaError(e)) => Ok((false, Some(format!("error: {e}")))),
173 Err(e) => Ok((false, Some(format!("error: {e}")))),
174 Ok(global_properties::Retval::List(properties)) => Ok((
175 false,
176 Some(format!(
177 "{} global properties registered:\n{}",
178 properties.len(),
179 properties.join("\n")
180 )),
181 )),
182 Ok(global_properties::Retval::Value(value)) => Ok((false, Some(value))),
183 }
184 }
185}
186
187struct HaltCommand;
189impl DebuggerCommand for HaltCommand {
190 fn handle(
191 &self,
192 context: &mut Context,
193 _matches: &ArgMatches,
194 ) -> Result<(bool, Option<String>), String> {
195 context.shutdown();
196 Ok((true, None))
197 }
198 fn extend(&self, command: Command) -> Command {
199 halt::Args::augment_subcommands(command)
200 }
201}
202
203struct NextCommand;
205impl DebuggerCommand for NextCommand {
206 fn handle(
207 &self,
208 context: &mut Context,
209 _matches: &ArgMatches,
210 ) -> Result<(bool, Option<String>), String> {
211 context.execute_single_step();
215 Ok((false, None))
216 }
217 fn extend(&self, command: Command) -> Command {
218 next::Args::augment_subcommands(command)
219 }
220}
221
222struct BreakpointCommand;
223impl DebuggerCommand for BreakpointCommand {
225 fn handle(
226 &self,
227 context: &mut Context,
228 matches: &ArgMatches,
229 ) -> Result<(bool, Option<String>), String> {
230 let args = breakpoint::Args::from_arg_matches(matches).unwrap();
231 match run_ext_api::<breakpoint::Api>(context, &args) {
232 Err(IxaError::IxaError(e)) => Ok((false, Some(format!("error: {e}")))),
233 Ok(return_value) => {
234 match return_value {
235 breakpoint::Retval::List(bp_list) => {
236 let mut msg = format!("Scheduled breakpoints: {}\n", bp_list.len());
237 for bp in bp_list {
238 _ = writeln!(&mut msg, "\t{bp}");
239 }
240 return Ok((false, Some(msg)));
241 }
242 breakpoint::Retval::Ok => { }
243 }
244
245 Ok((false, None))
246 }
247 _ => unimplemented!(),
248 }
249 }
250 fn extend(&self, command: Command) -> Command {
251 breakpoint::Args::augment_subcommands(command)
252 }
253}
254
255struct ContinueCommand;
256#[derive(Parser, Debug)]
257enum ContinueSubcommand {
258 Continue,
260}
261impl DebuggerCommand for ContinueCommand {
262 fn handle(
263 &self,
264 _context: &mut Context,
265 _matches: &ArgMatches,
266 ) -> Result<(bool, Option<String>), String> {
267 Ok((true, None))
268 }
269 fn extend(&self, command: Command) -> Command {
270 ContinueSubcommand::augment_subcommands(command)
271 }
272}
273
274fn exit_debugger() -> ! {
275 println!("Got Ctrl-D, Exiting...");
276 std::process::exit(0);
277}
278
279#[allow(clippy::missing_panics_doc)]
281pub fn enter_debugger(context: &mut Context) {
282 let current_time = context.get_current_time();
283 context.cancel_debugger_request();
284
285 let mut debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
288
289 println!("Debugging simulation at t={current_time}");
290 loop {
291 let line = match debugger.rl.readline(&format!("t={current_time:.4} $ ")) {
292 Ok(line) => line,
293 Err(
294 rustyline::error::ReadlineError::Signal(_)
295 | rustyline::error::ReadlineError::Interrupted,
296 ) => continue,
297 Err(rustyline::error::ReadlineError::Eof) => exit_debugger(),
298 Err(err) => panic!("Read error: {err}"),
299 };
300 debugger
301 .rl
302 .add_history_entry(line.clone())
303 .expect("Should be able to add to input");
304 let line = line.trim();
305 if line.is_empty() {
306 continue;
307 }
308
309 match debugger.process_command(line, context) {
310 Ok((quit, message)) => {
311 if let Some(message) = message {
312 println!("{message}");
313 }
314 if quit {
315 break;
316 }
317 }
318 Err(err) => {
319 eprintln!("{err}");
320 }
321 }
322 }
323
324 let saved_debugger = context.get_data_mut(DebuggerPlugin);
326 *saved_debugger = Some(debugger);
327}
328
329#[cfg(test)]
330mod tests {
331 use super::{enter_debugger, DebuggerPlugin};
332 use crate::{define_global_property, define_person_property, ContextGlobalPropertiesExt};
333 use crate::{Context, ContextPeopleExt, ExecutionPhase};
334 use assert_approx_eq::assert_approx_eq;
335
336 fn process_line(line: &str, context: &mut Context) -> (bool, Option<String>) {
337 let data_container = context.get_data_mut(DebuggerPlugin);
340 let debugger = data_container.take().unwrap();
341
342 let res = debugger.process_command(line, context).unwrap();
343 let data_container = context.get_data_mut(DebuggerPlugin);
344 *data_container = Some(debugger);
345 res
346 }
347
348 define_global_property!(FooGlobal, String);
349 define_global_property!(BarGlobal, u32);
350 define_person_property!(Age, u8);
351 define_person_property!(Smile, u32);
352
353 #[test]
354 fn test_cli_debugger_breakpoint_set() {
355 let context = &mut Context::new();
356 let (quits, _output) = process_line("breakpoint set 4.0\n", context);
357
358 assert!(!quits, "should not exit");
359
360 let list = context.list_breakpoints(0);
361 assert_eq!(list.len(), 1);
362 if let Some(schedule) = list.first() {
363 assert_eq!(schedule.priority, ExecutionPhase::First);
364 assert_eq!(schedule.plan_id, 0u64);
365 assert_approx_eq!(schedule.time, 4.0f64);
366 }
367 }
368
369 #[test]
370 fn test_cli_debugger_breakpoint_list() {
371 let context = &mut Context::new();
372
373 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
374 context.schedule_debugger(2.0, Some(ExecutionPhase::First), Box::new(enter_debugger));
375 context.schedule_debugger(3.0, Some(ExecutionPhase::Normal), Box::new(enter_debugger));
376 context.schedule_debugger(4.0, Some(ExecutionPhase::Last), Box::new(enter_debugger));
377
378 let expected = r"Scheduled breakpoints: 4
379 0: t=1 (First)
380 1: t=2 (First)
381 2: t=3 (Normal)
382 3: t=4 (Last)
383";
384
385 let (quits, output) = process_line("breakpoint list\n", context);
386
387 assert!(!quits, "should not exit");
388 assert!(output.is_some());
389 assert_eq!(output.unwrap(), expected);
390 }
391
392 #[test]
393 fn test_cli_debugger_breakpoint_delete_id() {
394 let context = &mut Context::new();
395
396 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
397 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
398
399 let (quits, _output) = process_line("breakpoint delete 0\n", context);
400 assert!(!quits, "should not exit");
401 let list = context.list_breakpoints(0);
402
403 assert_eq!(list.len(), 1);
404 if let Some(schedule) = list.first() {
405 assert_eq!(schedule.priority, ExecutionPhase::First);
406 assert_eq!(schedule.plan_id, 1u64);
407 assert_approx_eq!(schedule.time, 2.0f64);
408 }
409 }
410
411 #[test]
412 fn test_cli_debugger_breakpoint_delete_all() {
413 let context = &mut Context::new();
414
415 context.schedule_debugger(1.0, None, Box::new(enter_debugger));
416 context.schedule_debugger(2.0, None, Box::new(enter_debugger));
417
418 let (quits, _output) = process_line("breakpoint delete --all\n", context);
419 assert!(!quits, "should not exit");
420 let list = context.list_breakpoints(0);
421 assert_eq!(list.len(), 0);
422 }
423
424 #[test]
425 fn test_cli_debugger_breakpoint_disable_enable() {
426 let context = &mut Context::new();
427
428 let (quits, _output) = process_line("breakpoint disable\n", context);
429 assert!(!quits, "should not exit");
430 assert!(!context.breakpoints_are_enabled());
431
432 let (quits, _output) = process_line("breakpoint enable\n", context);
433 assert!(!quits, "should not exit");
434 assert!(context.breakpoints_are_enabled());
435 }
436
437 #[test]
438 fn test_cli_debugger_population() {
439 let context = &mut Context::new();
440 context.add_person(()).unwrap();
442 context.add_person(()).unwrap();
443
444 let (quits, output) = process_line("population\n", context);
445
446 assert!(!quits, "should not exit");
447 assert!(output.unwrap().contains('2'));
448 }
449
450 #[test]
451 fn test_cli_debugger_people_get() {
452 let context = &mut Context::new();
453 context.add_person((Age, 10)).unwrap();
455 context.add_person((Age, 5)).unwrap();
456 assert_eq!(context.remaining_plan_count(), 0);
457 let (_, output) = process_line("people get 0 Age", context);
458 assert_eq!(output.unwrap(), "Age: 10");
459 let (_, output) = process_line("people get 1 Age", context);
460 assert_eq!(output.unwrap(), "Age: 5");
461 }
462
463 #[test]
464 fn test_cli_debugger_people_properties() {
465 let context = &mut Context::new();
466 context.add_person(((Age, 10), (Smile, 50))).unwrap();
468 context.add_person(((Age, 5), (Smile, 60))).unwrap();
469 let (_, output) = process_line("people get 0 Smile", context);
470 assert_eq!(output.unwrap(), "Smile: 50");
471 let (_, output) = process_line("people properties", context);
472 let properties = output.unwrap();
473 assert!(properties.contains("Smile"));
474 assert!(properties.contains("Age"));
475 }
476
477 #[test]
478 fn test_cli_debugger_people_tabulate() {
479 let context = &mut Context::new();
480 context.add_person(((Age, 10), (Smile, 50))).unwrap();
482 context.add_person(((Age, 10), (Smile, 60))).unwrap();
483 context.add_person(((Age, 10), (Smile, 60))).unwrap();
484 let (_, output) = process_line("people tabulate Age", context);
485 assert_eq!(output.unwrap(), "3: Age=10");
486 let (_, output) = process_line("people tabulate Smile", context);
487 let results = output.unwrap();
488 assert!(results.contains("1: Smile=50"));
489 assert!(results.contains("2: Smile=60"));
490 }
491
492 #[test]
493 fn test_cli_debugger_global_list() {
494 let context = &mut Context::new();
495 let (_quits, output) = process_line("global list\n", context);
496 let expected = format!(
497 "{} global properties registered:",
498 context.list_registered_global_properties().len()
499 );
500 assert!(output.unwrap().contains(&expected));
502 }
503
504 #[test]
505 fn test_cli_debugger_global_no_args() {
506 let input = "global get\n";
507 let context = &mut Context::new();
508
509 let debugger = context.get_data_mut(DebuggerPlugin).take().unwrap();
512 let result = debugger.process_command(input, context);
513 let data_container = context.get_data_mut(DebuggerPlugin);
514 *data_container = Some(debugger);
515
516 assert!(result.is_err());
517 assert!(result
518 .unwrap_err()
519 .contains("required arguments were not provided"));
520 }
521
522 #[test]
523 fn test_cli_debugger_global_get_unregistered_prop() {
524 let context = &mut Context::new();
525 let (_quits, output) = process_line("global get NotExist\n", context);
526 assert_eq!(output.unwrap(), "error: No global property: NotExist");
527 }
528
529 #[test]
530 fn test_cli_debugger_global_get_registered_prop() {
531 let context = &mut Context::new();
532 context
533 .set_global_property_value(FooGlobal, "hello".to_string())
534 .unwrap();
535 let (_quits, output) = process_line("global get ixa.FooGlobal\n", context);
536 assert_eq!(output.unwrap(), "\"hello\"");
537 }
538
539 #[test]
540 fn test_cli_debugger_global_get_empty_prop() {
541 define_global_property!(EmptyGlobal, String);
542 let context = &mut Context::new();
543 let (_quits, output) = process_line("global get ixa.EmptyGlobal\n", context);
544 assert_eq!(
545 output.unwrap(),
546 "error: Property ixa.EmptyGlobal is not set"
547 );
548 }
549
550 #[test]
551 fn test_cli_continue() {
552 let context = &mut Context::new();
553 let (quits, _) = process_line("continue\n", context);
554 assert!(quits, "should exit");
555 }
556
557 #[test]
558 fn test_cli_next() {
559 let context = &mut Context::new();
560 assert_eq!(context.remaining_plan_count(), 0);
561 let (quits, _) = process_line("next\n", context);
562 assert!(!quits, "should not exit");
563 }
564}