ixa/
debugger.rs

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