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