diff --git a/.gitignore b/.gitignore index 835e5f2..bf790e1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ target # Test binaries -tests/e2e/test_* \ No newline at end of file +tests/e2e/test_* +tests/fixtures/cuda_test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 341def6..748e9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.1] - 2026-01-25 + +### Added + +- **Delve (Go) Support**: Full Go debugging via Delve DAP + - `setup go` / `setup delve` - Install and configure Delve adapter + - TCP transport mode for Delve's DAP server + - Go project detection via `go.mod` / `go.sum` + - `mode: "exec"` for pre-compiled binaries + - Delve-specific `stopAtEntry` handling + +- **GDB Support**: Native DAP support for GDB 14.1+ + - `setup gdb` - Install and configure GDB adapter + - Uses `-i=dap` interpreter mode for direct DAP communication + - Version detection and validation (requires GDB ≥14.1) + +- **CUDA-GDB Support**: NVIDIA GPU debugging via cuda-gdb + - `setup cuda-gdb` - Install and configure CUDA-GDB adapter + - **Dual-mode architecture**: Automatically detects best mode + - Native DAP (`-i=dap`) for NVIDIA official installs with DAP support + - cdt-gdb-adapter bridge for minimal builds (e.g., Arch Linux) + - CUDA project detection via `*.cu` files + - Linux-only (NVIDIA driver limitation) + +- **Initial Breakpoints**: Set breakpoints before program starts + - `--break` / `-b` flag for `start` command + - Set multiple breakpoints: `debugger start ./prog --break main --break file.c:42` + - Essential for adapters that don't support `stopOnEntry` (e.g., cdt-gdb-adapter) + - Breakpoints set during DAP configuration phase (before `configurationDone`) + +- **Adapter-specific Stop-on-Entry**: Proper handling for different adapters + - GDB/CUDA-GDB: `stopAtBeginningOfMainSubprogram` + - Delve: `stopAtEntry` + - Others: `stopOnEntry` + +### Fixed + +- **cuda-gdb Version Parsing**: Handle cuda-gdb's "exec:" wrapper line in version output + - Parser now searches for "GNU gdb X.Y" pattern across all lines + - Correctly extracts base GDB version (14.2) instead of cuda-gdb version (13.1) + +- **Address Parsing**: Enhanced address extraction in DAP client and verifier + +### Documentation + +- Added `docs/plan/cuda-gdb.md` with architecture details and tested features +- Added `docs/plan/go-delve-support.md` with Go debugging guide +- Updated `src/setup/adapters/CLAUDE.md` with adapter-specific behaviors +- Added `src/setup/adapters/README.md` with usage examples + ## [0.1.0] - 2026-01-18 ### Added diff --git a/Cargo.lock b/Cargo.lock index 432d884..91e4b04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "debugger-cli" -version = "0.1.0" +version = "0.1.1" dependencies = [ "async-trait", "clap", diff --git a/Cargo.toml b/Cargo.toml index db32f1e..a883cda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "debugger-cli" -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "LLM-friendly debugger CLI using the Debug Adapter Protocol" license = "GPL-3.0-only" diff --git a/docs/plan/cuda-gdb.md b/docs/plan/cuda-gdb.md new file mode 100644 index 0000000..f265c26 --- /dev/null +++ b/docs/plan/cuda-gdb.md @@ -0,0 +1,155 @@ +# GDB and CUDA-GDB Support Implementation Plan + +## Overview + +This plan adds DAP support for GDB and CUDA-GDB debuggers to debugger-cli. + +### Key Discoveries (2026-01-24) + +1. **CUDA-GDB Native DAP Support Varies by Distribution**: + - NVIDIA official installs (Ubuntu via `cuda-gdb-13-1` package): Native DAP works via `-i=dap` + - Arch Linux `cuda` package: Minimal build without DAP Python bindings ("Interpreter `dap' unrecognized") + +2. **Dual-Mode Architecture**: The adapter now supports two modes: + - **Native DAP** (preferred): `cuda-gdb -i=dap` when available + - **cdt-gdb-adapter bridge** (fallback): Node.js-based MI-to-DAP bridge for minimal builds + +3. **Stop-on-Entry Behavior**: + - Native DAP (GDB 14.1+): Uses `stopAtBeginningOfMainSubprogram` (not `stopOnEntry`) + - cdt-gdb-adapter: Does NOT support stop-on-entry; use a breakpoint on `main` instead + +### Architecture + +``` +Native DAP Mode (preferred): + Client <-> cuda-gdb -i=dap <-> GPU + +Bridge Mode (fallback for minimal builds): + Client <-> cdt-gdb-adapter (Node.js) <-> cuda-gdb (MI mode) <-> GPU +``` + +### Requirements + +| Mode | Requirements | +|------|--------------| +| Native DAP | cuda-gdb based on GDB 14.1+ with DAP Python bindings | +| Bridge (fallback) | cuda-gdb (any version) + Node.js + cdt-gdb-adapter (`npm install -g cdt-gdb-adapter`) | + +### Mode Detection + +The adapter automatically detects the best mode: +1. Check if cuda-gdb has GDB 14.1+ base version +2. Test `-i=dap -batch -ex quit` for "Interpreter `dap' unrecognized" error +3. If native DAP works → use it; otherwise → use cdt-gdb-adapter bridge + +## Adapter-Specific Behaviors + +### Stop-on-Entry + +| Adapter | Stop-on-Entry Support | Parameter | +|---------|----------------------|-----------| +| lldb-dap | ✅ | `stopOnEntry: true` | +| GDB native DAP | ✅ | `stopAtBeginningOfMainSubprogram: true` | +| Delve (Go) | ✅ | `stopAtEntry: true` | +| debugpy | ✅ | `stopOnEntry: true` | +| cdt-gdb-adapter | ❌ | Use `--break main` instead | + +### Using --break for Initial Breakpoints + +For adapters that don't support stop-on-entry (like cdt-gdb-adapter), or when you want to stop at a specific location: + +```bash +# Stop at main function +debugger start ./program --break main + +# Stop at specific line +debugger start ./program --break src/main.cu:42 + +# Multiple breakpoints +debugger start ./program --break main --break vectorAdd +``` + +Initial breakpoints are set during the DAP configuration phase (between `initialized` event and `configurationDone`), ensuring they're active before program execution begins. + +## Version Parsing + +cuda-gdb outputs a wrapper message on the first line which broke simple version parsing: + +``` +exec: /opt/cuda/bin/cuda-gdb +NVIDIA (R) CUDA Debugger +13.1 release +... +GNU gdb (GDB) 14.2 +``` + +The parser now searches for "GNU gdb X.Y" pattern to extract the base GDB version (14.2), ignoring the cuda-gdb version (13.1). + +## Tested Features + +Tested on Lambda Labs VM (A10 GPU, cuda-gdb 13.1 with native DAP): + +| Feature | Status | +|---------|--------| +| Stop at entry point | ✅ via `stopAtBeginningOfMainSubprogram` | +| Backtrace | ✅ Shows source location | +| Local variables | ✅ | +| Function breakpoint on kernel | ✅ | +| CUDA thread visibility | ✅ Shows GPU threads (e.g., `cuda00001400006`) | +| Continue to completion | ✅ | + +Tested on Arch Linux (cuda-gdb 13.1 minimal, via cdt-gdb-adapter bridge): + +| Feature | Status | +|---------|--------| +| DAP initialize | ✅ | +| Function breakpoint | ✅ verified=True | +| Hit breakpoint | ✅ reason="function breakpoint" | +| Stack trace | ✅ | +| Scopes (Locals/Registers) | ✅ | +| Continue to completion | ✅ | +| Stop-on-entry | ❌ (use `--break main` instead) | + +## Code Changes Summary + +### Files Modified + +| File | Change | +|------|--------| +| `src/setup/adapters/gdb_common.rs` | Fixed version parser to handle cuda-gdb's "exec:" wrapper | +| `src/setup/adapters/cuda_gdb.rs` | Added native DAP detection + cdt-gdb-adapter fallback | +| `src/dap/types.rs` | Added `stopAtBeginningOfMainSubprogram` field | +| `src/daemon/session.rs` | Set GDB-specific stop flag for gdb/cuda-gdb adapters | + +### Key Functions + +**`has_native_dap_support(cuda_gdb_path)`** in `cuda_gdb.rs`: +```rust +// 1. Check version >= 14.1 +// 2. Run: cuda-gdb -i=dap -batch -ex quit +// 3. Check stderr for "Interpreter `dap' unrecognized" +// 4. Return true if no error (native DAP available) +``` + +**`parse_gdb_version(output)`** in `gdb_common.rs`: +```rust +// Skip "exec:" wrapper lines +// Search for "GNU gdb X.Y" pattern +// Return X.Y as version string +``` + +## Decision Log + +| Decision | Reasoning | +|----------|-----------| +| Native DAP preferred over bridge | Zero dependencies, direct control, better performance | +| Fallback to cdt-gdb-adapter | Arch Linux and similar minimal builds lack DAP Python bindings | +| Auto-detect mode at setup time | User doesn't need to know which mode is available | +| Use `stopAtBeginningOfMainSubprogram` for GDB | GDB's DAP implementation uses this parameter, not `stopOnEntry` | +| Version check extracts GDB base version | cuda-gdb version (13.1) differs from GDB base (14.2) | + +## Known Limitations + +1. **cdt-gdb-adapter stop-on-entry**: Not supported. Use `--break main` as workaround. +2. **GPU compute capability**: CUDA 13.1 requires sm_75+ (Turing or newer). Older GPUs cannot run CUDA code. +3. **Kernel debugging context**: Breakpoints in kernels may show CPU-side context during `cudaDeviceSynchronize`. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cdbdf61..a259e7f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -27,24 +27,32 @@ pub async fn dispatch(command: Commands) -> Result<()> { args, adapter, stop_on_entry, + initial_breakpoints, } => { spawn::ensure_daemon_running().await?; let mut client = DaemonClient::connect().await?; let program = program.canonicalize().unwrap_or(program); - let result = client + let has_initial_breakpoints = !initial_breakpoints.is_empty(); + + let _result = client .send_command(Command::Start { program: program.clone(), args, adapter, stop_on_entry, + initial_breakpoints: initial_breakpoints.clone(), }) .await?; println!("Started debugging: {}", program.display()); - if stop_on_entry { + if has_initial_breakpoints { + println!("Set {} initial breakpoint(s)", initial_breakpoints.len()); + } + + if stop_on_entry || has_initial_breakpoints { println!("Stopped at entry point. Use 'debugger continue' to run."); } else { println!("Program is running. Use 'debugger await' to wait for a stop."); diff --git a/src/commands.rs b/src/commands.rs index 583fda9..cd161fb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -23,6 +23,11 @@ pub enum Commands { /// Stop at program entry point #[arg(long)] stop_on_entry: bool, + + /// Set initial breakpoint(s) before program starts (file:line or function name) + /// Can be specified multiple times: --break main --break src/file.c:42 + #[arg(long = "break", short = 'b')] + initial_breakpoints: Vec, }, /// Attach to a running process diff --git a/src/daemon/handler.rs b/src/daemon/handler.rs index 94d1c52..93dc45d 100644 --- a/src/daemon/handler.rs +++ b/src/daemon/handler.rs @@ -57,13 +57,14 @@ async fn handle_command_inner( args, adapter, stop_on_entry, + initial_breakpoints, } => { if session.is_some() { return Err(Error::SessionAlreadyActive); } let new_session = - DebugSession::launch(config, &program, args, adapter, stop_on_entry).await?; + DebugSession::launch(config, &program, args, adapter, stop_on_entry, initial_breakpoints).await?; *session = Some(new_session); Ok(json!({ @@ -242,7 +243,7 @@ async fn handle_command_inner( } // === State Inspection === - Command::StackTrace { thread_id, limit } => { + Command::StackTrace { thread_id: _, limit } => { let sess = session.as_mut().ok_or(Error::SessionNotActive)?; let frames = sess.stack_trace(limit).await?; diff --git a/src/daemon/session.rs b/src/daemon/session.rs index 8f795bb..8a1bb06 100644 --- a/src/daemon/session.rs +++ b/src/daemon/session.rs @@ -132,6 +132,7 @@ impl DebugSession { args: Vec, adapter_name: Option, stop_on_entry: bool, + initial_breakpoints: Vec, ) -> Result { let adapter_name = adapter_name.unwrap_or_else(|| config.defaults.adapter.clone()); @@ -199,6 +200,8 @@ impl DebugSession { mode: if is_go { Some("exec".to_string()) } else { None }, // Delve uses stopAtEntry instead of stopOnEntry stop_at_entry: if is_go && stop_on_entry { Some(true) } else { None }, + // GDB-based adapters (gdb, cuda-gdb) use stopAtBeginningOfMainSubprogram + stop_at_beginning_of_main_subprogram: if (adapter_name == "gdb" || adapter_name == "cuda-gdb") && stop_on_entry { Some(true) } else { None }, }; tracing::debug!( @@ -226,6 +229,77 @@ impl DebugSession { client.wait_initialized_with_timeout(request_timeout).await?; tracing::debug!("Received DAP initialized event"); + // Set initial breakpoints before configurationDone + // This is required for adapters that don't support stopOnEntry (e.g., cdt-gdb-adapter) + let has_initial_breakpoints = !initial_breakpoints.is_empty(); + if has_initial_breakpoints { + tracing::debug!(count = initial_breakpoints.len(), "Setting initial breakpoints"); + + // Group breakpoints by type (source vs function) + let mut source_bps: std::collections::HashMap> = std::collections::HashMap::new(); + let mut function_bps: Vec = Vec::new(); + + for bp_str in &initial_breakpoints { + match BreakpointLocation::parse(bp_str) { + Ok(BreakpointLocation::Line { file, line }) => { + source_bps.entry(file).or_default().push(dap::SourceBreakpoint { + line, + column: None, + condition: None, + hit_condition: None, + log_message: None, + }); + } + Ok(BreakpointLocation::Function { name }) => { + function_bps.push(dap::FunctionBreakpoint { + name, + condition: None, + hit_condition: None, + }); + } + Err(e) => { + tracing::warn!(breakpoint = %bp_str, error = %e, "Failed to parse initial breakpoint"); + } + } + } + + // Set source breakpoints + for (file, bps) in source_bps { + match client.set_breakpoints(&file, bps).await { + Ok(results) => { + for bp in results { + tracing::debug!( + verified = bp.verified, + line = bp.line, + "Initial source breakpoint set" + ); + } + } + Err(e) => { + tracing::warn!(file = %file.display(), error = %e, "Failed to set initial breakpoints"); + } + } + } + + // Set function breakpoints + if !function_bps.is_empty() { + match client.set_function_breakpoints(function_bps).await { + Ok(results) => { + for bp in results { + tracing::debug!( + verified = bp.verified, + line = bp.line, + "Initial function breakpoint set" + ); + } + } + Err(e) => { + tracing::warn!(error = %e, "Failed to set initial function breakpoints"); + } + } + } + } + // Signal configuration done - this tells the adapter to start execution tracing::debug!("Sending DAP configurationDone request"); client.configuration_done().await?; @@ -236,6 +310,8 @@ impl DebugSession { .take_event_receiver() .ok_or_else(|| Error::Internal("Failed to get event receiver".to_string()))?; + // Initial state: Stopped if stop_on_entry requested, otherwise Running + // Note: If initial breakpoints are set, the program will stop when it hits them let initial_state = if stop_on_entry { SessionState::Stopped } else { @@ -583,7 +659,7 @@ impl DebugSession { self.next_bp_id += 1; match &location { - BreakpointLocation::Line { file, line } => { + BreakpointLocation::Line { file, line: _ } => { // Add to our tracking let stored = StoredBreakpoint { id: bp_id, @@ -610,7 +686,7 @@ impl DebugSession { let info = self.get_breakpoint_info(bp_id)?; Ok(info) } - BreakpointLocation::Function { name } => { + BreakpointLocation::Function { name: _ } => { let stored = StoredBreakpoint { id: bp_id, location: location.clone(), diff --git a/src/dap/types.rs b/src/dap/types.rs index ae09fcd..fef18ad 100644 --- a/src/dap/types.rs +++ b/src/dap/types.rs @@ -153,6 +153,11 @@ pub struct LaunchArguments { /// Stop at entry point (Delve uses stopAtEntry instead of stopOnEntry) #[serde(skip_serializing_if = "Option::is_none")] pub stop_at_entry: Option, + + // === GDB-based adapters (GDB, CUDA-GDB) === + /// Stop at beginning of main (GDB uses stopAtBeginningOfMainSubprogram instead of stopOnEntry) + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_at_beginning_of_main_subprogram: Option, } /// Attach request arguments diff --git a/src/ipc/protocol.rs b/src/ipc/protocol.rs index 4c74082..0fd1e26 100644 --- a/src/ipc/protocol.rs +++ b/src/ipc/protocol.rs @@ -75,6 +75,9 @@ pub enum Command { args: Vec, adapter: Option, stop_on_entry: bool, + /// Initial breakpoints to set before program starts (file:line or function name) + #[serde(default)] + initial_breakpoints: Vec, }, /// Attach to a running process diff --git a/src/setup/adapters/CLAUDE.md b/src/setup/adapters/CLAUDE.md new file mode 100644 index 0000000..ed08ecb --- /dev/null +++ b/src/setup/adapters/CLAUDE.md @@ -0,0 +1,57 @@ +# Debug Adapter Installers + +| File | What | When | +|------|------|------| +| codelldb.rs | CodeLLDB installer (VS Code LLDB extension) | Setting up Rust/C/C++ debugging | +| cuda_gdb.rs | CUDA-GDB installer for NVIDIA GPU debugging | Setting up CUDA project debugging on Linux | +| debugpy.rs | Python debugger installer | Setting up Python debugging | +| delve.rs | Go debugger installer | Setting up Go debugging | +| gdb.rs | GDB native DAP adapter installer | Setting up C/C++ debugging with GDB ≥14.1 | +| gdb_common.rs | Shared utilities for GDB and CUDA-GDB | Version parsing and validation for GDB-based adapters | +| lldb.rs | LLDB native DAP adapter installer | Setting up C/C++/Rust/Swift debugging | +| mod.rs | Module exports for all adapters | Internal module organization | + +## CUDA-GDB Architecture + +CUDA-GDB supports two modes, automatically detected at setup time: + +| Mode | When Used | Command | +|------|-----------|---------| +| Native DAP | cuda-gdb with GDB 14.1+ and DAP Python bindings (NVIDIA official installs) | `cuda-gdb -i=dap` | +| cdt-gdb-adapter bridge | cuda-gdb without native DAP (e.g., Arch Linux minimal build) | `cdtDebugAdapter --config={"gdb":"/path/to/cuda-gdb"}` | + +### Stop-on-Entry Behavior + +Different adapters use different parameters for stop-on-entry: + +| Adapter | Parameter | Notes | +|---------|-----------|-------| +| lldb-dap | `stopOnEntry: true` | Standard DAP | +| GDB native DAP | `stopAtBeginningOfMainSubprogram: true` | GDB-specific | +| Delve (Go) | `stopAtEntry: true` | Delve-specific | +| cdt-gdb-adapter | Not supported | Use `--break main` instead | + +### Initial Breakpoints + +For adapters that don't support stop-on-entry, use the `--break` flag: + +```bash +debugger start ./program --break main +debugger start ./program --break vectorAdd --break main.cu:42 +``` + +Initial breakpoints are set during the DAP configuration phase (between `initialized` event and `configurationDone`), ensuring they're active before program execution begins. + +## Key Functions + +### `gdb_common.rs` + +- `parse_gdb_version(output)`: Extracts GDB version from `--version` output. Handles cuda-gdb's "exec:" wrapper line. +- `is_gdb_version_sufficient(version)`: Checks if version ≥14.1 for DAP support. +- `get_gdb_version(path)`: Async helper that runs `--version` and parses output. + +### `cuda_gdb.rs` + +- `has_native_dap_support(path)`: Tests if cuda-gdb supports `-i=dap` by checking for "Interpreter `dap' unrecognized" error. +- `find_cuda_gdb()`: Searches versioned CUDA installs, `/usr/local/cuda`, `/opt/cuda`, `CUDA_HOME`, then PATH. +- `find_cdt_gdb_adapter()`: Searches PATH, nvm installs, npm global directories. diff --git a/src/setup/adapters/README.md b/src/setup/adapters/README.md new file mode 100644 index 0000000..f5d49ab --- /dev/null +++ b/src/setup/adapters/README.md @@ -0,0 +1,42 @@ +# Debug Adapter Installers + +This directory contains installer implementations for various debug adapters. Each adapter implements the `Installer` trait and handles detection, installation, and verification of debugger binaries. + +## GDB and CUDA-GDB + +### Native DAP vs MI Adapter + +GDB ≥14.1 includes native DAP support via the `-i=dap` interpreter flag. This implementation uses native DAP rather than the MI (Machine Interface) adapter approach for three reasons: + +1. **Zero dependencies**: Native DAP requires only the GDB binary, while cdt-gdb-adapter requires Node.js runtime (50MB+ dependency) +2. **Simpler integration**: Native DAP uses stdin/stdout transport identical to lldb-dap, reusing existing `DapClient::spawn()` patterns +3. **Future-proof**: NVIDIA CUDA Toolkit ships CUDA-GDB based on GDB 14.2, inheriting native DAP support from upstream + +The `-i=dap` flag must be passed at startup; GDB cannot switch interpreters mid-session. + +### Version Requirements + +GDB native DAP requires Python support, added in GDB 14.1. Installers verify version at setup time and return `Broken` status for older versions with upgrade instructions. + +CUDA-GDB 13.x is based on GDB 14.2 and inherits DAP support. The installer validates DAP availability during verification via `verify_dap_adapter()`. + +### CUDA Toolkit Path Detection + +CUDA-GDB installer searches three locations in priority order: + +1. `/usr/local/cuda/bin/cuda-gdb` - NVIDIA's standard installation path (checked first to catch default installations) +2. `$CUDA_HOME/bin/cuda-gdb` - Custom toolkit installations via environment variable +3. `cuda-gdb` in PATH - Fallback for wrapper scripts and non-standard setups + +This order prioritizes official NVIDIA installations over custom configurations. + +### Separate Adapters for GDB vs CUDA-GDB + +Despite sharing 90% of implementation patterns, GDB and CUDA-GDB use separate adapters because they differ in: + +- **Platform support**: GDB works on Linux/macOS/Windows, CUDA-GDB GPU debugging requires Linux (NVIDIA driver limitation) +- **Path detection**: GDB found in PATH, CUDA-GDB in CUDA Toolkit locations +- **Language mapping**: GDB for C/C++, CUDA-GDB for CUDA (may overlap with C/C++) +- **Version requirements**: GDB ≥14.1, CUDA-GDB tied to CUDA Toolkit version + +Shared logic (version parsing, validation) lives in `gdb_common.rs`. diff --git a/src/setup/adapters/cuda_gdb.rs b/src/setup/adapters/cuda_gdb.rs new file mode 100644 index 0000000..14edd57 --- /dev/null +++ b/src/setup/adapters/cuda_gdb.rs @@ -0,0 +1,317 @@ +//! CUDA-GDB adapter installer +//! +//! CUDA-GDB supports two modes: +//! 1. Native DAP mode (-i=dap): Available in cuda-gdb builds based on GDB 14.1+ +//! when DAP Python bindings are included (NVIDIA official installs) +//! 2. cdt-gdb-adapter bridge: For cuda-gdb builds without native DAP (e.g., Arch Linux minimal) +//! +//! Architecture (native DAP): +//! Client <-> cuda-gdb -i=dap <-> GPU +//! +//! Architecture (cdt-gdb-adapter bridge): +//! Client <-> cdt-gdb-adapter (DAP) <-> cuda-gdb (MI mode) <-> GPU +//! +//! Requirements: +//! - CUDA Toolkit with cuda-gdb (Linux only) +//! - For bridge mode: Node.js runtime + cdt-gdb-adapter npm package + +use crate::common::{Error, Result}; +use crate::setup::installer::{InstallMethod, InstallOptions, InstallResult, InstallStatus, Installer}; +use crate::setup::registry::{DebuggerInfo, Platform}; +use crate::setup::verifier::{verify_dap_adapter, VerifyResult}; +use async_trait::async_trait; +use std::path::PathBuf; + +use super::gdb_common::{get_gdb_version, is_gdb_version_sufficient}; + +static INFO: DebuggerInfo = DebuggerInfo { + id: "cuda-gdb", + name: "CUDA-GDB", + languages: &["cuda", "c", "cpp"], + platforms: &[Platform::Linux], + description: "NVIDIA CUDA debugger for GPU code", + primary: true, +}; + +pub struct CudaGdbInstaller; + +/// Check if cuda-gdb supports native DAP mode by testing "-i=dap" +async fn has_native_dap_support(cuda_gdb_path: &PathBuf) -> bool { + // First check version - needs GDB 14.1+ base + if let Some(version) = get_gdb_version(cuda_gdb_path).await { + if !is_gdb_version_sufficient(&version) { + return false; + } + } else { + return false; + } + + // Test if DAP interpreter is available + // cuda-gdb without DAP will fail with "Interpreter `dap' unrecognized" + let output = tokio::process::Command::new(cuda_gdb_path) + .args(["-i=dap", "-batch", "-ex", "quit"]) + .output() + .await; + + match output { + Ok(result) => { + // Check stderr for "unrecognized" error + let stderr = String::from_utf8_lossy(&result.stderr); + !stderr.contains("unrecognized") && !stderr.contains("Interpreter") + } + Err(_) => false, + } +} + +#[async_trait] +impl Installer for CudaGdbInstaller { + fn info(&self) -> &DebuggerInfo { + &INFO + } + + async fn status(&self) -> Result { + if Platform::current() != Platform::Linux { + return Ok(InstallStatus::NotInstalled); + } + + // Check for cuda-gdb + let Some(cuda_gdb_path) = find_cuda_gdb() else { + return Ok(InstallStatus::NotInstalled); + }; + + let version = get_gdb_version(&cuda_gdb_path).await; + + // Check for native DAP support first (preferred) + if has_native_dap_support(&cuda_gdb_path).await { + return Ok(InstallStatus::Installed { + path: cuda_gdb_path, + version, + }); + } + + // Fall back to cdt-gdb-adapter bridge + if let Some(cdt_adapter) = find_cdt_gdb_adapter() { + return Ok(InstallStatus::Installed { + path: cdt_adapter, + version, + }); + } + + // cuda-gdb exists but no DAP method available + Ok(InstallStatus::Broken { + path: cuda_gdb_path, + reason: "cuda-gdb found but lacks native DAP support. Install cdt-gdb-adapter: npm install -g cdt-gdb-adapter".to_string(), + }) + } + + async fn best_method(&self) -> Result { + if Platform::current() != Platform::Linux { + return Ok(InstallMethod::NotSupported { + reason: "CUDA-GDB GPU debugging is only supported on Linux".to_string(), + }); + } + + // Check for cuda-gdb + let Some(cuda_gdb_path) = find_cuda_gdb() else { + return Ok(InstallMethod::NotSupported { + reason: "CUDA-GDB not found. Install NVIDIA CUDA Toolkit from https://developer.nvidia.com/cuda-downloads".to_string(), + }); + }; + + // Check for native DAP support first (preferred) + if has_native_dap_support(&cuda_gdb_path).await { + return Ok(InstallMethod::AlreadyInstalled { path: cuda_gdb_path }); + } + + // Fall back to cdt-gdb-adapter bridge + if let Some(cdt_adapter) = find_cdt_gdb_adapter() { + return Ok(InstallMethod::AlreadyInstalled { path: cdt_adapter }); + } + + Ok(InstallMethod::NotSupported { + reason: "cuda-gdb lacks native DAP support. Install cdt-gdb-adapter: npm install -g cdt-gdb-adapter".to_string(), + }) + } + + async fn install(&self, _opts: InstallOptions) -> Result { + let method = self.best_method().await?; + + match method { + InstallMethod::AlreadyInstalled { path } => { + let cuda_gdb_path = find_cuda_gdb().ok_or_else(|| { + Error::Internal("CUDA-GDB not found".to_string()) + })?; + let version = get_gdb_version(&cuda_gdb_path).await; + + // Determine if using native DAP or bridge mode + if has_native_dap_support(&cuda_gdb_path).await { + // Native DAP mode + Ok(InstallResult { + path: cuda_gdb_path, + version, + args: vec!["-i=dap".to_string()], + }) + } else { + // cdt-gdb-adapter bridge mode + Ok(InstallResult { + path, + version, + args: vec![format!("--config={{\"gdb\":\"{}\"}}", cuda_gdb_path.display())], + }) + } + } + InstallMethod::NotSupported { reason } => { + Err(Error::Internal(format!("Cannot install CUDA-GDB: {}", reason))) + } + _ => Err(Error::Internal("Unexpected installation method".to_string())), + } + } + + async fn uninstall(&self) -> Result<()> { + println!("CUDA-GDB is part of NVIDIA CUDA Toolkit. Uninstall the toolkit to remove it."); + Ok(()) + } + + async fn verify(&self) -> Result { + let status = self.status().await?; + + match status { + InstallStatus::Installed { path, .. } => { + let cuda_gdb_path = find_cuda_gdb().ok_or_else(|| { + Error::Internal("CUDA-GDB not found".to_string()) + })?; + + // Determine verification args based on mode + if has_native_dap_support(&cuda_gdb_path).await { + // Native DAP mode + verify_dap_adapter(&path, &["-i=dap".to_string()]).await + } else { + // cdt-gdb-adapter bridge mode + verify_dap_adapter( + &path, + &[format!("--config={{\"gdb\":\"{}\"}}", cuda_gdb_path.display())], + ).await + } + } + InstallStatus::Broken { reason, .. } => Ok(VerifyResult { + success: false, + capabilities: None, + error: Some(reason), + }), + InstallStatus::NotInstalled => Ok(VerifyResult { + success: false, + capabilities: None, + error: Some("Not installed".to_string()), + }), + } + } +} + +/// Locates cuda-gdb binary using NVIDIA Toolkit path conventions +/// +/// Search order: versioned CUDA installs → /usr/local/cuda → /opt/cuda → CUDA_HOME → PATH +fn find_cuda_gdb() -> Option { + // Check versioned CUDA installs (e.g., /usr/local/cuda-13.1) + // Prefer higher versions which are more likely to have DAP support + if let Ok(entries) = std::fs::read_dir("/usr/local") { + let mut cuda_paths: Vec<_> = entries + .flatten() + .filter_map(|e| { + let name = e.file_name().to_string_lossy().to_string(); + if name.starts_with("cuda-") { + let cuda_gdb = e.path().join("bin/cuda-gdb"); + if cuda_gdb.exists() { + // Extract version for sorting (e.g., "13.1" from "cuda-13.1") + let version = name.strip_prefix("cuda-").unwrap_or("0.0").to_string(); + return Some((version, cuda_gdb)); + } + } + None + }) + .collect(); + + // Sort by version descending (higher versions first) + cuda_paths.sort_by(|a, b| { + let parse_version = |s: &str| -> (u32, u32) { + let parts: Vec<&str> = s.split('.').collect(); + let major = parts.first().and_then(|p| p.parse().ok()).unwrap_or(0); + let minor = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0); + (major, minor) + }; + parse_version(&b.0).cmp(&parse_version(&a.0)) + }); + + if let Some((_, path)) = cuda_paths.first() { + return Some(path.clone()); + } + } + + // NVIDIA's standard install location (symlink to versioned install) + let default_path = PathBuf::from("/usr/local/cuda/bin/cuda-gdb"); + if default_path.exists() { + return Some(default_path); + } + + // Arch Linux installs to /opt/cuda + let arch_path = PathBuf::from("/opt/cuda/bin/cuda-gdb"); + if arch_path.exists() { + return Some(arch_path); + } + + // CUDA_HOME environment variable + if let Ok(cuda_home) = std::env::var("CUDA_HOME") { + let cuda_home_path = PathBuf::from(cuda_home).join("bin/cuda-gdb"); + if cuda_home_path.exists() { + return Some(cuda_home_path); + } + } + + // Fall back to PATH + which::which("cuda-gdb").ok() +} + +/// Locates cdt-gdb-adapter (cdtDebugAdapter) binary +/// +/// Searches npm global bin directories and common locations +fn find_cdt_gdb_adapter() -> Option { + // Check PATH first + if let Ok(path) = which::which("cdtDebugAdapter") { + return Some(path); + } + + // Check common npm global bin locations + if let Ok(home) = std::env::var("HOME") { + // nvm installations + let nvm_path = PathBuf::from(&home).join(".nvm/versions/node"); + if nvm_path.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_path) { + for entry in entries.flatten() { + let bin_path = entry.path().join("bin/cdtDebugAdapter"); + if bin_path.exists() { + return Some(bin_path); + } + } + } + } + + // Standard npm global + let npm_global = PathBuf::from(&home).join(".npm-global/bin/cdtDebugAdapter"); + if npm_global.exists() { + return Some(npm_global); + } + + // npm prefix bin + let npm_prefix = PathBuf::from(&home).join("node_modules/.bin/cdtDebugAdapter"); + if npm_prefix.exists() { + return Some(npm_prefix); + } + } + + // System-wide npm + let system_path = PathBuf::from("/usr/local/bin/cdtDebugAdapter"); + if system_path.exists() { + return Some(system_path); + } + + None +} diff --git a/src/setup/adapters/gdb.rs b/src/setup/adapters/gdb.rs new file mode 100644 index 0000000..4a12ba1 --- /dev/null +++ b/src/setup/adapters/gdb.rs @@ -0,0 +1,117 @@ +//! GDB native DAP adapter installer +//! +//! Installs GDB with native DAP support (GDB ≥14.1). + +use crate::common::{Error, Result}; +use crate::setup::installer::{InstallMethod, InstallOptions, InstallResult, InstallStatus, Installer}; +use crate::setup::registry::{DebuggerInfo, Platform}; +use crate::setup::verifier::{verify_dap_adapter, VerifyResult}; +use async_trait::async_trait; + +use super::gdb_common::{get_gdb_version, is_gdb_version_sufficient}; + +static INFO: DebuggerInfo = DebuggerInfo { + id: "gdb", + name: "GDB", + languages: &["c", "cpp"], + platforms: &[Platform::Linux, Platform::MacOS, Platform::Windows], + description: "GDB native DAP adapter", + primary: true, +}; + +pub struct GdbInstaller; + +#[async_trait] +impl Installer for GdbInstaller { + fn info(&self) -> &DebuggerInfo { + &INFO + } + + async fn status(&self) -> Result { + if let Ok(path) = which::which("gdb") { + match get_gdb_version(&path).await { + Some(version) if is_gdb_version_sufficient(&version) => { + return Ok(InstallStatus::Installed { + path, + version: Some(version), + }); + } + Some(version) => { + return Ok(InstallStatus::Broken { + path, + reason: format!( + "GDB version {} found, but ≥14.1 required for native DAP support", + version + ), + }); + } + None => { + return Ok(InstallStatus::Broken { + path, + reason: "Could not determine GDB version".to_string(), + }); + } + } + } + + Ok(InstallStatus::NotInstalled) + } + + async fn best_method(&self) -> Result { + if let Ok(path) = which::which("gdb") { + if let Some(version) = get_gdb_version(&path).await { + if is_gdb_version_sufficient(&version) { + return Ok(InstallMethod::AlreadyInstalled { path }); + } + } + } + + Ok(InstallMethod::NotSupported { + reason: "GDB ≥14.1 not found. Install via your system package manager.".to_string(), + }) + } + + async fn install(&self, _opts: InstallOptions) -> Result { + let method = self.best_method().await?; + + match method { + InstallMethod::AlreadyInstalled { path } => { + let version = get_gdb_version(&path).await; + Ok(InstallResult { + path, + version, + args: vec!["-i=dap".to_string()], + }) + } + InstallMethod::NotSupported { reason } => { + Err(Error::Internal(format!("Cannot install GDB: {}", reason))) + } + _ => Err(Error::Internal("Unexpected installation method".to_string())), + } + } + + async fn uninstall(&self) -> Result<()> { + println!("GDB is a system package. Use your package manager to uninstall."); + Ok(()) + } + + async fn verify(&self) -> Result { + let status = self.status().await?; + + match status { + InstallStatus::Installed { path, .. } => { + verify_dap_adapter(&path, &["-i=dap".to_string()]).await + } + InstallStatus::Broken { reason, .. } => Ok(VerifyResult { + success: false, + capabilities: None, + error: Some(reason), + }), + InstallStatus::NotInstalled => Ok(VerifyResult { + success: false, + capabilities: None, + error: Some("Not installed".to_string()), + }), + } + } +} diff --git a/src/setup/adapters/gdb_common.rs b/src/setup/adapters/gdb_common.rs new file mode 100644 index 0000000..1188cc5 --- /dev/null +++ b/src/setup/adapters/gdb_common.rs @@ -0,0 +1,76 @@ +//! Shared utilities for GDB-based adapters (GDB and CUDA-GDB) + +/// Extracts version string from GDB --version output +/// +/// Searches for "GNU gdb" line and extracts the version number. +/// Handles cuda-gdb output which may have "exec:" wrapper on first line. +pub fn parse_gdb_version(output: &str) -> Option { + for line in output.lines() { + // Skip cuda-gdb exec wrapper line + if line.starts_with("exec:") { + continue; + } + // Look for "GNU gdb X.Y" pattern to get the base GDB version + if line.contains("GNU gdb") { + let parts: Vec<&str> = line.split_whitespace().collect(); + for (i, part) in parts.iter().enumerate() { + if *part == "gdb" { + if let Some(version) = parts.get(i + 1) { + // Verify it starts with a digit (version number) + if version.chars().next().map_or(false, |c| c.is_ascii_digit()) { + return Some(version.to_string()); + } + } + } + } + } + } + // Fallback: try first line with digit token (for non-GDB outputs) + output + .lines() + .next() + .and_then(|line| { + line.split_whitespace() + .find(|token| token.chars().next().map_or(false, |c| c.is_ascii_digit())) + }) + .map(|s| s.to_string()) +} + +/// Checks if GDB version meets DAP support requirement (≥14.1) +/// +/// Returns false on parse failure to prevent launching incompatible GDB +pub fn is_gdb_version_sufficient(version: &str) -> bool { + let parts: Vec<&str> = version.split('.').collect(); + let Some(major_str) = parts.get(0) else { + return false; + }; + let Some(minor_str) = parts.get(1) else { + return false; + }; + let Ok(major) = major_str.parse::() else { + return false; + }; + let Ok(minor) = minor_str.parse::() else { + return false; + }; + + major > 14 || (major == 14 && minor >= 1) +} + +/// Retrieves GDB version by executing --version flag +/// +/// Returns None on exec failure or unparseable output +pub async fn get_gdb_version(path: &std::path::PathBuf) -> Option { + let output = tokio::process::Command::new(path) + .arg("--version") + .output() + .await + .ok()?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + parse_gdb_version(&stdout) + } else { + None + } +} diff --git a/src/setup/adapters/mod.rs b/src/setup/adapters/mod.rs index c185270..f3e1995 100644 --- a/src/setup/adapters/mod.rs +++ b/src/setup/adapters/mod.rs @@ -3,6 +3,9 @@ //! Individual installers for each supported debug adapter. pub mod codelldb; +pub mod cuda_gdb; pub mod debugpy; pub mod delve; +pub mod gdb_common; +pub mod gdb; pub mod lldb; diff --git a/src/setup/detector.rs b/src/setup/detector.rs index a0928fe..47917ec 100644 --- a/src/setup/detector.rs +++ b/src/setup/detector.rs @@ -8,6 +8,7 @@ use std::path::Path; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ProjectType { Rust, + Cuda, Go, Python, JavaScript, @@ -27,6 +28,11 @@ pub fn detect_project_types(dir: &Path) -> Vec { types.push(ProjectType::Rust); } + // CUDA detection must precede C/C++ (.cu files are valid C++ but require CUDA-GDB) + if has_extension_in_dir(dir, "cu") { + types.push(ProjectType::Cuda); + } + // Go if dir.join("go.mod").exists() || dir.join("go.sum").exists() { types.push(ProjectType::Go); @@ -88,6 +94,7 @@ pub fn detect_project_types(dir: &Path) -> Vec { pub fn debuggers_for_project(project: &ProjectType) -> Vec<&'static str> { match project { ProjectType::Rust => vec!["codelldb", "lldb"], + ProjectType::Cuda => vec!["cuda-gdb"], ProjectType::Go => vec!["go"], ProjectType::Python => vec!["python"], ProjectType::JavaScript | ProjectType::TypeScript => vec![], // js-debug not yet implemented diff --git a/src/setup/registry.rs b/src/setup/registry.rs index f54a6d7..6323925 100644 --- a/src/setup/registry.rs +++ b/src/setup/registry.rs @@ -60,6 +60,22 @@ pub struct DebuggerInfo { /// All available debuggers static DEBUGGERS: &[DebuggerInfo] = &[ + DebuggerInfo { + id: "gdb", + name: "GDB", + languages: &["c", "cpp"], + platforms: &[Platform::Linux, Platform::MacOS, Platform::Windows], + description: "GDB native DAP adapter", + primary: true, + }, + DebuggerInfo { + id: "cuda-gdb", + name: "CUDA-GDB", + languages: &["cuda", "c", "cpp"], + platforms: &[Platform::Linux], + description: "NVIDIA CUDA debugger with DAP support", + primary: true, + }, DebuggerInfo { id: "lldb", name: "lldb-dap", @@ -124,6 +140,8 @@ pub fn get_installer(id: &str) -> Option> { use super::adapters; match id { + "gdb" => Some(Arc::new(adapters::gdb::GdbInstaller)), + "cuda-gdb" => Some(Arc::new(adapters::cuda_gdb::CudaGdbInstaller)), "lldb" => Some(Arc::new(adapters::lldb::LldbInstaller)), "codelldb" => Some(Arc::new(adapters::codelldb::CodeLldbInstaller)), "python" => Some(Arc::new(adapters::debugpy::DebugpyInstaller)), diff --git a/src/testing/runner.rs b/src/testing/runner.rs index be5f6fa..f0f756a 100644 --- a/src/testing/runner.rs +++ b/src/testing/runner.rs @@ -129,6 +129,7 @@ pub async fn run_scenario(path: &Path, verbose: bool) -> Result { args: scenario.target.args.clone().unwrap_or_default(), adapter: scenario.target.adapter.clone(), stop_on_entry: scenario.target.stop_on_entry, + initial_breakpoints: Vec::new(), }) .await?; diff --git a/tests/fixtures/cuda_test.cu b/tests/fixtures/cuda_test.cu new file mode 100644 index 0000000..d06138d --- /dev/null +++ b/tests/fixtures/cuda_test.cu @@ -0,0 +1,48 @@ +#include +#include + +__global__ void vectorAdd(float *a, float *b, float *c, int n) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; // BREAKPOINT: kernel_entry + if (idx < n) { + float val_a = a[idx]; // BREAKPOINT: kernel_compute + float val_b = b[idx]; + c[idx] = val_a + val_b; + } +} + +int main() { + printf("CUDA Test Program\n"); // BREAKPOINT: main_start + + const int N = 256; + float *h_a = (float*)malloc(N * sizeof(float)); + float *h_b = (float*)malloc(N * sizeof(float)); + float *h_c = (float*)malloc(N * sizeof(float)); + + for (int i = 0; i < N; i++) { // BREAKPOINT: init_loop + h_a[i] = (float)i; + h_b[i] = (float)(i * 2); + } + + float *d_a, *d_b, *d_c; + cudaMalloc(&d_a, N * sizeof(float)); + cudaMalloc(&d_b, N * sizeof(float)); + cudaMalloc(&d_c, N * sizeof(float)); + + cudaMemcpy(d_a, h_a, N * sizeof(float), cudaMemcpyHostToDevice); + cudaMemcpy(d_b, h_b, N * sizeof(float), cudaMemcpyHostToDevice); + + printf("Launching kernel\n"); // BREAKPOINT: before_kernel + vectorAdd<<<1, 256>>>(d_a, d_b, d_c, N); + cudaDeviceSynchronize(); + + cudaMemcpy(h_c, d_c, N * sizeof(float), cudaMemcpyDeviceToHost); // BREAKPOINT: after_kernel + + printf("Result[0] = %f (expected 0)\n", h_c[0]); + printf("Result[100] = %f (expected 300)\n", h_c[100]); + + cudaFree(d_a); cudaFree(d_b); cudaFree(d_c); + free(h_a); free(h_b); free(h_c); + + printf("Done\n"); // BREAKPOINT: main_end + return 0; +} diff --git a/tests/integration.rs b/tests/integration.rs index d69cf5c..76c8663 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -141,11 +141,20 @@ impl TestContext { /// Create a config file for the test fn create_config(&self, adapter_name: &str, adapter_path: &str) { + self.create_config_with_args(adapter_name, adapter_path, &[]); + } + + /// Create a config file for the test with custom args + fn create_config_with_args(&self, adapter_name: &str, adapter_path: &str, args: &[&str]) { + let args_str = args.iter() + .map(|a| format!("\"{}\"", a)) + .collect::>() + .join(", "); let config_content = format!( r#" [adapters.{adapter_name}] path = "{adapter_path}" -args = [] +args = [{args_str}] [defaults] adapter = "{adapter_name}" @@ -164,6 +173,7 @@ max_bytes_mb = 1 "#, adapter_name = adapter_name, adapter_path = adapter_path, + args_str = args_str, ); let config_path = self.config_dir.join("debugger-cli").join("config.toml"); @@ -281,6 +291,50 @@ fn lldb_dap_available() -> Option { None } +/// Checks if GDB ≥14.1 is available for testing +/// +/// Returns path only if version meets DAP support requirement +fn gdb_available() -> Option { + use debugger::setup::adapters::gdb_common::{parse_gdb_version, is_gdb_version_sufficient}; + + let path = which::which("gdb").ok()?; + + let output = std::process::Command::new(&path) + .arg("--version") + .output() + .ok()?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let version = parse_gdb_version(&stdout)?; + + if is_gdb_version_sufficient(&version) { + return Some(path); + } + } + + None +} + +/// Checks if cuda-gdb is available for testing +/// +/// Uses same path search as CudaGdbInstaller::find_cuda_gdb() +fn cuda_gdb_available() -> Option { + let default_path = PathBuf::from("/usr/local/cuda/bin/cuda-gdb"); + if default_path.exists() { + return Some(default_path); + } + + if let Ok(cuda_home) = std::env::var("CUDA_HOME") { + let cuda_home_path = PathBuf::from(cuda_home).join("bin/cuda-gdb"); + if cuda_home_path.exists() { + return Some(cuda_home_path); + } + } + + which::which("cuda-gdb").ok() +} + // ============== Tests ============== #[test] @@ -413,6 +467,80 @@ fn test_basic_debugging_workflow_c() { let _ = ctx.run_debugger(&["stop"]); } +#[test] +#[ignore = "GDB DAP mode has different stopOnEntry behavior than LLDB"] +fn test_basic_debugging_workflow_c_gdb() { + let gdb_path = match gdb_available() { + Some(path) => path, + None => { + eprintln!("Skipping test: GDB ≥14.1 not available"); + return; + } + }; + + let mut ctx = TestContext::new("basic_workflow_c_gdb"); + ctx.create_config_with_args("gdb", gdb_path.to_str().unwrap(), &["-i=dap"]); + + // Build the C fixture + let binary = ctx.build_c_fixture("simple").clone(); + + // Find breakpoint markers + let markers = ctx.find_breakpoint_markers(&ctx.fixtures_dir.join("simple.c")); + let main_start_line = markers.get("main_start").expect("Missing main_start marker"); + + // Cleanup any existing daemon + ctx.cleanup_daemon(); + + // Start debugging + let output = ctx.run_debugger_ok(&[ + "start", + binary.to_str().unwrap(), + "--stop-on-entry", + ]); + assert!(output.contains("Started debugging") || output.contains("Stopped")); + + // Set a breakpoint + let bp_location = format!("simple.c:{}", main_start_line); + let output = ctx.run_debugger_ok(&["break", &bp_location]); + assert!(output.contains("Breakpoint") || output.contains("breakpoint")); + + // Continue execution + let output = ctx.run_debugger_ok(&["continue"]); + assert!(output.contains("Continuing") || output.contains("running")); + + // Wait for breakpoint hit + let output = ctx.run_debugger_ok(&["await", "--timeout", "30"]); + assert!( + output.contains("Stopped") || output.contains("breakpoint"), + "Expected stop at breakpoint: {}", + output + ); + + // Get local variables + let output = ctx.run_debugger_ok(&["locals"]); + assert!( + output.contains("x") || output.contains("Local"), + "Expected locals output: {}", + output + ); + + // Stop the session + let _ = ctx.run_debugger(&["stop"]); +} + +#[test] +fn test_cuda_gdb_adapter_available() { + let cuda_gdb_path = match cuda_gdb_available() { + Some(path) => path, + None => { + eprintln!("Skipping test: CUDA-GDB not available"); + return; + } + }; + + assert!(cuda_gdb_path.exists(), "CUDA-GDB path should exist"); +} + #[test] #[ignore = "requires lldb-dap"] fn test_stepping_c() { @@ -444,7 +572,7 @@ fn test_stepping_c() { // Step into add() ctx.run_debugger_ok(&["step"]); - let output = ctx.run_debugger_ok(&["await", "--timeout", "10"]); + let _output = ctx.run_debugger_ok(&["await", "--timeout", "10"]); // Get context to verify we're in add() let output = ctx.run_debugger_ok(&["backtrace"]); @@ -669,7 +797,7 @@ fn test_output_capture_c() { ctx.run_debugger_ok(&["start", binary.to_str().unwrap()]); // Wait for program to finish - let output = ctx.run_debugger(&["await", "--timeout", "30"]); + let _output = ctx.run_debugger(&["await", "--timeout", "30"]); // Get output let output = ctx.run_debugger_ok(&["output"]);