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