1use std::path::Path;
7use std::process::Stdio;
8
9use colored::Colorize;
10use tokio::process::Command as TokioCommand;
11
12use crate::cli::spawn::ensure_daemon_running;
13use crate::common::{Error, Result};
14use crate::ipc::protocol::{
15 BreakpointLocation, Command, EvaluateContext, EvaluateResult, StackFrameInfo,
16 StopResult, VariableInfo,
17};
18use crate::ipc::DaemonClient;
19
20use super::config::{
21 CommandExpectation, EvaluateExpectation, FrameAssertion, StopExpectation, TestScenario,
22 TestStep, VariableAssertion,
23};
24
25#[derive(Debug)]
27pub struct TestResult {
28 pub name: String,
29 pub passed: bool,
30 pub steps_run: usize,
31 pub steps_total: usize,
32 pub error: Option<String>,
33}
34
35pub async fn run_scenario(path: &Path, verbose: bool) -> Result<TestResult> {
37 let content = std::fs::read_to_string(path).map_err(|e| {
39 Error::Config(format!(
40 "Failed to read test scenario '{}': {}",
41 path.display(),
42 e
43 ))
44 })?;
45
46 let scenario: TestScenario = serde_yaml::from_str(&content)
47 .map_err(|e| Error::Config(format!("Failed to parse test scenario: {}", e)))?;
48
49 let steps_total = scenario.steps.len();
50
51 println!(
52 "\n{} {}",
53 "Running Test:".blue().bold(),
54 scenario.name.white().bold()
55 );
56
57 if let Some(desc) = &scenario.description {
58 println!(" {}", desc.dimmed());
59 }
60
61 if let Some(setup_steps) = &scenario.setup {
63 println!("\n{}", "Setup:".cyan());
64 for step in setup_steps {
65 if verbose {
66 println!(" $ {}", step.shell.dimmed());
67 }
68
69 let status = TokioCommand::new("sh")
70 .arg("-c")
71 .arg(&step.shell)
72 .stdin(Stdio::null())
73 .stdout(if verbose {
74 Stdio::inherit()
75 } else {
76 Stdio::null()
77 })
78 .stderr(if verbose {
79 Stdio::inherit()
80 } else {
81 Stdio::null()
82 })
83 .status()
84 .await
85 .map_err(|e| Error::Config(format!("Setup command failed to execute: {}", e)))?;
86
87 if !status.success() {
88 return Ok(TestResult {
89 name: scenario.name.clone(),
90 passed: false,
91 steps_run: 0,
92 steps_total,
93 error: Some(format!(
94 "Setup command '{}' failed with exit code {:?}",
95 step.shell,
96 status.code()
97 )),
98 });
99 }
100 println!(" {} {}", "✓".green(), step.shell.dimmed());
101 }
102 }
103
104 ensure_daemon_running().await?;
106 let mut client = DaemonClient::connect().await?;
107
108 let scenario_dir = path.parent().unwrap_or(Path::new("."));
110 let program_path = if scenario.target.program.is_relative() {
111 scenario_dir.join(&scenario.target.program)
112 } else {
113 scenario.target.program.clone()
114 };
115
116 if scenario.target.mode == "attach" {
118 let pid = if let Some(pid) = scenario.target.pid {
120 pid
121 } else if let Some(pid_file_path) = &scenario.target.pid_file {
122 let pid_file = if pid_file_path.is_relative() {
123 scenario_dir.join(pid_file_path)
124 } else {
125 pid_file_path.clone()
126 };
127
128 let pid_str = std::fs::read_to_string(&pid_file).map_err(|e| {
129 Error::Config(format!(
130 "Failed to read PID file '{}': {}",
131 pid_file.display(),
132 e
133 ))
134 })?;
135
136 pid_str.trim().parse::<u32>().map_err(|e| {
137 Error::Config(format!(
138 "Invalid PID in file '{}': {}",
139 pid_file.display(),
140 e
141 ))
142 })?
143 } else {
144 return Err(Error::Config(
145 "Attach mode requires either 'pid' or 'pid_file' field".to_string(),
146 ));
147 };
148
149 #[cfg(unix)]
151 {
152 let result = unsafe { libc::kill(pid as i32, 0) };
154 if result != 0 {
155 return Err(Error::Config(format!(
156 "Process with PID {} not found or not accessible",
157 pid
158 )));
159 }
160 }
161
162 println!("\n{}", "Attaching to process...".cyan());
163 client
164 .send_command(Command::Attach {
165 pid,
166 adapter: scenario.target.adapter.clone(),
167 })
168 .await?;
169
170 if verbose {
171 println!(" PID: {}", pid.to_string().dimmed());
172 if let Some(adapter) = &scenario.target.adapter {
173 println!(" Adapter: {}", adapter.dimmed());
174 }
175 }
176
177 println!(" {} Attached to process", "✓".green());
178 } else if scenario.target.mode != "launch" && scenario.target.mode != "launch" {
179 return Err(Error::Config(format!(
181 "Unknown target mode '{}'. Supported modes: 'launch', 'attach'",
182 scenario.target.mode
183 )));
184 } else {
185 let program_path = program_path.canonicalize().map_err(|e| {
187 Error::Config(format!(
188 "Program not found '{}': {}",
189 scenario.target.program.display(),
190 e
191 ))
192 })?;
193
194 println!("\n{}", "Starting debug session...".cyan());
195 client
196 .send_command(Command::Start {
197 program: program_path.clone(),
198 args: scenario.target.args.clone().unwrap_or_default(),
199 adapter: scenario.target.adapter.clone(),
200 stop_on_entry: scenario.target.stop_on_entry,
201 initial_breakpoints: Vec::new(),
202 })
203 .await?;
204
205 if verbose {
206 println!(
207 " Program: {}",
208 program_path.display().to_string().dimmed()
209 );
210 if let Some(adapter) = &scenario.target.adapter {
211 println!(" Adapter: {}", adapter.dimmed());
212 }
213 }
214
215 println!(" {} Session started", "✓".green());
216 }
217
218 println!("\n{}", "Steps:".cyan());
220
221 for (i, step) in scenario.steps.iter().enumerate() {
222 let step_num = i + 1;
223
224 match execute_step(&mut client, step, step_num, verbose).await {
225 Ok(()) => {
226 }
228 Err(e) => {
229 println!(" {} Step {}: {}", "✗".red(), step_num, e);
230
231 let _ = client.send_command(Command::Stop).await;
233
234 return Ok(TestResult {
235 name: scenario.name.clone(),
236 passed: false,
237 steps_run: step_num,
238 steps_total,
239 error: Some(e.to_string()),
240 });
241 }
242 }
243 }
244
245 let _ = client.send_command(Command::Stop).await;
247
248 println!(
249 "\n{} {}\n",
250 "✓".green().bold(),
251 "Test Passed".green().bold()
252 );
253
254 Ok(TestResult {
255 name: scenario.name,
256 passed: true,
257 steps_run: steps_total,
258 steps_total,
259 error: None,
260 })
261}
262
263async fn execute_step(
265 client: &mut DaemonClient,
266 step: &TestStep,
267 step_num: usize,
268 verbose: bool,
269) -> Result<()> {
270 match step {
271 TestStep::Command { command, expect } => {
272 execute_command_step(client, command, expect.as_ref(), step_num, verbose).await
273 }
274 TestStep::Await { timeout, expect } => {
275 execute_await_step(client, *timeout, expect.as_ref(), step_num, verbose).await
276 }
277 TestStep::InspectLocals { asserts } => {
278 execute_inspect_locals_step(client, asserts, step_num, verbose).await
279 }
280 TestStep::InspectStack { asserts } => {
281 execute_inspect_stack_step(client, asserts, step_num, verbose).await
282 }
283 TestStep::CheckOutput { contains, equals } => {
284 execute_check_output_step(client, contains.as_ref(), equals.as_ref(), step_num, verbose)
285 .await
286 }
287 TestStep::Evaluate { expression, expect } => {
288 execute_evaluate_step(client, expression, expect.as_ref(), step_num, verbose).await
289 }
290 }
291}
292
293async fn execute_command_step(
295 client: &mut DaemonClient,
296 command_str: &str,
297 expect: Option<&CommandExpectation>,
298 step_num: usize,
299 _verbose: bool,
300) -> Result<()> {
301 let cmd = parse_command(command_str)?;
302
303 let result = client.send_command(cmd).await;
304
305 if let Some(exp) = expect {
307 if let Some(should_succeed) = exp.success {
308 let did_succeed = result.is_ok();
309 if should_succeed != did_succeed {
310 return Err(Error::TestAssertion(format!(
311 "Command '{}' expected success={}, got success={}",
312 command_str, should_succeed, did_succeed
313 )));
314 }
315 }
316 }
317
318 if expect.map(|e| e.success == Some(false)).unwrap_or(false) {
320 println!(
321 " {} Step {}: {} (expected failure)",
322 "✓".green(),
323 step_num,
324 command_str.dimmed()
325 );
326 return Ok(());
327 }
328
329 result?;
330
331 println!(
332 " {} Step {}: {}",
333 "✓".green(),
334 step_num,
335 command_str.dimmed()
336 );
337
338 Ok(())
339}
340
341async fn execute_await_step(
343 client: &mut DaemonClient,
344 timeout: Option<u64>,
345 expect: Option<&StopExpectation>,
346 step_num: usize,
347 _verbose: bool,
348) -> Result<()> {
349 let timeout_secs = timeout.unwrap_or(30);
350
351 let result = client
352 .send_command(Command::Await { timeout_secs })
353 .await?;
354
355 let stop_result: StopResult = serde_json::from_value(result)
356 .map_err(|e| Error::TestAssertion(format!("Failed to parse stop result: {}", e)))?;
357
358 if let Some(exp) = expect {
360 if let Some(expected_reason) = &exp.reason {
361 if !stop_result.reason.contains(expected_reason) {
362 return Err(Error::TestAssertion(format!(
363 "Expected stop reason '{}', got '{}'",
364 expected_reason, stop_result.reason
365 )));
366 }
367 }
368
369 if let Some(expected_file) = &exp.file {
370 let actual_file = stop_result.source.as_deref().unwrap_or("");
371 if !actual_file.contains(expected_file) {
372 return Err(Error::TestAssertion(format!(
373 "Expected file '{}', got '{}'",
374 expected_file, actual_file
375 )));
376 }
377 }
378
379 if let Some(expected_line) = exp.line {
380 let actual_line = stop_result.line.unwrap_or(0);
381 if expected_line != actual_line {
382 return Err(Error::TestAssertion(format!(
383 "Expected line {}, got {}",
384 expected_line, actual_line
385 )));
386 }
387 }
388 }
389
390 let location = if let Some(source) = &stop_result.source {
391 if let Some(line) = stop_result.line {
392 format!("{}:{}", source, line)
393 } else {
394 source.clone()
395 }
396 } else {
397 "unknown location".to_string()
398 };
399
400 println!(
401 " {} Step {}: await ({} at {})",
402 "✓".green(),
403 step_num,
404 stop_result.reason.dimmed(),
405 location.dimmed()
406 );
407
408 Ok(())
409}
410
411async fn execute_inspect_locals_step(
413 client: &mut DaemonClient,
414 asserts: &[VariableAssertion],
415 step_num: usize,
416 _verbose: bool,
417) -> Result<()> {
418 let result = client
419 .send_command(Command::Locals { frame_id: None })
420 .await?;
421
422 let vars: Vec<VariableInfo> = serde_json::from_value(result["variables"].clone())
423 .map_err(|e| Error::TestAssertion(format!("Failed to parse variables: {}", e)))?;
424
425 for assertion in asserts {
426 let var = vars.iter().find(|v| v.name == assertion.name);
427
428 match var {
429 Some(v) => {
430 if let Some(expected_value) = &assertion.value {
432 if &v.value != expected_value {
433 return Err(Error::TestAssertion(format!(
434 "Variable '{}': expected value '{}', got '{}'",
435 assertion.name, expected_value, v.value
436 )));
437 }
438 }
439
440 if let Some(expected_substr) = &assertion.value_contains {
442 if !v.value.contains(expected_substr) {
443 return Err(Error::TestAssertion(format!(
444 "Variable '{}': expected value containing '{}', got '{}'",
445 assertion.name, expected_substr, v.value
446 )));
447 }
448 }
449
450 if let Some(expected_type) = &assertion.type_name {
452 let actual_type = v.type_name.as_deref().unwrap_or("");
453 if actual_type != expected_type {
454 return Err(Error::TestAssertion(format!(
455 "Variable '{}': expected type '{}', got '{}'",
456 assertion.name, expected_type, actual_type
457 )));
458 }
459 }
460 }
461 None => {
462 let available: Vec<&str> = vars.iter().map(|v| v.name.as_str()).collect();
463 return Err(Error::TestAssertion(format!(
464 "Variable '{}' not found. Available: {:?}",
465 assertion.name, available
466 )));
467 }
468 }
469 }
470
471 let checked: Vec<&str> = asserts.iter().map(|a| a.name.as_str()).collect();
472 println!(
473 " {} Step {}: inspect locals ({:?})",
474 "✓".green(),
475 step_num,
476 checked
477 );
478
479 Ok(())
480}
481
482async fn execute_inspect_stack_step(
484 client: &mut DaemonClient,
485 asserts: &[FrameAssertion],
486 step_num: usize,
487 _verbose: bool,
488) -> Result<()> {
489 let result = client
490 .send_command(Command::StackTrace {
491 thread_id: None,
492 limit: 50,
493 })
494 .await?;
495
496 let frames: Vec<StackFrameInfo> = serde_json::from_value(result["frames"].clone())
497 .map_err(|e| Error::TestAssertion(format!("Failed to parse stack frames: {}", e)))?;
498
499 for assertion in asserts {
500 if assertion.index >= frames.len() {
501 return Err(Error::TestAssertion(format!(
502 "Frame {} does not exist (only {} frames)",
503 assertion.index,
504 frames.len()
505 )));
506 }
507
508 let frame = &frames[assertion.index];
509
510 if let Some(expected_func) = &assertion.function {
511 if !frame.name.contains(expected_func) {
512 return Err(Error::TestAssertion(format!(
513 "Frame {}: expected function '{}', got '{}'",
514 assertion.index, expected_func, frame.name
515 )));
516 }
517 }
518
519 if let Some(expected_file) = &assertion.file {
520 let actual_file = frame.source.as_deref().unwrap_or("");
521 if !actual_file.contains(expected_file) {
522 return Err(Error::TestAssertion(format!(
523 "Frame {}: expected file '{}', got '{}'",
524 assertion.index, expected_file, actual_file
525 )));
526 }
527 }
528
529 if let Some(expected_line) = assertion.line {
530 let actual_line = frame.line.unwrap_or(0);
531 if expected_line != actual_line {
532 return Err(Error::TestAssertion(format!(
533 "Frame {}: expected line {}, got {}",
534 assertion.index, expected_line, actual_line
535 )));
536 }
537 }
538 }
539
540 println!(
541 " {} Step {}: inspect stack ({} frames checked)",
542 "✓".green(),
543 step_num,
544 asserts.len()
545 );
546
547 Ok(())
548}
549
550async fn execute_check_output_step(
552 client: &mut DaemonClient,
553 contains: Option<&String>,
554 equals: Option<&String>,
555 step_num: usize,
556 _verbose: bool,
557) -> Result<()> {
558 let result = client
559 .send_command(Command::GetOutput {
560 tail: None,
561 clear: false,
562 })
563 .await?;
564
565 let output = result["output"].as_str().unwrap_or("");
566
567 if let Some(expected_substr) = contains {
568 if !output.contains(expected_substr) {
569 return Err(Error::TestAssertion(format!(
570 "Output does not contain '{}'. Got: '{}'",
571 expected_substr,
572 if output.len() > 200 {
573 format!("{}...", &output[..200])
574 } else {
575 output.to_string()
576 }
577 )));
578 }
579 }
580
581 if let Some(expected_exact) = equals {
582 if output.trim() != expected_exact.trim() {
583 return Err(Error::TestAssertion(format!(
584 "Output mismatch. Expected: '{}', got: '{}'",
585 expected_exact, output
586 )));
587 }
588 }
589
590 println!(
591 " {} Step {}: check output",
592 "✓".green(),
593 step_num
594 );
595
596 Ok(())
597}
598
599async fn execute_evaluate_step(
601 client: &mut DaemonClient,
602 expression: &str,
603 expect: Option<&EvaluateExpectation>,
604 step_num: usize,
605 _verbose: bool,
606) -> Result<()> {
607 let result = client
608 .send_command(Command::Evaluate {
609 expression: expression.to_string(),
610 frame_id: None,
611 context: EvaluateContext::Watch,
612 })
613 .await;
614
615 let expect_success = expect.and_then(|e| e.success).unwrap_or(true);
617
618 if !expect_success {
619 match result {
621 Err(_) => {
622 println!(
623 " {} Step {}: evaluate '{}' (expected failure)",
624 "✓".green(),
625 step_num,
626 expression.dimmed()
627 );
628 return Ok(());
629 }
630 Ok(val) => {
631 let eval_result: EvaluateResult = serde_json::from_value(val)
633 .map_err(|e| Error::TestAssertion(format!("Failed to parse evaluate result: {}", e)))?;
634
635 if let Some(exp) = expect {
637 if let Some(expected_substr) = &exp.result_contains {
638 if eval_result.result.to_lowercase().contains(&expected_substr.to_lowercase()) {
639 println!(
640 " {} Step {}: evaluate '{}' = {} (expected error)",
641 "✓".green(),
642 step_num,
643 expression.dimmed(),
644 eval_result.result.dimmed()
645 );
646 return Ok(());
647 }
648 }
649 }
650
651 return Err(Error::TestAssertion(format!(
652 "Evaluate '{}': expected failure but got result '{}'",
653 expression, eval_result.result
654 )));
655 }
656 }
657 }
658
659 let result = result?;
661 let eval_result: EvaluateResult = serde_json::from_value(result)
662 .map_err(|e| Error::TestAssertion(format!("Failed to parse evaluate result: {}", e)))?;
663
664 if let Some(exp) = expect {
665 if let Some(expected_result) = &exp.result {
666 if &eval_result.result != expected_result {
667 return Err(Error::TestAssertion(format!(
668 "Evaluate '{}': expected '{}', got '{}'",
669 expression, expected_result, eval_result.result
670 )));
671 }
672 }
673
674 if let Some(expected_substr) = &exp.result_contains {
675 if !eval_result.result.contains(expected_substr) {
676 return Err(Error::TestAssertion(format!(
677 "Evaluate '{}': expected result containing '{}', got '{}'",
678 expression, expected_substr, eval_result.result
679 )));
680 }
681 }
682
683 if let Some(expected_type) = &exp.type_name {
684 let actual_type = eval_result.type_name.as_deref().unwrap_or("");
685 if actual_type != expected_type {
686 return Err(Error::TestAssertion(format!(
687 "Evaluate '{}': expected type '{}', got '{}'",
688 expression, expected_type, actual_type
689 )));
690 }
691 }
692 }
693
694 println!(
695 " {} Step {}: evaluate '{}' = {}",
696 "✓".green(),
697 step_num,
698 expression.dimmed(),
699 eval_result.result.dimmed()
700 );
701
702 Ok(())
703}
704
705fn parse_command(s: &str) -> Result<Command> {
707 let parts: Vec<&str> = s.split_whitespace().collect();
708 if parts.is_empty() {
709 return Err(Error::Config("Empty command".to_string()));
710 }
711
712 let cmd = parts[0].to_lowercase();
713 let args = &parts[1..];
714
715 match cmd.as_str() {
716 "continue" | "c" => Ok(Command::Continue),
717 "next" | "n" => Ok(Command::Next),
718 "step" | "s" => Ok(Command::StepIn),
719 "finish" | "out" => Ok(Command::StepOut),
720 "pause" => Ok(Command::Pause),
721
722 "break" | "b" => {
723 if args.is_empty() {
724 return Err(Error::Config(
725 "break command requires a location".to_string(),
726 ));
727 }
728 let mut location_str = String::new();
731 let mut condition: Option<String> = None;
732 let mut i = 0;
733
734 if args.get(0) == Some(&"add") && args.len() > 1 {
737 i = 1;
738 }
739
740 while i < args.len() {
741 if args[i] == "--condition" && i + 1 < args.len() {
742 i += 1;
744 let mut cond_parts = Vec::new();
745 while i < args.len() && !args[i].starts_with("--") {
746 cond_parts.push(args[i]);
747 i += 1;
748 }
749 condition = Some(cond_parts.join(" ").trim_matches('"').to_string());
750 } else if !args[i].starts_with("--") {
751 if !location_str.is_empty() {
752 location_str.push(' ');
753 }
754 location_str.push_str(args[i]);
755 i += 1;
756 } else {
757 i += 1;
758 }
759 }
760
761 let location = BreakpointLocation::parse(&location_str)?;
762 Ok(Command::BreakpointAdd {
763 location,
764 condition,
765 hit_count: None,
766 })
767 }
768
769 "breakpoint" => {
770 if args.is_empty() {
771 return Err(Error::Config(
772 "breakpoint command requires a subcommand".to_string(),
773 ));
774 }
775
776 match args[0] {
777 "add" => {
778 if args.len() < 2 {
779 return Err(Error::Config(
780 "breakpoint add requires a location".to_string(),
781 ));
782 }
783 let location = BreakpointLocation::parse(args[1])?;
784 Ok(Command::BreakpointAdd {
785 location,
786 condition: None,
787 hit_count: None,
788 })
789 }
790 "remove" => {
791 if args.len() < 2 {
792 return Ok(Command::BreakpointRemove { id: None, all: true });
793 }
794 if args[1] == "all" || args[1] == "--all" {
795 return Ok(Command::BreakpointRemove { id: None, all: true });
796 }
797 let id: u32 = args[1].parse().map_err(|_| {
798 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
799 })?;
800 Ok(Command::BreakpointRemove {
801 id: Some(id),
802 all: false,
803 })
804 }
805 "list" => Ok(Command::BreakpointList),
806 "enable" => {
807 if args.len() < 2 {
808 return Err(Error::Config(
809 "breakpoint enable requires an ID".to_string(),
810 ));
811 }
812 let id: u32 = args[1].parse().map_err(|_| {
813 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
814 })?;
815 Ok(Command::BreakpointEnable { id })
816 }
817 "disable" => {
818 if args.len() < 2 {
819 return Err(Error::Config(
820 "breakpoint disable requires an ID".to_string(),
821 ));
822 }
823 let id: u32 = args[1].parse().map_err(|_| {
824 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
825 })?;
826 Ok(Command::BreakpointDisable { id })
827 }
828 _ => Err(Error::Config(format!(
829 "Unknown breakpoint subcommand: {}",
830 args[0]
831 ))),
832 }
833 }
834
835 "locals" => Ok(Command::Locals { frame_id: None }),
836
837 "backtrace" | "bt" => Ok(Command::StackTrace {
838 thread_id: None,
839 limit: 20,
840 }),
841
842 "threads" => Ok(Command::Threads),
843
844 "thread" => {
845 if args.is_empty() {
846 return Err(Error::Config("thread command requires an ID".to_string()));
847 }
848 let id: i64 = args[0]
849 .parse()
850 .map_err(|_| Error::Config(format!("Invalid thread ID: {}", args[0])))?;
851 Ok(Command::ThreadSelect { id })
852 }
853
854 "frame" => {
855 if args.is_empty() {
856 return Err(Error::Config(
857 "frame command requires a number".to_string(),
858 ));
859 }
860 let number: usize = args[0]
861 .parse()
862 .map_err(|_| Error::Config(format!("Invalid frame number: {}", args[0])))?;
863 Ok(Command::FrameSelect { number })
864 }
865
866 "up" => Ok(Command::FrameUp),
867 "down" => Ok(Command::FrameDown),
868
869 "print" | "p" | "eval" => {
870 if args.is_empty() {
871 return Err(Error::Config(
872 "print/eval command requires an expression".to_string(),
873 ));
874 }
875 Ok(Command::Evaluate {
876 expression: args.join(" "),
877 frame_id: None,
878 context: EvaluateContext::Watch,
879 })
880 }
881
882 "stop" => Ok(Command::Stop),
883 "detach" => Ok(Command::Detach),
884 "restart" => Ok(Command::Restart),
885
886 "output" => {
887 let mut tail: Option<usize> = None;
889 let mut clear = false;
890 let mut i = 0;
891 while i < args.len() {
892 match args[i] {
893 "--tail" => {
894 if i + 1 < args.len() {
895 tail = args[i + 1].parse().ok();
896 i += 2;
897 } else {
898 i += 1;
899 }
900 }
901 "--clear" => {
902 clear = true;
903 i += 1;
904 }
905 _ => {
906 i += 1;
907 }
908 }
909 }
910 Ok(Command::GetOutput { tail, clear })
911 }
912
913 _ => Err(Error::Config(format!("Unknown command: {}", cmd))),
914 }
915}
916
917#[cfg(test)]
918mod tests {
919 use super::*;
920
921 #[test]
922 fn test_parse_simple_commands() {
923 assert!(matches!(parse_command("continue").unwrap(), Command::Continue));
924 assert!(matches!(parse_command("c").unwrap(), Command::Continue));
925 assert!(matches!(parse_command("next").unwrap(), Command::Next));
926 assert!(matches!(parse_command("step").unwrap(), Command::StepIn));
927 assert!(matches!(parse_command("finish").unwrap(), Command::StepOut));
928 assert!(matches!(parse_command("pause").unwrap(), Command::Pause));
929 }
930
931 #[test]
932 fn test_parse_break_commands() {
933 let cmd = parse_command("break main").unwrap();
934 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
935
936 let cmd = parse_command("break add main.rs:42").unwrap();
937 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
938
939 let cmd = parse_command("b foo.c:10").unwrap();
940 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
941 }
942
943 #[test]
944 fn test_parse_breakpoint_subcommands() {
945 assert!(matches!(
946 parse_command("breakpoint add main").unwrap(),
947 Command::BreakpointAdd { .. }
948 ));
949 assert!(matches!(
950 parse_command("breakpoint list").unwrap(),
951 Command::BreakpointList
952 ));
953 assert!(matches!(
954 parse_command("breakpoint remove 1").unwrap(),
955 Command::BreakpointRemove { .. }
956 ));
957 }
958
959 #[test]
960 fn test_parse_print_commands() {
961 let cmd = parse_command("print x + y").unwrap();
962 match cmd {
963 Command::Evaluate { expression, .. } => {
964 assert_eq!(expression, "x + y");
965 }
966 _ => panic!("Expected Evaluate command"),
967 }
968 }
969}