Skip to main content

debugger/testing/
runner.rs

1//! Test runner implementation
2//!
3//! Executes test scenarios by communicating directly with the daemon
4//! using structured data rather than parsing CLI output.
5
6use 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/// Result of a test run
26#[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
35/// Run a test scenario from a YAML file
36pub async fn run_scenario(path: &Path, verbose: bool) -> Result<TestResult> {
37    // Load and parse the YAML scenario
38    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    // Run setup steps
62    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 is running
105    ensure_daemon_running().await?;
106    let mut client = DaemonClient::connect().await?;
107
108    // Resolve program path relative to the scenario file
109    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    // Handle launch vs attach mode
117    if scenario.target.mode == "attach" {
118        // Attach mode: get PID from scenario or pid_file
119        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        // Validate process exists before attempting attach (signal 0 checks existence)
150        #[cfg(unix)]
151        {
152            // Signal 0 tests process existence without side effects
153            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        // Unknown mode - fail explicitly
180        return Err(Error::Config(format!(
181            "Unknown target mode '{}'. Supported modes: 'launch', 'attach'",
182            scenario.target.mode
183        )));
184    } else {
185        // Launch mode (default)
186        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    // Execute test steps
219    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                // Step passed
227            }
228            Err(e) => {
229                println!("  {} Step {}: {}", "✗".red(), step_num, e);
230
231                // Cleanup: stop the debug session
232                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    // Cleanup: stop the debug session
246    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
263/// Execute a single test step
264async 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
293/// Execute a command step
294async 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    // Check expectations
306    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    // For commands that are expected to fail, we don't propagate the error
319    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
341/// Execute an await step
342async 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    // Check expectations
359    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
411/// Execute an inspect locals step
412async 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                // Check value (exact match)
431                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                // Check value (partial match)
441                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                // Check type
451                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
482/// Execute an inspect stack step
483async 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
550/// Execute a check output step
551async 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
599/// Execute an evaluate step
600async 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    // Check if we expect failure
616    let expect_success = expect.and_then(|e| e.success).unwrap_or(true);
617
618    if !expect_success {
619        // We expect evaluation to fail
620        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                // Check if the result contains an error indicator
632                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 result_contains is specified, check if error message matches
636                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    // Normal success path
660    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
705/// Parse a command string into a Command enum
706fn 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            // Handle "break add <location>" or just "break <location>"
729            // Also handle --condition "expr" flag
730            let mut location_str = String::new();
731            let mut condition: Option<String> = None;
732            let mut i = 0;
733
734            // Skip "add" subcommand if present AND there are more args
735            // (otherwise "add" is the function name to break on)
736            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                    // Collect condition expression (may be quoted)
743                    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            // Parse --tail N and --clear flags
888            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}