debugger beta: Fix gdb/delve JSON data conversion from New Session Modal (#31501)

test that check's that each conversion works properly based on the
adapter's config validation function. 

Co-authored-by: Zed AI \<ai@zed.dev\>

Release Notes:

- debugger beta: Fix bug where Go/GDB configuration's wouldn't work from
NewSessionModal
This commit is contained in:
Anthony Eid 2025-05-27 20:28:41 +03:00 committed by GitHub
parent b7c5540075
commit b63cea1f17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 134 additions and 44 deletions

View file

@ -26,10 +26,12 @@ impl DebugAdapter for GdbDebugAdapter {
match &zed_scenario.request { match &zed_scenario.request {
dap::DebugRequest::Attach(attach) => { dap::DebugRequest::Attach(attach) => {
obj.insert("request".into(), "attach".into());
obj.insert("pid".into(), attach.process_id.into()); obj.insert("pid".into(), attach.process_id.into());
} }
dap::DebugRequest::Launch(launch) => { dap::DebugRequest::Launch(launch) => {
obj.insert("request".into(), "launch".into());
obj.insert("program".into(), launch.program.clone().into()); obj.insert("program".into(), launch.program.clone().into());
if !launch.args.is_empty() { if !launch.args.is_empty() {

View file

@ -307,10 +307,14 @@ impl DebugAdapter for GoDebugAdapter {
let mut args = match &zed_scenario.request { let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => { dap::DebugRequest::Attach(attach_config) => {
json!({ json!({
"request": "attach",
"mode": "debug",
"processId": attach_config.process_id, "processId": attach_config.process_id,
}) })
} }
dap::DebugRequest::Launch(launch_config) => json!({ dap::DebugRequest::Launch(launch_config) => json!({
"request": "launch",
"mode": "debug",
"program": launch_config.program, "program": launch_config.program,
"cwd": launch_config.cwd, "cwd": launch_config.cwd,
"args": launch_config.args, "args": launch_config.args,

View file

@ -47,13 +47,6 @@ impl PhpDebugAdapter {
}) })
} }
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
async fn get_installed_binary( async fn get_installed_binary(
&self, &self,
delegate: &Arc<dyn DapDelegate>, delegate: &Arc<dyn DapDelegate>,
@ -101,7 +94,7 @@ impl PhpDebugAdapter {
envs: HashMap::default(), envs: HashMap::default(),
request_args: StartDebuggingRequestArguments { request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(), configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?, request: <Self as DebugAdapter>::validate_config(self, &task_definition.config)?,
}, },
}) })
} }
@ -303,6 +296,13 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into()) Some(SharedString::new_static("PHP").into())
} }
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> { fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let obj = match &zed_scenario.request { let obj = match &zed_scenario.request {
dap::DebugRequest::Attach(_) => { dap::DebugRequest::Attach(_) => {

View file

@ -25,7 +25,6 @@ mod inline_values;
#[cfg(test)] #[cfg(test)]
mod module_list; mod module_list;
#[cfg(test)] #[cfg(test)]
#[cfg(not(windows))]
mod new_session_modal; mod new_session_modal;
#[cfg(test)] #[cfg(test)]
mod persistence; mod persistence;

View file

@ -1,14 +1,14 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugScenario, TaskContext, VariableName}; use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use util::path; use util::path;
use crate::tests::{init_test, init_test_workspace}; use crate::tests::{init_test, init_test_workspace};
// todo(tasks) figure out why task replacement is broken on windows
#[gpui::test] #[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths( async fn test_debug_session_substitutes_variables_and_relativizes_paths(
executor: BackgroundExecutor, executor: BackgroundExecutor,
@ -29,10 +29,9 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
let workspace = init_test_workspace(&project, cx).await; let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx); let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Set up task variables to simulate a real environment
let test_variables = vec![( let test_variables = vec![(
VariableName::WorktreeRoot, VariableName::WorktreeRoot,
"/test/worktree/path".to_string(), path!("/test/worktree/path").to_string(),
)] )]
.into_iter() .into_iter()
.collect(); .collect();
@ -45,33 +44,35 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
let home_dir = paths::home_dir(); let home_dir = paths::home_dir();
let sep = std::path::MAIN_SEPARATOR; let test_cases: Vec<(&'static str, &'static str)> = vec![
// Test cases for different path formats
let test_cases: Vec<(Arc<String>, Arc<String>)> = vec![
// Absolute path - should not be relativized // Absolute path - should not be relativized
( (
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)), path!("/absolute/path/to/program"),
Arc::from(format!("{0}absolute{0}path{0}to{0}program", sep)), path!("/absolute/path/to/program"),
), ),
// Relative path - should be prefixed with worktree root // Relative path - should be prefixed with worktree root
( (
Arc::from(format!(".{0}src{0}program", sep)), format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)), path!("/test/worktree/path/src/program"),
), ),
// Home directory path - should be prefixed with worktree root // Home directory path - should be expanded to full home directory path
( (
Arc::from(format!("~{0}src{0}program", sep)), format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
Arc::from(format!( home_dir
"{1}{0}src{0}program", .join("src")
sep, .join("program")
home_dir.to_string_lossy() .to_string_lossy()
)), .to_string()
.leak(),
), ),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
( (
Arc::from(format!("$ZED_WORKTREE_ROOT{0}src{0}program", sep)), format!(
Arc::from(format!("{0}test{0}worktree{0}path{0}src{0}program", sep)), "$ZED_WORKTREE_ROOT{0}src{0}program",
std::path::MAIN_SEPARATOR
)
.leak(),
path!("/test/worktree/path/src/program"),
), ),
]; ];
@ -80,44 +81,38 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
for (input_path, expected_path) in test_cases { for (input_path, expected_path) in test_cases {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, { let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let called_launch = called_launch.clone(); let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |client| { move |client| {
client.on_request::<dap::requests::Launch, _>({ client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone(); let called_launch = called_launch.clone();
let input_path = input_path.clone();
let expected_path = expected_path.clone();
move |_, args| { move |_, args| {
let config = args.raw.as_object().unwrap(); let config = args.raw.as_object().unwrap();
// Verify the program path was substituted correctly
assert_eq!( assert_eq!(
config["program"].as_str().unwrap(), config["program"].as_str().unwrap(),
expected_path.as_str(), expected_path,
"Program path was not correctly substituted for input: {}", "Program path was not correctly substituted for input: {}",
input_path.as_str() input_path
); );
// Verify the cwd path was substituted correctly
assert_eq!( assert_eq!(
config["cwd"].as_str().unwrap(), config["cwd"].as_str().unwrap(),
expected_path.as_str(), expected_path,
"CWD path was not correctly substituted for input: {}", "CWD path was not correctly substituted for input: {}",
input_path.as_str() input_path
); );
// Verify that otherField was substituted but not relativized
// It should still have $ZED_WORKTREE_ROOT substituted if present
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path.replace("$ZED_WORKTREE_ROOT", "/test/worktree/path") input_path
.replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path"))
.to_owned()
} else { } else {
input_path.to_string() input_path.to_string()
}; };
assert_eq!( assert_eq!(
config["otherField"].as_str().unwrap(), config["otherField"].as_str().unwrap(),
expected_other_field, &expected_other_field,
"Other field was incorrectly modified for input: {}", "Other field was incorrectly modified for input: {}",
input_path input_path
); );
@ -155,3 +150,93 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
called_launch.store(false, Ordering::SeqCst); called_launch.store(false, Ordering::SeqCst);
} }
} }
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
init_test(cx);
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
"PHP",
"JavaScript",
"Ruby",
"Delve",
"GDB",
"fake-adapter",
];
let adapter_names = cx.update(|cx| {
let registry = DapRegistry::global(cx);
registry.enumerate_adapters()
});
let zed_config = ZedDebugConfig {
label: "test_debug_session".into(),
adapter: "test_adapter".into(),
request: DebugRequest::Launch(LaunchRequest {
program: "test_program".into(),
cwd: None,
args: vec![],
env: Default::default(),
}),
stop_on_entry: Some(true),
};
for adapter_name in adapter_names {
let adapter_str = adapter_name.to_string();
if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
expected_adapters.remove(pos);
}
let adapter = cx
.update(|cx| {
let registry = DapRegistry::global(cx);
registry.adapter(adapter_name.as_ref())
})
.unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
let mut adapter_specific_config = zed_config.clone();
adapter_specific_config.adapter = adapter_name.to_string().into();
let debug_scenario = adapter
.config_from_zed_format(adapter_specific_config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should successfully convert from Zed format",
adapter_name
)
});
assert!(
debug_scenario.config.is_object(),
"Adapter {} should produce a JSON object for config",
adapter_name
);
let request_type = adapter
.validate_config(&debug_scenario.config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should validate the config successfully",
adapter_name
)
});
match request_type {
dap::StartDebuggingRequestArgumentsRequest::Launch => {}
dap::StartDebuggingRequestArgumentsRequest::Attach => {
panic!(
"Expected Launch request but got Attach for adapter {}",
adapter_name
);
}
}
}
assert!(
expected_adapters.is_empty(),
"The following expected adapters were not found in the registry: {:?}",
expected_adapters
);
}