ixa/
debugger.rs

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    /// Handle the command and any inputs; returning true will exit the debugger
14    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    // Build the debugger context.
29    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() // cli can only be used once.
72            .try_get_matches_from(args)
73            .map_err(|e| e.to_string())?;
74
75        if let Some((command, _)) = matches.subcommand() {
76            // If the provided command is known, run its handler
77
78            if let Some(handler) = self.get_command(command) {
79                return handler.handle(context, &matches);
80            }
81            // Unexpected command: print an error
82            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
187/// Exits the debugger and ends the simulation.
188struct 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
203/// Adds a new debugger breakpoint at t
204struct NextCommand;
205impl DebuggerCommand for NextCommand {
206    fn handle(
207        &self,
208        context: &mut Context,
209        _matches: &ArgMatches,
210    ) -> Result<(bool, Option<String>), String> {
211        // We execute directly instead of setting `Context::break_requested` so as not to interfere
212        // with anything else that might be requesting a break, or in case debugger sessions become
213        // stateful.
214        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;
223/// Adds a new debugger breakpoint at t
224impl 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 => { /* pass */ }
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    /// Exits the debugger and continues the simulation
259    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/// Starts a debugging REPL session, interrupting the normal simulation event loop.
280#[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    // We temporarily swap out the debugger so we can have simultaneous mutable access to
286    // it and to `context`. We swap it back in at the end of the function.
287    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    // Restore the debugger
325    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        // Temporarily take the data container out of context so that
338        // we can operate on context.
339        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        // Add 2 people
441        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        // Add 2 people
454        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        // Add 2 people
467        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        // Add 3 people
481        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        // Note: the global property names are also listed as part of the output
501        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        // We can't use process_line here because we expect an error to be
510        // returned rather than string output
511        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}