ixa/
debugger.rs

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    /// Handle the command and any inputs; returning true will exit the debugger
15    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() // cli can only be used once.
44            .try_get_matches_from(args)
45            .map_err(|e| e.to_string())?;
46
47        if let Some((command, _)) = matches.subcommand() {
48            // If the provided command is known, run its handler
49
50            if let Some(handler) = self.get_command(command) {
51                return handler.handle(context, &matches);
52            }
53            // Unexpected command: print an error
54            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
159/// Exits the debugger and ends the simulation.
160struct 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
175/// Adds a new debugger breakpoint at t
176struct NextCommand;
177impl DebuggerCommand for NextCommand {
178    fn handle(
179        &self,
180        context: &mut Context,
181        _matches: &ArgMatches,
182    ) -> Result<(bool, Option<String>), String> {
183        // We execute directly instead of setting `Context::break_requested` so as not to interfere
184        // with anything else that might be requesting a break, or in case debugger sessions become
185        // stateful.
186        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;
195/// Adds a new debugger breakpoint at t
196impl 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 => { /* pass */ }
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    /// Exits the debugger and continues the simulation
231    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
246// Build the debugger context.
247fn 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/// Starts a debugging session.
287#[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
295/// Enters the debugger REPL, interrupting the normal simulation event loop. This private
296/// function is called from the public API `enter_debug()`.
297fn 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        // Temporarily take the data container out of context so that
347        // we can operate on context.
348        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        // Add 2 people
451        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        // Add 2 people
464        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        // Add 2 people
477        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        // Add 3 people
491        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        // Note: the global property names are also listed as part of the output
511        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        // We can't use process_line here because we an expect an error to be
520        // returned rather than string output
521        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}