1use std::collections::HashMap;
13use std::sync::Arc;
14
15use super::compiler::{Chunk, Compiler, Instruction};
16use super::parser::Parser;
17use super::vm::{ScriptError, Table, Value, Vm};
18
19#[derive(Debug, Clone, PartialEq)]
23pub enum BreakpointKind {
24 Line(usize),
26 Function(String),
28 Conditional(String),
30 Exception,
32}
33
34#[derive(Debug, Clone)]
38pub struct Breakpoint {
39 pub id: u32,
40 pub kind: BreakpointKind,
41 pub enabled: bool,
42 pub hit_count: u32,
44 pub ignore_count: u32,
46}
47
48impl Breakpoint {
49 fn new(id: u32, kind: BreakpointKind) -> Self {
50 Breakpoint { id, kind, enabled: true, hit_count: 0, ignore_count: 0 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
58pub enum StepMode {
59 None,
60 StepIn,
61 StepOver,
62 StepOut,
63 Continue,
64}
65
66#[derive(Debug)]
70pub struct DebuggerState {
71 pub breakpoints: HashMap<u32, Breakpoint>,
72 pub step_mode: StepMode,
73 pub current_depth: usize,
74 pub step_depth_target: usize,
75 pub watch_expressions: Vec<String>,
76 next_bp_id: u32,
77 pub paused: bool,
78 pub pause_reason: String,
79}
80
81impl DebuggerState {
82 pub fn new() -> Self {
83 DebuggerState {
84 breakpoints: HashMap::new(),
85 step_mode: StepMode::Continue,
86 current_depth: 0,
87 step_depth_target: 0,
88 watch_expressions: Vec::new(),
89 next_bp_id: 1,
90 paused: false,
91 pause_reason: String::new(),
92 }
93 }
94
95 pub fn add_breakpoint(&mut self, kind: BreakpointKind) -> u32 {
97 let id = self.next_bp_id;
98 self.next_bp_id += 1;
99 self.breakpoints.insert(id, Breakpoint::new(id, kind));
100 id
101 }
102
103 pub fn remove_breakpoint(&mut self, id: u32) -> bool {
104 self.breakpoints.remove(&id).is_some()
105 }
106
107 pub fn enable_breakpoint(&mut self, id: u32) {
108 if let Some(bp) = self.breakpoints.get_mut(&id) { bp.enabled = true; }
109 }
110
111 pub fn disable_breakpoint(&mut self, id: u32) {
112 if let Some(bp) = self.breakpoints.get_mut(&id) { bp.enabled = false; }
113 }
114
115 pub fn add_watch(&mut self, expr: String) {
116 self.watch_expressions.push(expr);
117 }
118
119 pub fn remove_watch(&mut self, idx: usize) {
120 if idx < self.watch_expressions.len() {
121 self.watch_expressions.remove(idx);
122 }
123 }
124
125 fn should_pause(&mut self, ip: usize, chunk_name: &str) -> bool {
128 match self.step_mode {
129 StepMode::StepIn => {
130 self.paused = true;
131 self.pause_reason = format!("step-in at {}:{}", chunk_name, ip);
132 return true;
133 }
134 StepMode::StepOver => {
135 if self.current_depth <= self.step_depth_target {
136 self.paused = true;
137 self.pause_reason = format!("step-over at {}:{}", chunk_name, ip);
138 return true;
139 }
140 }
141 StepMode::StepOut => {
142 if self.current_depth < self.step_depth_target {
143 self.paused = true;
144 self.pause_reason = format!("step-out at {}:{}", chunk_name, ip);
145 return true;
146 }
147 }
148 StepMode::Continue => {}
149 StepMode::None => {}
150 }
151
152 for bp in self.breakpoints.values_mut() {
153 if !bp.enabled { continue; }
154 let fires = match &bp.kind {
155 BreakpointKind::Line(line) => *line == ip,
156 BreakpointKind::Function(name) => chunk_name.contains(name.as_str()),
157 BreakpointKind::Conditional(_) => false, BreakpointKind::Exception => false, };
160 if fires {
161 bp.hit_count += 1;
162 if bp.hit_count > bp.ignore_count {
163 self.paused = true;
164 self.pause_reason = format!("breakpoint {} at {}:{}", bp.id, chunk_name, ip);
165 return true;
166 }
167 }
168 }
169 false
170 }
171}
172
173impl Default for DebuggerState {
174 fn default() -> Self { Self::new() }
175}
176
177pub struct ScriptDebugger {
181 pub state: DebuggerState,
182 pub coverage: CoverageTracker,
183 pub pause_events: Vec<(usize, String)>,
185}
186
187impl ScriptDebugger {
188 pub fn new() -> Self {
189 ScriptDebugger {
190 state: DebuggerState::new(),
191 coverage: CoverageTracker::new(),
192 pause_events: Vec::new(),
193 }
194 }
195
196 pub fn execute(&mut self, vm: &mut Vm, chunk: Arc<Chunk>) -> Result<Vec<Value>, ScriptError> {
201 self.coverage.register_chunk(&chunk);
202 let result = vm.execute(Arc::clone(&chunk))?;
204 Ok(result)
205 }
206
207 pub fn pre_instruction(&mut self, ip: usize, chunk_name: &str, depth: usize) -> bool {
210 self.state.current_depth = depth;
211 self.coverage.mark(chunk_name, ip);
212 if self.state.should_pause(ip, chunk_name) {
213 self.pause_events.push((ip, chunk_name.to_string()));
214 self.state.step_mode = StepMode::Continue;
216 return true;
217 }
218 false
219 }
220
221 pub fn step_in(&mut self) {
222 self.state.step_mode = StepMode::StepIn;
223 self.state.paused = false;
224 }
225
226 pub fn step_over(&mut self, current_depth: usize) {
227 self.state.step_mode = StepMode::StepOver;
228 self.state.step_depth_target = current_depth;
229 self.state.paused = false;
230 }
231
232 pub fn step_out(&mut self, current_depth: usize) {
233 self.state.step_mode = StepMode::StepOut;
234 self.state.step_depth_target = current_depth;
235 self.state.paused = false;
236 }
237
238 pub fn resume(&mut self) {
239 self.state.step_mode = StepMode::Continue;
240 self.state.paused = false;
241 }
242}
243
244impl Default for ScriptDebugger {
245 fn default() -> Self { Self::new() }
246}
247
248pub struct LocalInspector;
252
253impl LocalInspector {
254 pub fn format_value(v: &Value, depth: usize) -> String {
256 match v {
257 Value::Nil => "nil".to_string(),
258 Value::Bool(b) => b.to_string(),
259 Value::Int(n) => n.to_string(),
260 Value::Float(f) => format!("{:.6}", f),
261 Value::Str(s) => format!("\"{}\"", s.replace('"', "\\\"")),
262 Value::Function(f) => format!("function<{}>", f.chunk.name),
263 Value::NativeFunction(f)=> format!("function<{}>", f.name),
264 Value::Table(t) if depth == 0 => format!("table({} entries)", t.length()),
265 Value::Table(t) => {
266 let mut out = String::from("{");
267 let mut key = Value::Nil;
268 let mut count = 0;
269 loop {
270 match t.next(&key) {
271 Some((k, val)) => {
272 if count > 0 { out.push_str(", "); }
273 if count >= 8 { out.push_str("..."); break; }
274 out.push_str(&format!("[{}]={}", Self::format_value(&k, 0), Self::format_value(&val, depth - 1)));
275 key = k; count += 1;
276 }
277 None => break,
278 }
279 }
280 out.push('}');
281 out
282 }
283 }
284 }
285
286 pub fn inspect(locals: &[(&str, Value)]) -> Vec<(String, String)> {
288 locals.iter().map(|(name, val)| {
289 (name.to_string(), Self::format_value(val, 4))
290 }).collect()
291 }
292
293 pub fn inspect_globals(vm: &Vm) -> Vec<(String, String)> {
295 let mut out = Vec::new();
296 for name in &["math", "string", "table", "io", "os", "bit", "print", "type", "pcall"] {
298 let v = vm.get_global(name);
299 if !matches!(v, Value::Nil) {
300 out.push((name.to_string(), Self::format_value(&v, 0)));
301 }
302 }
303 out
304 }
305}
306
307#[derive(Debug, Clone)]
311pub struct FrameInfo {
312 pub name: String,
313 pub ip: usize,
314 pub depth: usize,
315}
316
317pub struct CallStackTrace {
319 pub frames: Vec<FrameInfo>,
320}
321
322impl CallStackTrace {
323 pub fn new() -> Self { CallStackTrace { frames: Vec::new() } }
324
325 pub fn push(&mut self, name: impl Into<String>, ip: usize, depth: usize) {
326 self.frames.push(FrameInfo { name: name.into(), ip, depth });
327 }
328
329 pub fn format(&self) -> String {
331 let mut out = String::new();
332 for (i, f) in self.frames.iter().enumerate() {
333 out.push_str(&format!(" #{:<3} {} (instruction {})\n", i, f.name, f.ip));
334 }
335 if out.is_empty() { out = " (empty stack)\n".to_string(); }
336 out
337 }
338}
339
340impl Default for CallStackTrace {
341 fn default() -> Self { Self::new() }
342}
343
344pub struct WatchExpression {
348 pub expression: String,
349}
350
351impl WatchExpression {
352 pub fn new(expr: impl Into<String>) -> Self {
353 WatchExpression { expression: expr.into() }
354 }
355
356 pub fn evaluate(&self, vm: &mut Vm) -> String {
359 let src = format!("return {}", self.expression);
360 match Parser::from_source("<watch>", &src) {
361 Ok(script) => {
362 let chunk = Compiler::compile_script(&script);
363 match vm.execute(chunk) {
364 Ok(vals) => {
365 let parts: Vec<String> = vals.iter()
366 .map(|v| LocalInspector::format_value(v, 4))
367 .collect();
368 if parts.is_empty() { "(no value)".to_string() }
369 else { parts.join(", ") }
370 }
371 Err(e) => format!("error: {}", e.message),
372 }
373 }
374 Err(e) => format!("parse error: {}", e),
375 }
376 }
377}
378
379#[derive(Debug, Clone, PartialEq)]
383pub enum DebugCommand {
384 Continue,
385 StepIn,
386 StepOver,
387 StepOut,
388 SetBreakpoint(BreakpointKind),
390 RemoveBreakpoint(u32),
392 ListBreakpoints,
393 Evaluate(String),
395 PrintLocals,
396 PrintStack,
397 PrintGlobals,
398 AddWatch(String),
400 RemoveWatch(usize),
402 ListWatches,
403 EvalWatches,
405 Coverage,
407 Help,
408 Unknown(String),
409}
410
411impl DebugCommand {
412 pub fn parse(line: &str) -> Self {
414 let line = line.trim();
415 let (cmd, rest) = line.split_once(' ').unwrap_or((line, ""));
416 let rest = rest.trim();
417 match cmd {
418 "c" | "continue" => DebugCommand::Continue,
419 "si" | "stepin" => DebugCommand::StepIn,
420 "so" | "stepover" | "n" => DebugCommand::StepOver,
421 "sout" | "stepout" => DebugCommand::StepOut,
422 "bp" | "break" => {
423 if let Ok(n) = rest.parse::<usize>() {
424 DebugCommand::SetBreakpoint(BreakpointKind::Line(n))
425 } else if rest.starts_with("fn:") {
426 DebugCommand::SetBreakpoint(BreakpointKind::Function(rest[3..].trim().to_string()))
427 } else if rest.starts_with("if:") {
428 DebugCommand::SetBreakpoint(BreakpointKind::Conditional(rest[3..].trim().to_string()))
429 } else if rest == "exception" {
430 DebugCommand::SetBreakpoint(BreakpointKind::Exception)
431 } else {
432 DebugCommand::Unknown(line.to_string())
433 }
434 }
435 "rbp" | "rmbreak" => {
436 if let Ok(id) = rest.parse::<u32>() {
437 DebugCommand::RemoveBreakpoint(id)
438 } else {
439 DebugCommand::Unknown(line.to_string())
440 }
441 }
442 "lbp" | "bplist" | "breakpoints" => DebugCommand::ListBreakpoints,
443 "e" | "eval" | "p" => DebugCommand::Evaluate(rest.to_string()),
444 "locals" => DebugCommand::PrintLocals,
445 "stack" | "bt" => DebugCommand::PrintStack,
446 "globals" => DebugCommand::PrintGlobals,
447 "watch" => DebugCommand::AddWatch(rest.to_string()),
448 "rmwatch" => {
449 if let Ok(n) = rest.parse::<usize>() {
450 DebugCommand::RemoveWatch(n)
451 } else {
452 DebugCommand::Unknown(line.to_string())
453 }
454 }
455 "watches" => DebugCommand::ListWatches,
456 "evwatches" => DebugCommand::EvalWatches,
457 "coverage" | "cov" => DebugCommand::Coverage,
458 "h" | "help" | "?" => DebugCommand::Help,
459 _ => DebugCommand::Unknown(line.to_string()),
460 }
461 }
462}
463
464pub struct DebugSession {
469 pub debugger: ScriptDebugger,
470 pub call_stack: CallStackTrace,
472 pub locals: Vec<(String, Value)>,
474}
475
476impl DebugSession {
477 pub fn new() -> Self {
478 DebugSession {
479 debugger: ScriptDebugger::new(),
480 call_stack: CallStackTrace::new(),
481 locals: Vec::new(),
482 }
483 }
484
485 pub fn execute_command(&mut self, vm: &mut Vm, input: &str) -> String {
487 let cmd = DebugCommand::parse(input);
488 match cmd {
489 DebugCommand::Continue => {
490 self.debugger.resume();
491 "Continuing execution.\n".to_string()
492 }
493 DebugCommand::StepIn => {
494 self.debugger.step_in();
495 "Step-in mode set.\n".to_string()
496 }
497 DebugCommand::StepOver => {
498 let d = self.debugger.state.current_depth;
499 self.debugger.step_over(d);
500 "Step-over mode set.\n".to_string()
501 }
502 DebugCommand::StepOut => {
503 let d = self.debugger.state.current_depth;
504 self.debugger.step_out(d);
505 "Step-out mode set.\n".to_string()
506 }
507 DebugCommand::SetBreakpoint(kind) => {
508 let id = self.debugger.state.add_breakpoint(kind.clone());
509 format!("Breakpoint {} set: {:?}\n", id, kind)
510 }
511 DebugCommand::RemoveBreakpoint(id) => {
512 if self.debugger.state.remove_breakpoint(id) {
513 format!("Breakpoint {} removed.\n", id)
514 } else {
515 format!("Breakpoint {} not found.\n", id)
516 }
517 }
518 DebugCommand::ListBreakpoints => {
519 if self.debugger.state.breakpoints.is_empty() {
520 "No breakpoints.\n".to_string()
521 } else {
522 let mut ids: Vec<u32> = self.debugger.state.breakpoints.keys().cloned().collect();
523 ids.sort();
524 let mut out = String::new();
525 for id in ids {
526 let bp = &self.debugger.state.breakpoints[&id];
527 out.push_str(&format!(
528 " #{} {:?} enabled={} hits={} ignore={}\n",
529 bp.id, bp.kind, bp.enabled, bp.hit_count, bp.ignore_count
530 ));
531 }
532 out
533 }
534 }
535 DebugCommand::Evaluate(expr) => {
536 if expr.is_empty() {
537 "Usage: eval <expression>\n".to_string()
538 } else {
539 let mut w = WatchExpression::new(&expr);
540 let result = w.evaluate(vm);
541 format!("= {}\n", result)
542 }
543 }
544 DebugCommand::PrintLocals => {
545 if self.locals.is_empty() {
546 "(no locals in current scope)\n".to_string()
547 } else {
548 let pairs: Vec<(&str, Value)> = self.locals.iter()
549 .map(|(k, v)| (k.as_str(), v.clone()))
550 .collect();
551 let inspected = LocalInspector::inspect(&pairs);
552 let mut out = String::new();
553 for (k, v) in inspected {
554 out.push_str(&format!(" {} = {}\n", k, v));
555 }
556 out
557 }
558 }
559 DebugCommand::PrintStack => {
560 self.call_stack.format()
561 }
562 DebugCommand::PrintGlobals => {
563 let globals = LocalInspector::inspect_globals(vm);
564 let mut out = String::new();
565 for (k, v) in globals {
566 out.push_str(&format!(" {} = {}\n", k, v));
567 }
568 if out.is_empty() { "(no globals)\n".to_string() } else { out }
569 }
570 DebugCommand::AddWatch(expr) => {
571 let idx = self.debugger.state.watch_expressions.len();
572 self.debugger.state.add_watch(expr.clone());
573 format!("Watch #{} added: {}\n", idx, expr)
574 }
575 DebugCommand::RemoveWatch(idx) => {
576 let len = self.debugger.state.watch_expressions.len();
577 if idx < len {
578 let expr = self.debugger.state.watch_expressions.remove(idx);
579 format!("Watch #{} ({}) removed.\n", idx, expr)
580 } else {
581 format!("Watch #{} not found.\n", idx)
582 }
583 }
584 DebugCommand::ListWatches => {
585 if self.debugger.state.watch_expressions.is_empty() {
586 "No watches.\n".to_string()
587 } else {
588 let mut out = String::new();
589 for (i, w) in self.debugger.state.watch_expressions.iter().enumerate() {
590 out.push_str(&format!(" #{}: {}\n", i, w));
591 }
592 out
593 }
594 }
595 DebugCommand::EvalWatches => {
596 let exprs: Vec<String> = self.debugger.state.watch_expressions.clone();
597 if exprs.is_empty() {
598 "No watches to evaluate.\n".to_string()
599 } else {
600 let mut out = String::new();
601 for (i, expr) in exprs.iter().enumerate() {
602 let mut w = WatchExpression::new(expr);
603 let result = w.evaluate(vm);
604 out.push_str(&format!(" #{} {} = {}\n", i, expr, result));
605 }
606 out
607 }
608 }
609 DebugCommand::Coverage => {
610 self.debugger.coverage.report()
611 }
612 DebugCommand::Help => {
613 concat!(
614 "Debug commands:\n",
615 " c / continue Resume execution\n",
616 " si / stepin Step into next instruction\n",
617 " n / stepover Step over (stay at same depth)\n",
618 " sout / stepout Step out of current function\n",
619 " bp <n> Set line breakpoint at instruction n\n",
620 " bp fn:<name> Set function breakpoint\n",
621 " bp if:<expr> Set conditional breakpoint\n",
622 " bp exception Break on exceptions\n",
623 " rbp <id> Remove breakpoint\n",
624 " bplist List breakpoints\n",
625 " e / eval <expr> Evaluate expression\n",
626 " locals Print locals\n",
627 " bt / stack Print call stack\n",
628 " globals Print known globals\n",
629 " watch <expr> Add watch expression\n",
630 " rmwatch <n> Remove watch\n",
631 " watches List watches\n",
632 " evwatches Evaluate all watches\n",
633 " coverage Show coverage report\n",
634 " h / help This help\n",
635 ).to_string()
636 }
637 DebugCommand::Unknown(s) => {
638 format!("Unknown command: {:?}. Type 'help' for commands.\n", s)
639 }
640 }
641 }
642}
643
644impl Default for DebugSession {
645 fn default() -> Self { Self::new() }
646}
647
648pub struct CoverageTracker {
652 chunks: HashMap<String, Vec<u8>>, chunk_sizes: HashMap<String, usize>,
655}
656
657impl CoverageTracker {
658 pub fn new() -> Self {
659 CoverageTracker {
660 chunks: HashMap::new(),
661 chunk_sizes: HashMap::new(),
662 }
663 }
664
665 pub fn register_chunk(&mut self, chunk: &Chunk) {
667 let name = chunk.name.clone();
668 let size = chunk.instructions.len();
669 if !self.chunk_sizes.contains_key(&name) {
670 self.chunk_sizes.insert(name.clone(), size);
671 let byte_len = (size + 7) / 8;
672 self.chunks.insert(name, vec![0u8; byte_len]);
673 }
674 for sub in &chunk.sub_chunks {
676 self.register_chunk(sub);
677 }
678 }
679
680 pub fn mark(&mut self, name: &str, ip: usize) {
682 if let Some(bits) = self.chunks.get_mut(name) {
683 let byte_idx = ip / 8;
684 let bit_idx = ip % 8;
685 if byte_idx < bits.len() {
686 bits[byte_idx] |= 1 << bit_idx;
687 }
688 }
689 }
690
691 pub fn is_covered(&self, name: &str, ip: usize) -> bool {
693 self.chunks.get(name).map(|bits| {
694 let byte_idx = ip / 8;
695 let bit_idx = ip % 8;
696 byte_idx < bits.len() && (bits[byte_idx] & (1 << bit_idx)) != 0
697 }).unwrap_or(false)
698 }
699
700 pub fn coverage_percent(&self, name: &str) -> f64 {
702 let size = match self.chunk_sizes.get(name) {
703 Some(&s) => s,
704 None => return 0.0,
705 };
706 if size == 0 { return 100.0; }
707 let covered = (0..size).filter(|&ip| self.is_covered(name, ip)).count();
708 (covered as f64 / size as f64) * 100.0
709 }
710
711 pub fn uncovered_lines(&self, name: &str) -> Vec<usize> {
713 let size = self.chunk_sizes.get(name).copied().unwrap_or(0);
714 (0..size).filter(|&ip| !self.is_covered(name, ip)).collect()
715 }
716
717 pub fn report(&self) -> String {
719 if self.chunk_sizes.is_empty() {
720 return "No coverage data.\n".to_string();
721 }
722 let mut out = String::new();
723 let mut names: Vec<&String> = self.chunk_sizes.keys().collect();
724 names.sort();
725 for name in names {
726 let pct = self.coverage_percent(name);
727 let size = self.chunk_sizes[name];
728 let uncov = self.uncovered_lines(name);
729 out.push_str(&format!(" {} : {:.1}% ({}/{} instructions)\n", name, pct, size - uncov.len(), size));
730 if !uncov.is_empty() {
731 let uc_str: Vec<String> = uncov.iter().map(|n| n.to_string()).collect();
732 out.push_str(&format!(" uncovered: {}\n", uc_str.join(", ")));
733 }
734 }
735 out
736 }
737}
738
739impl Default for CoverageTracker {
740 fn default() -> Self { Self::new() }
741}
742
743#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::scripting::{compiler::Compiler, parser::Parser};
749 use crate::scripting::stdlib::register_all;
750
751 fn make_vm() -> Vm {
752 let mut vm = Vm::new();
753 register_all(&mut vm);
754 vm
755 }
756
757 fn compile(src: &str) -> Arc<Chunk> {
758 let script = Parser::from_source("test", src).expect("parse");
759 Compiler::compile_script(&script)
760 }
761
762 #[test]
763 fn test_breakpoint_add_remove() {
764 let mut state = DebuggerState::new();
765 let id = state.add_breakpoint(BreakpointKind::Line(5));
766 assert!(state.breakpoints.contains_key(&id));
767 assert!(state.remove_breakpoint(id));
768 assert!(!state.breakpoints.contains_key(&id));
769 }
770
771 #[test]
772 fn test_breakpoint_line_fires() {
773 let mut state = DebuggerState::new();
774 state.add_breakpoint(BreakpointKind::Line(3));
775 state.step_mode = StepMode::Continue;
776 assert!(!state.should_pause(0, "test"));
777 assert!(!state.should_pause(1, "test"));
778 assert!(!state.should_pause(2, "test"));
779 assert!(state.should_pause(3, "test"));
780 }
781
782 #[test]
783 fn test_step_in_pauses_immediately() {
784 let mut state = DebuggerState::new();
785 state.step_mode = StepMode::StepIn;
786 assert!(state.should_pause(0, "main"));
787 }
788
789 #[test]
790 fn test_step_over_pauses_at_same_depth() {
791 let mut state = DebuggerState::new();
792 state.step_mode = StepMode::StepOver;
793 state.step_depth_target = 2;
794 state.current_depth = 3;
795 assert!(!state.should_pause(0, "inner"));
797 state.paused = false;
799 state.current_depth = 2;
800 assert!(state.should_pause(1, "main"));
801 }
802
803 #[test]
804 fn test_debugger_coverage_marks() {
805 let mut tracker = CoverageTracker::new();
806 tracker.chunk_sizes.insert("main".to_string(), 10);
807 tracker.chunks.insert("main".to_string(), vec![0u8; 2]);
808 tracker.mark("main", 0);
809 tracker.mark("main", 1);
810 tracker.mark("main", 9);
811 assert!(tracker.is_covered("main", 0));
812 assert!(tracker.is_covered("main", 9));
813 assert!(!tracker.is_covered("main", 5));
814 }
815
816 #[test]
817 fn test_coverage_percent() {
818 let mut tracker = CoverageTracker::new();
819 tracker.chunk_sizes.insert("f".to_string(), 4);
820 tracker.chunks.insert("f".to_string(), vec![0u8; 1]);
821 tracker.mark("f", 0);
822 tracker.mark("f", 1);
823 let pct = tracker.coverage_percent("f");
824 assert!((pct - 50.0).abs() < 0.1);
825 }
826
827 #[test]
828 fn test_uncovered_lines() {
829 let mut tracker = CoverageTracker::new();
830 tracker.chunk_sizes.insert("g".to_string(), 3);
831 tracker.chunks.insert("g".to_string(), vec![0u8; 1]);
832 tracker.mark("g", 1);
833 let uc = tracker.uncovered_lines("g");
834 assert_eq!(uc, vec![0, 2]);
835 }
836
837 #[test]
838 fn test_debug_command_parse_continue() {
839 assert_eq!(DebugCommand::parse("c"), DebugCommand::Continue);
840 assert_eq!(DebugCommand::parse("continue"), DebugCommand::Continue);
841 }
842
843 #[test]
844 fn test_debug_command_parse_breakpoint() {
845 assert_eq!(DebugCommand::parse("bp 10"), DebugCommand::SetBreakpoint(BreakpointKind::Line(10)));
846 assert_eq!(DebugCommand::parse("bp fn:foo"), DebugCommand::SetBreakpoint(BreakpointKind::Function("foo".to_string())));
847 assert_eq!(DebugCommand::parse("bp exception"), DebugCommand::SetBreakpoint(BreakpointKind::Exception));
848 }
849
850 #[test]
851 fn test_debug_command_evaluate() {
852 assert_eq!(DebugCommand::parse("eval 1+2"), DebugCommand::Evaluate("1+2".to_string()));
853 }
854
855 #[test]
856 fn test_watch_expression_eval() {
857 let mut vm = make_vm();
858 let w = WatchExpression::new("1 + 2");
859 let result = w.evaluate(&mut vm);
860 assert_eq!(result, "3");
861 }
862
863 #[test]
864 fn test_session_set_breakpoint() {
865 let mut vm = make_vm();
866 let mut session = DebugSession::new();
867 let resp = session.execute_command(&mut vm, "bp 5");
868 assert!(resp.contains("Breakpoint"));
869 assert!(!session.debugger.state.breakpoints.is_empty());
870 }
871
872 #[test]
873 fn test_session_eval() {
874 let mut vm = make_vm();
875 let mut session = DebugSession::new();
876 let resp = session.execute_command(&mut vm, "eval 10 * 3");
877 assert!(resp.contains("30"));
878 }
879
880 #[test]
881 fn test_format_value_table() {
882 let t = Table::new();
883 t.set(Value::Int(1), Value::Int(42));
884 let s = LocalInspector::format_value(&Value::Table(t), 1);
885 assert!(s.contains("42"));
886 }
887}