1#![allow(dead_code)]
9
10use anyhow::Result;
11use chrono::{DateTime, Utc};
12use indexmap::IndexMap;
13use parking_lot::{Mutex, RwLock};
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, VecDeque};
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::DebugConfig;
20
21#[derive(Debug)]
23pub struct InteractiveDebugger {
24 config: DebugConfig,
25 state: Arc<RwLock<DebuggerState>>,
26 breakpoints: Arc<RwLock<HashMap<String, Breakpoint>>>,
27 execution_history: Arc<Mutex<VecDeque<ExecutionSnapshot>>>,
28 current_step: Arc<Mutex<usize>>,
29 max_history_size: usize,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DebuggerState {
35 pub is_running: bool,
36 pub is_paused: bool,
37 pub current_location: Option<DebugLocation>,
38 pub call_stack: Vec<StackFrame>,
39 pub variables: IndexMap<String, VariableValue>,
40 pub step_mode: StepMode,
41 pub session_start: DateTime<Utc>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct DebugLocation {
47 pub module: String,
48 pub function: String,
49 pub line: Option<u32>,
50 pub instruction: Option<String>,
51 pub context: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StackFrame {
57 pub id: Uuid,
58 pub location: DebugLocation,
59 pub locals: IndexMap<String, VariableValue>,
60 pub timestamp: DateTime<Utc>,
61 pub depth: usize,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct VariableValue {
67 pub name: String,
68 pub value: String,
69 pub type_name: String,
70 pub size_bytes: Option<usize>,
71 pub shape: Option<Vec<usize>>,
72 pub is_tensor: bool,
73 pub metadata: HashMap<String, String>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum StepMode {
79 StepInto,
81 StepOver,
83 StepOut,
85 Continue,
87 SingleStep,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct Breakpoint {
94 pub id: Uuid,
95 pub location: DebugLocation,
96 pub condition: Option<String>,
97 pub hit_count: usize,
98 pub enabled: bool,
99 pub temporary: bool,
100 pub log_message: Option<String>,
101 pub created_at: DateTime<Utc>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct ExecutionSnapshot {
107 pub id: Uuid,
108 pub timestamp: DateTime<Utc>,
109 pub step_number: usize,
110 pub location: DebugLocation,
111 pub call_stack: Vec<StackFrame>,
112 pub variables: IndexMap<String, VariableValue>,
113 pub memory_usage: Option<usize>,
114 pub performance_metrics: HashMap<String, f64>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub enum DebuggerCommand {
120 Start,
121 Pause,
122 Resume,
123 Step(StepMode),
124 SetBreakpoint(DebugLocation, Option<String>),
125 RemoveBreakpoint(Uuid),
126 InspectVariable(String),
127 EvaluateExpression(String),
128 ShowCallStack,
129 ShowHistory,
130 JumpToStep(usize),
131 Reset,
132 Exit,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub enum DebuggerResponse {
138 Started,
139 Paused(DebugLocation),
140 Resumed,
141 Stepped(ExecutionSnapshot),
142 BreakpointHit(Breakpoint, ExecutionSnapshot),
143 VariableInspected(VariableValue),
144 ExpressionEvaluated(String),
145 CallStackShown(Vec<StackFrame>),
146 HistoryShown(Vec<ExecutionSnapshot>),
147 JumpedToStep(ExecutionSnapshot),
148 Reset,
149 Error(String),
150}
151
152impl InteractiveDebugger {
153 pub fn new(config: &DebugConfig) -> Self {
155 Self {
156 config: config.clone(),
157 state: Arc::new(RwLock::new(DebuggerState {
158 is_running: false,
159 is_paused: false,
160 current_location: None,
161 call_stack: Vec::new(),
162 variables: IndexMap::new(),
163 step_mode: StepMode::Continue,
164 session_start: Utc::now(),
165 })),
166 breakpoints: Arc::new(RwLock::new(HashMap::new())),
167 execution_history: Arc::new(Mutex::new(VecDeque::new())),
168 current_step: Arc::new(Mutex::new(0)),
169 max_history_size: config.max_gradient_history, }
171 }
172
173 pub async fn start(&mut self) -> Result<()> {
175 let mut state = self.state.write();
176 state.is_running = true;
177 state.session_start = Utc::now();
178 tracing::info!("Interactive debugger started");
179 Ok(())
180 }
181
182 pub async fn process_command(&self, command: DebuggerCommand) -> Result<DebuggerResponse> {
184 match command {
185 DebuggerCommand::Start => {
186 let mut state = self.state.write();
187 state.is_running = true;
188 Ok(DebuggerResponse::Started)
189 },
190
191 DebuggerCommand::Pause => {
192 let mut state = self.state.write();
193 state.is_paused = true;
194 if let Some(location) = &state.current_location {
195 Ok(DebuggerResponse::Paused(location.clone()))
196 } else {
197 Ok(DebuggerResponse::Paused(DebugLocation {
198 module: "unknown".to_string(),
199 function: "unknown".to_string(),
200 line: None,
201 instruction: None,
202 context: None,
203 }))
204 }
205 },
206
207 DebuggerCommand::Resume => {
208 let mut state = self.state.write();
209 state.is_paused = false;
210 state.step_mode = StepMode::Continue;
211 Ok(DebuggerResponse::Resumed)
212 },
213
214 DebuggerCommand::Step(mode) => self.execute_step(mode).await,
215
216 DebuggerCommand::SetBreakpoint(location, condition) => {
217 self.set_breakpoint(location, condition).await
218 },
219
220 DebuggerCommand::RemoveBreakpoint(id) => self.remove_breakpoint(id).await,
221
222 DebuggerCommand::InspectVariable(name) => self.inspect_variable(&name).await,
223
224 DebuggerCommand::EvaluateExpression(expr) => self.evaluate_expression(&expr).await,
225
226 DebuggerCommand::ShowCallStack => {
227 let state = self.state.read();
228 Ok(DebuggerResponse::CallStackShown(state.call_stack.clone()))
229 },
230
231 DebuggerCommand::ShowHistory => {
232 let history = self.execution_history.lock();
233 Ok(DebuggerResponse::HistoryShown(
234 history.iter().cloned().collect(),
235 ))
236 },
237
238 DebuggerCommand::JumpToStep(step_num) => self.jump_to_step(step_num).await,
239
240 DebuggerCommand::Reset => self.reset().await,
241
242 DebuggerCommand::Exit => {
243 let mut state = self.state.write();
244 state.is_running = false;
245 Ok(DebuggerResponse::Reset)
246 },
247 }
248 }
249
250 async fn execute_step(&self, mode: StepMode) -> Result<DebuggerResponse> {
252 let mut state = self.state.write();
253 state.step_mode = mode;
254 state.is_paused = true;
255
256 let snapshot = ExecutionSnapshot {
258 id: Uuid::new_v4(),
259 timestamp: Utc::now(),
260 step_number: {
261 let mut step = self.current_step.lock();
262 *step += 1;
263 *step
264 },
265 location: state.current_location.clone().unwrap_or_else(|| DebugLocation {
266 module: "runtime".to_string(),
267 function: "step".to_string(),
268 line: None,
269 instruction: Some(format!("Step {:?}", mode)),
270 context: None,
271 }),
272 call_stack: state.call_stack.clone(),
273 variables: state.variables.clone(),
274 memory_usage: None,
275 performance_metrics: HashMap::new(),
276 };
277
278 {
280 let mut history = self.execution_history.lock();
281 history.push_back(snapshot.clone());
282 if history.len() > self.max_history_size {
283 history.pop_front();
284 }
285 }
286
287 Ok(DebuggerResponse::Stepped(snapshot))
288 }
289
290 async fn set_breakpoint(
292 &self,
293 location: DebugLocation,
294 condition: Option<String>,
295 ) -> Result<DebuggerResponse> {
296 let breakpoint = Breakpoint {
297 id: Uuid::new_v4(),
298 location,
299 condition,
300 hit_count: 0,
301 enabled: true,
302 temporary: false,
303 log_message: None,
304 created_at: Utc::now(),
305 };
306
307 self.breakpoints.write().insert(breakpoint.id.to_string(), breakpoint.clone());
308
309 tracing::info!(
310 "Breakpoint set at {}::{}",
311 breakpoint.location.module,
312 breakpoint.location.function
313 );
314 Ok(DebuggerResponse::Started) }
316
317 async fn remove_breakpoint(&self, id: Uuid) -> Result<DebuggerResponse> {
319 if self.breakpoints.write().remove(&id.to_string()).is_some() {
320 tracing::info!("Breakpoint {} removed", id);
321 }
322 Ok(DebuggerResponse::Started)
323 }
324
325 async fn inspect_variable(&self, name: &str) -> Result<DebuggerResponse> {
327 let state = self.state.read();
328 if let Some(var) = state.variables.get(name) {
329 Ok(DebuggerResponse::VariableInspected(var.clone()))
330 } else {
331 Ok(DebuggerResponse::Error(format!(
332 "Variable '{}' not found",
333 name
334 )))
335 }
336 }
337
338 async fn evaluate_expression(&self, expr: &str) -> Result<DebuggerResponse> {
351 let variables = self.state.read().variables.clone();
352 match ExpressionEvaluator::new(expr, &variables).evaluate() {
353 Ok(value) => Ok(DebuggerResponse::ExpressionEvaluated(format_eval_result(
354 value,
355 ))),
356 Err(message) => Ok(DebuggerResponse::Error(message)),
357 }
358 }
359
360 async fn jump_to_step(&self, step_num: usize) -> Result<DebuggerResponse> {
362 let history = self.execution_history.lock();
363 if let Some(snapshot) = history.iter().find(|s| s.step_number == step_num) {
364 let snapshot = snapshot.clone();
365 drop(history);
366
367 {
369 let mut state = self.state.write();
370 state.current_location = Some(snapshot.location.clone());
371 state.call_stack = snapshot.call_stack.clone();
372 state.variables = snapshot.variables.clone();
373 state.is_paused = true;
374 }
375
376 *self.current_step.lock() = step_num;
377 Ok(DebuggerResponse::JumpedToStep(snapshot))
378 } else {
379 Ok(DebuggerResponse::Error(format!(
380 "Step {} not found in history",
381 step_num
382 )))
383 }
384 }
385
386 pub async fn reset(&self) -> Result<DebuggerResponse> {
388 {
389 let mut state = self.state.write();
390 *state = DebuggerState {
391 is_running: false,
392 is_paused: false,
393 current_location: None,
394 call_stack: Vec::new(),
395 variables: IndexMap::new(),
396 step_mode: StepMode::Continue,
397 session_start: Utc::now(),
398 };
399 }
400
401 self.breakpoints.write().clear();
402 self.execution_history.lock().clear();
403 *self.current_step.lock() = 0;
404
405 Ok(DebuggerResponse::Reset)
406 }
407
408 pub async fn add_variable(&self, name: String, value: String, type_name: String) -> Result<()> {
410 let var = VariableValue {
411 name: name.clone(),
412 value,
413 type_name,
414 size_bytes: None,
415 shape: None,
416 is_tensor: false,
417 metadata: HashMap::new(),
418 };
419
420 self.state.write().variables.insert(name, var);
421 Ok(())
422 }
423
424 pub async fn update_location(&self, location: DebugLocation) -> Result<()> {
426 let mut state = self.state.write();
427 state.current_location = Some(location.clone());
428
429 let breakpoints = self.breakpoints.read();
431 for (_, breakpoint) in breakpoints.iter() {
432 if breakpoint.enabled
433 && breakpoint.location.module == location.module
434 && breakpoint.location.function == location.function
435 {
436 state.is_paused = true;
437 tracing::info!(
438 "Breakpoint hit at {}::{}",
439 location.module,
440 location.function
441 );
442 break;
443 }
444 }
445
446 Ok(())
447 }
448
449 pub async fn push_frame(&self, location: DebugLocation) -> Result<()> {
451 let frame = StackFrame {
452 id: Uuid::new_v4(),
453 location,
454 locals: IndexMap::new(),
455 timestamp: Utc::now(),
456 depth: self.state.read().call_stack.len(),
457 };
458
459 self.state.write().call_stack.push(frame);
460 Ok(())
461 }
462
463 pub async fn pop_frame(&self) -> Result<Option<StackFrame>> {
465 Ok(self.state.write().call_stack.pop())
466 }
467
468 pub async fn generate_report(&self) -> Result<InteractiveDebuggerReport> {
470 let state = self.state.read();
471 let breakpoints = self.breakpoints.read();
472 let history = self.execution_history.lock();
473
474 Ok(InteractiveDebuggerReport {
475 session_duration: Utc::now() - state.session_start,
476 total_steps: *self.current_step.lock(),
477 total_breakpoints: breakpoints.len(),
478 breakpoint_hits: breakpoints.values().map(|b| b.hit_count).sum(),
479 max_call_stack_depth: state.call_stack.len(),
480 variables_tracked: state.variables.len(),
481 history_entries: history.len(),
482 current_state: state.clone(),
483 })
484 }
485
486 pub fn is_paused(&self) -> bool {
488 self.state.read().is_paused
489 }
490
491 pub fn current_step(&self) -> usize {
493 *self.current_step.lock()
494 }
495
496 pub fn get_breakpoints(&self) -> Vec<Breakpoint> {
498 self.breakpoints.read().values().cloned().collect()
499 }
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct InteractiveDebuggerReport {
505 pub session_duration: chrono::Duration,
506 pub total_steps: usize,
507 pub total_breakpoints: usize,
508 pub breakpoint_hits: usize,
509 pub max_call_stack_depth: usize,
510 pub variables_tracked: usize,
511 pub history_entries: usize,
512 pub current_state: DebuggerState,
513}
514
515impl Default for DebuggerState {
516 fn default() -> Self {
517 Self {
518 is_running: false,
519 is_paused: false,
520 current_location: None,
521 call_stack: Vec::new(),
522 variables: IndexMap::new(),
523 step_mode: StepMode::Continue,
524 session_start: Utc::now(),
525 }
526 }
527}
528
529fn format_eval_result(value: f64) -> String {
533 if value.fract() == 0.0 && value.is_finite() && value.abs() < 1e15 {
534 format!("{}", value as i64)
535 } else {
536 format!("{value}")
537 }
538}
539
540struct ExpressionEvaluator<'a> {
552 tokens: Vec<Token>,
553 pos: usize,
554 variables: &'a IndexMap<String, VariableValue>,
555 source: &'a str,
556}
557
558#[derive(Debug, Clone, PartialEq)]
559enum Token {
560 Number(f64),
561 Ident(String),
562 Plus,
563 Minus,
564 Star,
565 Slash,
566 LParen,
567 RParen,
568}
569
570impl<'a> ExpressionEvaluator<'a> {
571 fn new(expr: &'a str, variables: &'a IndexMap<String, VariableValue>) -> Self {
572 Self {
573 tokens: Vec::new(),
574 pos: 0,
575 variables,
576 source: expr,
577 }
578 }
579
580 fn evaluate(mut self) -> Result<f64, String> {
585 self.tokenize()?;
586 if self.tokens.is_empty() {
587 return Err("empty expression".to_string());
588 }
589 let value = self.parse_expr()?;
590 if self.pos != self.tokens.len() {
591 return Err(format!(
592 "unexpected trailing input in expression {:?} at token {}",
593 self.source, self.pos
594 ));
595 }
596 Ok(value)
597 }
598
599 fn tokenize(&mut self) -> Result<(), String> {
600 let chars: Vec<char> = self.source.chars().collect();
601 let mut i = 0;
602 while i < chars.len() {
603 let c = chars[i];
604 match c {
605 ' ' | '\t' | '\n' | '\r' => {
606 i += 1;
607 },
608 '+' => {
609 self.tokens.push(Token::Plus);
610 i += 1;
611 },
612 '-' => {
613 self.tokens.push(Token::Minus);
614 i += 1;
615 },
616 '*' => {
617 self.tokens.push(Token::Star);
618 i += 1;
619 },
620 '/' => {
621 self.tokens.push(Token::Slash);
622 i += 1;
623 },
624 '(' => {
625 self.tokens.push(Token::LParen);
626 i += 1;
627 },
628 ')' => {
629 self.tokens.push(Token::RParen);
630 i += 1;
631 },
632 c if c.is_ascii_digit() || c == '.' => {
633 let start = i;
634 while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
635 i += 1;
636 }
637 let text: String = chars[start..i].iter().collect();
638 let value = text.parse::<f64>().map_err(|_| {
639 format!("invalid number literal {text:?} in {:?}", self.source)
640 })?;
641 self.tokens.push(Token::Number(value));
642 },
643 c if c.is_alphabetic() || c == '_' => {
644 let start = i;
645 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
646 i += 1;
647 }
648 let text: String = chars[start..i].iter().collect();
649 self.tokens.push(Token::Ident(text));
650 },
651 other => {
652 return Err(format!(
653 "unsupported character {other:?} in expression {:?}",
654 self.source
655 ));
656 },
657 }
658 }
659 Ok(())
660 }
661
662 fn peek(&self) -> Option<&Token> {
663 self.tokens.get(self.pos)
664 }
665
666 fn advance(&mut self) -> Option<Token> {
667 let tok = self.tokens.get(self.pos).cloned();
668 if tok.is_some() {
669 self.pos += 1;
670 }
671 tok
672 }
673
674 fn parse_expr(&mut self) -> Result<f64, String> {
675 let mut value = self.parse_term()?;
676 loop {
677 match self.peek() {
678 Some(Token::Plus) => {
679 self.advance();
680 value += self.parse_term()?;
681 },
682 Some(Token::Minus) => {
683 self.advance();
684 value -= self.parse_term()?;
685 },
686 _ => break,
687 }
688 }
689 Ok(value)
690 }
691
692 fn parse_term(&mut self) -> Result<f64, String> {
693 let mut value = self.parse_unary()?;
694 loop {
695 match self.peek() {
696 Some(Token::Star) => {
697 self.advance();
698 value *= self.parse_unary()?;
699 },
700 Some(Token::Slash) => {
701 self.advance();
702 let divisor = self.parse_unary()?;
703 if divisor == 0.0 {
704 return Err(format!("division by zero in expression {:?}", self.source));
705 }
706 value /= divisor;
707 },
708 _ => break,
709 }
710 }
711 Ok(value)
712 }
713
714 fn parse_unary(&mut self) -> Result<f64, String> {
715 if matches!(self.peek(), Some(Token::Minus)) {
716 self.advance();
717 return Ok(-self.parse_unary()?);
718 }
719 self.parse_atom()
720 }
721
722 fn parse_atom(&mut self) -> Result<f64, String> {
723 match self.advance() {
724 Some(Token::Number(n)) => Ok(n),
725 Some(Token::Ident(name)) => {
726 let var = self.variables.get(&name).ok_or_else(|| {
727 format!(
728 "unknown variable {name:?} referenced in expression {:?}",
729 self.source
730 )
731 })?;
732 var.value.trim().parse::<f64>().map_err(|_| {
733 format!(
734 "variable {name:?} has non-numeric value {:?} and cannot be used in an \
735 arithmetic expression",
736 var.value
737 )
738 })
739 },
740 Some(Token::LParen) => {
741 let value = self.parse_expr()?;
742 match self.advance() {
743 Some(Token::RParen) => Ok(value),
744 _ => Err(format!(
745 "missing closing ')' in expression {:?}",
746 self.source
747 )),
748 }
749 },
750 other => Err(format!(
751 "unexpected token {other:?} in expression {:?}",
752 self.source
753 )),
754 }
755 }
756}
757
758#[cfg(test)]
759mod expression_evaluator_tests {
760 use super::*;
761
762 fn var(name: &str, value: &str) -> VariableValue {
763 VariableValue {
764 name: name.to_string(),
765 value: value.to_string(),
766 type_name: "f64".to_string(),
767 size_bytes: None,
768 shape: None,
769 is_tensor: false,
770 metadata: HashMap::new(),
771 }
772 }
773
774 #[test]
775 fn test_numeric_literal() {
776 let vars = IndexMap::new();
777 let result = ExpressionEvaluator::new("42", &vars).evaluate();
778 assert_eq!(result, Ok(42.0));
779 }
780
781 #[test]
782 fn test_basic_arithmetic_precedence() {
783 let vars = IndexMap::new();
784 assert_eq!(
786 ExpressionEvaluator::new("2 + 3 * 4", &vars).evaluate(),
787 Ok(14.0)
788 );
789 }
790
791 #[test]
792 fn test_parentheses_override_precedence() {
793 let vars = IndexMap::new();
794 assert_eq!(
795 ExpressionEvaluator::new("(2 + 3) * 4", &vars).evaluate(),
796 Ok(20.0)
797 );
798 }
799
800 #[test]
801 fn test_unary_minus() {
802 let vars = IndexMap::new();
803 assert_eq!(
804 ExpressionEvaluator::new("-5 + 3", &vars).evaluate(),
805 Ok(-2.0)
806 );
807 }
808
809 #[test]
810 fn test_variable_lookup_resolves_real_value() {
811 let mut vars = IndexMap::new();
812 vars.insert("loss".to_string(), var("loss", "0.485"));
813 let result = ExpressionEvaluator::new("loss", &vars).evaluate();
814 assert_eq!(result, Ok(0.485));
815 }
816
817 #[test]
818 fn test_variable_in_arithmetic_expression() {
819 let mut vars = IndexMap::new();
820 vars.insert("grad_norm".to_string(), var("grad_norm", "2.5"));
821 let result = ExpressionEvaluator::new("grad_norm * 2", &vars).evaluate();
822 assert_eq!(result, Ok(5.0));
823 }
824
825 #[test]
826 fn test_unknown_variable_is_a_real_error_not_a_placeholder() {
827 let vars = IndexMap::new();
828 let result = ExpressionEvaluator::new("undefined_var", &vars).evaluate();
829 assert!(result.is_err());
830 assert!(result.expect_err("should be an error").contains("unknown variable"));
831 }
832
833 #[test]
834 fn test_non_numeric_variable_is_a_real_error() {
835 let mut vars = IndexMap::new();
836 vars.insert("model_name".to_string(), var("model_name", "gpt2"));
837 let result = ExpressionEvaluator::new("model_name", &vars).evaluate();
838 assert!(result.is_err());
839 assert!(result.expect_err("should be an error").contains("non-numeric"));
840 }
841
842 #[test]
843 fn test_division_by_zero_is_a_real_error() {
844 let vars = IndexMap::new();
845 let result = ExpressionEvaluator::new("1 / 0", &vars).evaluate();
846 assert!(result.is_err());
847 assert!(result.expect_err("should be an error").contains("division by zero"));
848 }
849
850 #[test]
851 fn test_malformed_expression_is_a_real_error() {
852 let vars = IndexMap::new();
853 assert!(ExpressionEvaluator::new("(1 + 2", &vars).evaluate().is_err());
854 assert!(ExpressionEvaluator::new("1 +", &vars).evaluate().is_err());
855 assert!(ExpressionEvaluator::new("1 2", &vars).evaluate().is_err());
856 assert!(ExpressionEvaluator::new("1 $ 2", &vars).evaluate().is_err());
857 }
858
859 #[test]
860 fn test_format_eval_result_integral_vs_fractional() {
861 assert_eq!(format_eval_result(3.0), "3");
862 assert_eq!(format_eval_result(3.5), "3.5");
863 assert_eq!(format_eval_result(-2.0), "-2");
864 }
865}
866
867#[cfg(test)]
868mod evaluate_expression_integration_tests {
869 use super::*;
870
871 fn make_debug_config() -> DebugConfig {
872 DebugConfig::default()
873 }
874
875 #[tokio::test]
876 async fn test_process_command_returns_error_for_unknown_variable_not_fake_success() {
877 let debugger = InteractiveDebugger::new(&make_debug_config());
878 let response = debugger
879 .process_command(DebuggerCommand::EvaluateExpression(
880 "nonexistent".to_string(),
881 ))
882 .await
883 .expect("process_command should not itself error");
884
885 match response {
889 DebuggerResponse::Error(message) => {
890 assert!(message.contains("unknown variable"));
891 },
892 other => panic!("expected DebuggerResponse::Error, got {other:?}"),
893 }
894 }
895
896 #[tokio::test]
897 async fn test_process_command_evaluates_real_arithmetic() {
898 let debugger = InteractiveDebugger::new(&make_debug_config());
899 let response = debugger
900 .process_command(DebuggerCommand::EvaluateExpression(
901 "(2 + 3) * 4".to_string(),
902 ))
903 .await
904 .expect("process_command should not error");
905
906 match response {
907 DebuggerResponse::ExpressionEvaluated(value) => assert_eq!(value, "20"),
908 other => panic!("expected DebuggerResponse::ExpressionEvaluated, got {other:?}"),
909 }
910 }
911
912 #[tokio::test]
913 async fn test_process_command_evaluates_expression_over_real_tracked_variable() {
914 let debugger = InteractiveDebugger::new(&make_debug_config());
915 {
916 let mut state = debugger.state.write();
917 state.variables.insert(
918 "batch_size".to_string(),
919 VariableValue {
920 name: "batch_size".to_string(),
921 value: "32".to_string(),
922 type_name: "usize".to_string(),
923 size_bytes: None,
924 shape: None,
925 is_tensor: false,
926 metadata: HashMap::new(),
927 },
928 );
929 }
930
931 let response = debugger
932 .process_command(DebuggerCommand::EvaluateExpression(
933 "batch_size * 2".to_string(),
934 ))
935 .await
936 .expect("process_command should not error");
937
938 match response {
939 DebuggerResponse::ExpressionEvaluated(value) => assert_eq!(value, "64"),
940 other => panic!("expected DebuggerResponse::ExpressionEvaluated, got {other:?}"),
941 }
942 }
943}