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 let program_path = program_path.canonicalize().map_err(|e| {
117 Error::Config(format!(
118 "Program not found '{}': {}",
119 scenario.target.program.display(),
120 e
121 ))
122 })?;
123
124 println!("\n{}", "Starting debug session...".cyan());
126 client
127 .send_command(Command::Start {
128 program: program_path.clone(),
129 args: scenario.target.args.clone().unwrap_or_default(),
130 adapter: scenario.target.adapter.clone(),
131 stop_on_entry: scenario.target.stop_on_entry,
132 })
133 .await?;
134
135 if verbose {
136 println!(
137 " Program: {}",
138 program_path.display().to_string().dimmed()
139 );
140 if let Some(adapter) = &scenario.target.adapter {
141 println!(" Adapter: {}", adapter.dimmed());
142 }
143 }
144
145 println!(" {} Session started", "✓".green());
146
147 println!("\n{}", "Steps:".cyan());
149
150 for (i, step) in scenario.steps.iter().enumerate() {
151 let step_num = i + 1;
152
153 match execute_step(&mut client, step, step_num, verbose).await {
154 Ok(()) => {
155 }
157 Err(e) => {
158 println!(" {} Step {}: {}", "✗".red(), step_num, e);
159
160 let _ = client.send_command(Command::Stop).await;
162
163 return Ok(TestResult {
164 name: scenario.name.clone(),
165 passed: false,
166 steps_run: step_num,
167 steps_total,
168 error: Some(e.to_string()),
169 });
170 }
171 }
172 }
173
174 let _ = client.send_command(Command::Stop).await;
176
177 println!(
178 "\n{} {}\n",
179 "✓".green().bold(),
180 "Test Passed".green().bold()
181 );
182
183 Ok(TestResult {
184 name: scenario.name,
185 passed: true,
186 steps_run: steps_total,
187 steps_total,
188 error: None,
189 })
190}
191
192async fn execute_step(
194 client: &mut DaemonClient,
195 step: &TestStep,
196 step_num: usize,
197 verbose: bool,
198) -> Result<()> {
199 match step {
200 TestStep::Command { command, expect } => {
201 execute_command_step(client, command, expect.as_ref(), step_num, verbose).await
202 }
203 TestStep::Await { timeout, expect } => {
204 execute_await_step(client, *timeout, expect.as_ref(), step_num, verbose).await
205 }
206 TestStep::InspectLocals { asserts } => {
207 execute_inspect_locals_step(client, asserts, step_num, verbose).await
208 }
209 TestStep::InspectStack { asserts } => {
210 execute_inspect_stack_step(client, asserts, step_num, verbose).await
211 }
212 TestStep::CheckOutput { contains, equals } => {
213 execute_check_output_step(client, contains.as_ref(), equals.as_ref(), step_num, verbose)
214 .await
215 }
216 TestStep::Evaluate { expression, expect } => {
217 execute_evaluate_step(client, expression, expect.as_ref(), step_num, verbose).await
218 }
219 }
220}
221
222async fn execute_command_step(
224 client: &mut DaemonClient,
225 command_str: &str,
226 expect: Option<&CommandExpectation>,
227 step_num: usize,
228 _verbose: bool,
229) -> Result<()> {
230 let cmd = parse_command(command_str)?;
231
232 let result = client.send_command(cmd).await;
233
234 if let Some(exp) = expect {
236 if let Some(should_succeed) = exp.success {
237 let did_succeed = result.is_ok();
238 if should_succeed != did_succeed {
239 return Err(Error::TestAssertion(format!(
240 "Command '{}' expected success={}, got success={}",
241 command_str, should_succeed, did_succeed
242 )));
243 }
244 }
245 }
246
247 if expect.map(|e| e.success == Some(false)).unwrap_or(false) {
249 println!(
250 " {} Step {}: {} (expected failure)",
251 "✓".green(),
252 step_num,
253 command_str.dimmed()
254 );
255 return Ok(());
256 }
257
258 result?;
259
260 println!(
261 " {} Step {}: {}",
262 "✓".green(),
263 step_num,
264 command_str.dimmed()
265 );
266
267 Ok(())
268}
269
270async fn execute_await_step(
272 client: &mut DaemonClient,
273 timeout: Option<u64>,
274 expect: Option<&StopExpectation>,
275 step_num: usize,
276 _verbose: bool,
277) -> Result<()> {
278 let timeout_secs = timeout.unwrap_or(30);
279
280 let result = client
281 .send_command(Command::Await { timeout_secs })
282 .await?;
283
284 let stop_result: StopResult = serde_json::from_value(result)
285 .map_err(|e| Error::TestAssertion(format!("Failed to parse stop result: {}", e)))?;
286
287 if let Some(exp) = expect {
289 if let Some(expected_reason) = &exp.reason {
290 if !stop_result.reason.contains(expected_reason) {
291 return Err(Error::TestAssertion(format!(
292 "Expected stop reason '{}', got '{}'",
293 expected_reason, stop_result.reason
294 )));
295 }
296 }
297
298 if let Some(expected_file) = &exp.file {
299 let actual_file = stop_result.source.as_deref().unwrap_or("");
300 if !actual_file.contains(expected_file) {
301 return Err(Error::TestAssertion(format!(
302 "Expected file '{}', got '{}'",
303 expected_file, actual_file
304 )));
305 }
306 }
307
308 if let Some(expected_line) = exp.line {
309 let actual_line = stop_result.line.unwrap_or(0);
310 if expected_line != actual_line {
311 return Err(Error::TestAssertion(format!(
312 "Expected line {}, got {}",
313 expected_line, actual_line
314 )));
315 }
316 }
317 }
318
319 let location = if let Some(source) = &stop_result.source {
320 if let Some(line) = stop_result.line {
321 format!("{}:{}", source, line)
322 } else {
323 source.clone()
324 }
325 } else {
326 "unknown location".to_string()
327 };
328
329 println!(
330 " {} Step {}: await ({} at {})",
331 "✓".green(),
332 step_num,
333 stop_result.reason.dimmed(),
334 location.dimmed()
335 );
336
337 Ok(())
338}
339
340async fn execute_inspect_locals_step(
342 client: &mut DaemonClient,
343 asserts: &[VariableAssertion],
344 step_num: usize,
345 _verbose: bool,
346) -> Result<()> {
347 let result = client
348 .send_command(Command::Locals { frame_id: None })
349 .await?;
350
351 let vars: Vec<VariableInfo> = serde_json::from_value(result["variables"].clone())
352 .map_err(|e| Error::TestAssertion(format!("Failed to parse variables: {}", e)))?;
353
354 for assertion in asserts {
355 let var = vars.iter().find(|v| v.name == assertion.name);
356
357 match var {
358 Some(v) => {
359 if let Some(expected_value) = &assertion.value {
361 if &v.value != expected_value {
362 return Err(Error::TestAssertion(format!(
363 "Variable '{}': expected value '{}', got '{}'",
364 assertion.name, expected_value, v.value
365 )));
366 }
367 }
368
369 if let Some(expected_substr) = &assertion.value_contains {
371 if !v.value.contains(expected_substr) {
372 return Err(Error::TestAssertion(format!(
373 "Variable '{}': expected value containing '{}', got '{}'",
374 assertion.name, expected_substr, v.value
375 )));
376 }
377 }
378
379 if let Some(expected_type) = &assertion.type_name {
381 let actual_type = v.type_name.as_deref().unwrap_or("");
382 if actual_type != expected_type {
383 return Err(Error::TestAssertion(format!(
384 "Variable '{}': expected type '{}', got '{}'",
385 assertion.name, expected_type, actual_type
386 )));
387 }
388 }
389 }
390 None => {
391 let available: Vec<&str> = vars.iter().map(|v| v.name.as_str()).collect();
392 return Err(Error::TestAssertion(format!(
393 "Variable '{}' not found. Available: {:?}",
394 assertion.name, available
395 )));
396 }
397 }
398 }
399
400 let checked: Vec<&str> = asserts.iter().map(|a| a.name.as_str()).collect();
401 println!(
402 " {} Step {}: inspect locals ({:?})",
403 "✓".green(),
404 step_num,
405 checked
406 );
407
408 Ok(())
409}
410
411async fn execute_inspect_stack_step(
413 client: &mut DaemonClient,
414 asserts: &[FrameAssertion],
415 step_num: usize,
416 _verbose: bool,
417) -> Result<()> {
418 let result = client
419 .send_command(Command::StackTrace {
420 thread_id: None,
421 limit: 50,
422 })
423 .await?;
424
425 let frames: Vec<StackFrameInfo> = serde_json::from_value(result["frames"].clone())
426 .map_err(|e| Error::TestAssertion(format!("Failed to parse stack frames: {}", e)))?;
427
428 for assertion in asserts {
429 if assertion.index >= frames.len() {
430 return Err(Error::TestAssertion(format!(
431 "Frame {} does not exist (only {} frames)",
432 assertion.index,
433 frames.len()
434 )));
435 }
436
437 let frame = &frames[assertion.index];
438
439 if let Some(expected_func) = &assertion.function {
440 if !frame.name.contains(expected_func) {
441 return Err(Error::TestAssertion(format!(
442 "Frame {}: expected function '{}', got '{}'",
443 assertion.index, expected_func, frame.name
444 )));
445 }
446 }
447
448 if let Some(expected_file) = &assertion.file {
449 let actual_file = frame.source.as_deref().unwrap_or("");
450 if !actual_file.contains(expected_file) {
451 return Err(Error::TestAssertion(format!(
452 "Frame {}: expected file '{}', got '{}'",
453 assertion.index, expected_file, actual_file
454 )));
455 }
456 }
457
458 if let Some(expected_line) = assertion.line {
459 let actual_line = frame.line.unwrap_or(0);
460 if expected_line != actual_line {
461 return Err(Error::TestAssertion(format!(
462 "Frame {}: expected line {}, got {}",
463 assertion.index, expected_line, actual_line
464 )));
465 }
466 }
467 }
468
469 println!(
470 " {} Step {}: inspect stack ({} frames checked)",
471 "✓".green(),
472 step_num,
473 asserts.len()
474 );
475
476 Ok(())
477}
478
479async fn execute_check_output_step(
481 client: &mut DaemonClient,
482 contains: Option<&String>,
483 equals: Option<&String>,
484 step_num: usize,
485 _verbose: bool,
486) -> Result<()> {
487 let result = client
488 .send_command(Command::GetOutput {
489 tail: None,
490 clear: false,
491 })
492 .await?;
493
494 let output = result["output"].as_str().unwrap_or("");
495
496 if let Some(expected_substr) = contains {
497 if !output.contains(expected_substr) {
498 return Err(Error::TestAssertion(format!(
499 "Output does not contain '{}'. Got: '{}'",
500 expected_substr,
501 if output.len() > 200 {
502 format!("{}...", &output[..200])
503 } else {
504 output.to_string()
505 }
506 )));
507 }
508 }
509
510 if let Some(expected_exact) = equals {
511 if output.trim() != expected_exact.trim() {
512 return Err(Error::TestAssertion(format!(
513 "Output mismatch. Expected: '{}', got: '{}'",
514 expected_exact, output
515 )));
516 }
517 }
518
519 println!(
520 " {} Step {}: check output",
521 "✓".green(),
522 step_num
523 );
524
525 Ok(())
526}
527
528async fn execute_evaluate_step(
530 client: &mut DaemonClient,
531 expression: &str,
532 expect: Option<&EvaluateExpectation>,
533 step_num: usize,
534 _verbose: bool,
535) -> Result<()> {
536 let result = client
537 .send_command(Command::Evaluate {
538 expression: expression.to_string(),
539 frame_id: None,
540 context: EvaluateContext::Watch,
541 })
542 .await?;
543
544 let eval_result: EvaluateResult = serde_json::from_value(result)
545 .map_err(|e| Error::TestAssertion(format!("Failed to parse evaluate result: {}", e)))?;
546
547 if let Some(exp) = expect {
548 if let Some(expected_result) = &exp.result {
549 if &eval_result.result != expected_result {
550 return Err(Error::TestAssertion(format!(
551 "Evaluate '{}': expected '{}', got '{}'",
552 expression, expected_result, eval_result.result
553 )));
554 }
555 }
556
557 if let Some(expected_substr) = &exp.result_contains {
558 if !eval_result.result.contains(expected_substr) {
559 return Err(Error::TestAssertion(format!(
560 "Evaluate '{}': expected result containing '{}', got '{}'",
561 expression, expected_substr, eval_result.result
562 )));
563 }
564 }
565
566 if let Some(expected_type) = &exp.type_name {
567 let actual_type = eval_result.type_name.as_deref().unwrap_or("");
568 if actual_type != expected_type {
569 return Err(Error::TestAssertion(format!(
570 "Evaluate '{}': expected type '{}', got '{}'",
571 expression, expected_type, actual_type
572 )));
573 }
574 }
575 }
576
577 println!(
578 " {} Step {}: evaluate '{}' = {}",
579 "✓".green(),
580 step_num,
581 expression.dimmed(),
582 eval_result.result.dimmed()
583 );
584
585 Ok(())
586}
587
588fn parse_command(s: &str) -> Result<Command> {
590 let parts: Vec<&str> = s.split_whitespace().collect();
591 if parts.is_empty() {
592 return Err(Error::Config("Empty command".to_string()));
593 }
594
595 let cmd = parts[0].to_lowercase();
596 let args = &parts[1..];
597
598 match cmd.as_str() {
599 "continue" | "c" => Ok(Command::Continue),
600 "next" | "n" => Ok(Command::Next),
601 "step" | "s" => Ok(Command::StepIn),
602 "finish" | "out" => Ok(Command::StepOut),
603 "pause" => Ok(Command::Pause),
604
605 "break" | "b" => {
606 if args.is_empty() {
607 return Err(Error::Config(
608 "break command requires a location".to_string(),
609 ));
610 }
611 let location_str = if args[0] == "add" && args.len() > 1 {
613 args[1..].join(" ")
614 } else {
615 args.join(" ")
616 };
617
618 let location = BreakpointLocation::parse(&location_str)?;
619 Ok(Command::BreakpointAdd {
620 location,
621 condition: None,
622 hit_count: None,
623 })
624 }
625
626 "breakpoint" => {
627 if args.is_empty() {
628 return Err(Error::Config(
629 "breakpoint command requires a subcommand".to_string(),
630 ));
631 }
632
633 match args[0] {
634 "add" => {
635 if args.len() < 2 {
636 return Err(Error::Config(
637 "breakpoint add requires a location".to_string(),
638 ));
639 }
640 let location = BreakpointLocation::parse(args[1])?;
641 Ok(Command::BreakpointAdd {
642 location,
643 condition: None,
644 hit_count: None,
645 })
646 }
647 "remove" => {
648 if args.len() < 2 {
649 return Ok(Command::BreakpointRemove { id: None, all: true });
650 }
651 if args[1] == "all" || args[1] == "--all" {
652 return Ok(Command::BreakpointRemove { id: None, all: true });
653 }
654 let id: u32 = args[1].parse().map_err(|_| {
655 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
656 })?;
657 Ok(Command::BreakpointRemove {
658 id: Some(id),
659 all: false,
660 })
661 }
662 "list" => Ok(Command::BreakpointList),
663 "enable" => {
664 if args.len() < 2 {
665 return Err(Error::Config(
666 "breakpoint enable requires an ID".to_string(),
667 ));
668 }
669 let id: u32 = args[1].parse().map_err(|_| {
670 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
671 })?;
672 Ok(Command::BreakpointEnable { id })
673 }
674 "disable" => {
675 if args.len() < 2 {
676 return Err(Error::Config(
677 "breakpoint disable requires an ID".to_string(),
678 ));
679 }
680 let id: u32 = args[1].parse().map_err(|_| {
681 Error::Config(format!("Invalid breakpoint ID: {}", args[1]))
682 })?;
683 Ok(Command::BreakpointDisable { id })
684 }
685 _ => Err(Error::Config(format!(
686 "Unknown breakpoint subcommand: {}",
687 args[0]
688 ))),
689 }
690 }
691
692 "locals" => Ok(Command::Locals { frame_id: None }),
693
694 "backtrace" | "bt" => Ok(Command::StackTrace {
695 thread_id: None,
696 limit: 20,
697 }),
698
699 "threads" => Ok(Command::Threads),
700
701 "thread" => {
702 if args.is_empty() {
703 return Err(Error::Config("thread command requires an ID".to_string()));
704 }
705 let id: i64 = args[0]
706 .parse()
707 .map_err(|_| Error::Config(format!("Invalid thread ID: {}", args[0])))?;
708 Ok(Command::ThreadSelect { id })
709 }
710
711 "frame" => {
712 if args.is_empty() {
713 return Err(Error::Config(
714 "frame command requires a number".to_string(),
715 ));
716 }
717 let number: usize = args[0]
718 .parse()
719 .map_err(|_| Error::Config(format!("Invalid frame number: {}", args[0])))?;
720 Ok(Command::FrameSelect { number })
721 }
722
723 "up" => Ok(Command::FrameUp),
724 "down" => Ok(Command::FrameDown),
725
726 "print" | "p" | "eval" => {
727 if args.is_empty() {
728 return Err(Error::Config(
729 "print/eval command requires an expression".to_string(),
730 ));
731 }
732 Ok(Command::Evaluate {
733 expression: args.join(" "),
734 frame_id: None,
735 context: EvaluateContext::Watch,
736 })
737 }
738
739 "stop" => Ok(Command::Stop),
740 "detach" => Ok(Command::Detach),
741 "restart" => Ok(Command::Restart),
742
743 _ => Err(Error::Config(format!("Unknown command: {}", cmd))),
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750
751 #[test]
752 fn test_parse_simple_commands() {
753 assert!(matches!(parse_command("continue").unwrap(), Command::Continue));
754 assert!(matches!(parse_command("c").unwrap(), Command::Continue));
755 assert!(matches!(parse_command("next").unwrap(), Command::Next));
756 assert!(matches!(parse_command("step").unwrap(), Command::StepIn));
757 assert!(matches!(parse_command("finish").unwrap(), Command::StepOut));
758 assert!(matches!(parse_command("pause").unwrap(), Command::Pause));
759 }
760
761 #[test]
762 fn test_parse_break_commands() {
763 let cmd = parse_command("break main").unwrap();
764 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
765
766 let cmd = parse_command("break add main.rs:42").unwrap();
767 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
768
769 let cmd = parse_command("b foo.c:10").unwrap();
770 assert!(matches!(cmd, Command::BreakpointAdd { .. }));
771 }
772
773 #[test]
774 fn test_parse_breakpoint_subcommands() {
775 assert!(matches!(
776 parse_command("breakpoint add main").unwrap(),
777 Command::BreakpointAdd { .. }
778 ));
779 assert!(matches!(
780 parse_command("breakpoint list").unwrap(),
781 Command::BreakpointList
782 ));
783 assert!(matches!(
784 parse_command("breakpoint remove 1").unwrap(),
785 Command::BreakpointRemove { .. }
786 ));
787 }
788
789 #[test]
790 fn test_parse_print_commands() {
791 let cmd = parse_command("print x + y").unwrap();
792 match cmd {
793 Command::Evaluate { expression, .. } => {
794 assert_eq!(expression, "x + y");
795 }
796 _ => panic!("Expected Evaluate command"),
797 }
798 }
799}