use dap::DapRegistry;use editor::Editor;use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};use project::{FakeFs, Fs as _, Project};use serde_json::json;use std::sync::Arc;use std::sync::atomic::{AtomicBool, Ordering};use task::{ DebugRequest, DebugScenario, LaunchRequest, SharedTaskContext, TaskContext, VariableName, ZedDebugConfig,};use text::Point;use util::path; use crate::NewProcessMode;use crate::new_process_modal::NewProcessModal;use crate::tests::{init_test, init_test_workspace}; #[gpui::test]async fn test_debug_session_substitutes_variables_and_relativizes_paths( executor: BackgroundExecutor, cx: &mut TestAppContext,) { init_test(cx); let fs = FakeFs::new(executor.clone()); fs.insert_tree( path!("/project"), json!({ "main.rs": "fn main() {}" }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let test_variables = vec![( VariableName::WorktreeRoot, path!("/test/worktree/path").to_string(), )] .into_iter() .collect(); let task_context: SharedTaskContext = TaskContext { cwd: None, task_variables: test_variables, project_env: Default::default(), } .into(); let home_dir = paths::home_dir(); let test_cases: Vec<(&'static str, &'static str)> = vec![ // Absolute path - should not be relativized ( path!("/absolute/path/to/program"), path!("/absolute/path/to/program"), ), // Relative path - should be prefixed with worktree root ( format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(), path!("/test/worktree/path/src/program"), ), // Home directory path - should be expanded to full home directory path ( format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(), home_dir .join("src") .join("program") .to_string_lossy() .to_string() .leak(), ), // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending ( format!( "$ZED_WORKTREE_ROOT{0}src{0}program", std::path::MAIN_SEPARATOR ) .leak(), path!("/test/worktree/path/src/program"), ), ]; let called_launch = Arc::new(AtomicBool::new(false)); for (input_path, expected_path) in test_cases { let _subscription = project::debugger::test::intercept_debug_sessions(cx, { let called_launch = called_launch.clone(); move |client| { client.on_request::<dap::requests::Launch, _>({ let called_launch = called_launch.clone(); move |_, args| { let config = args.raw.as_object().unwrap(); assert_eq!( config["program"].as_str().unwrap(), expected_path, "Program path was not correctly substituted for input: {}", input_path ); assert_eq!( config["cwd"].as_str().unwrap(), expected_path, "CWD path was not correctly substituted for input: {}", input_path ); let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") { input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path")) } else { input_path.to_string() }; assert_eq!( config["otherField"].as_str().unwrap(), &expected_other_field, "Other field was incorrectly modified for input: {}", input_path ); called_launch.store(true, Ordering::SeqCst); Ok(()) } }); } }); let scenario = DebugScenario { adapter: "fake-adapter".into(), label: "test-debug-session".into(), build: None, config: json!({ "request": "launch", "program": input_path, "cwd": input_path, "otherField": input_path }), tcp_connection: None, }; workspace .update(cx, |multi, window, cx| { multi.workspace().update(cx, |workspace, cx| { workspace.start_debug_session( scenario, task_context.clone(), None, None, window, cx, ); }) }) .unwrap(); cx.run_until_parked(); assert!(called_launch.load(Ordering::SeqCst)); called_launch.store(false, Ordering::SeqCst); }} #[gpui::test]async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); fs.insert_tree( path!("/project"), json!({ "main.rs": "fn main() {}" }), ) .await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace .update(cx, |multi, window, cx| { multi.workspace().update(cx, |workspace, cx| { NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); }); }) .unwrap(); cx.run_until_parked(); let modal = workspace .update(cx, |workspace, _, cx| { workspace.active_modal::<NewProcessModal>(cx) }) .unwrap() .expect("Modal should be active"); modal.update_in(cx, |modal, window, cx| { modal.set_configure("/project/main", "/project", false, window, cx); modal.save_debug_scenario(window, cx); }); cx.executor().run_until_parked(); let editor = workspace .update(cx, |workspace, _window, cx| { workspace.active_item_as::<Editor>(cx).unwrap() }) .unwrap(); let debug_json_content = fs .load(path!("/project/.zed/debug.json").as_ref()) .await .expect("debug.json should exist") .lines() .filter(|line| !line.starts_with("//")) .collect::<Vec<_>>() .join("\n"); let expected_content = indoc::indoc! {r#" [ { "adapter": "fake-adapter", "label": "main (fake-adapter)", "request": "launch", "program": "/project/main", "cwd": "/project", "args": [], "env": {} } ]"#}; pretty_assertions::assert_eq!(expected_content, debug_json_content); editor.update(cx, |editor, cx| { assert_eq!( editor .selections .newest::<Point>(&editor.display_snapshot(cx)) .head(), Point::new(5, 2) ) }); modal.update_in(cx, |modal, window, cx| { modal.set_configure("/project/other", "/project", true, window, cx); modal.save_debug_scenario(window, cx); }); cx.executor().run_until_parked(); let expected_content = indoc::indoc! {r#" [ { "adapter": "fake-adapter", "label": "main (fake-adapter)", "request": "launch", "program": "/project/main", "cwd": "/project", "args": [], "env": {} }, { "adapter": "fake-adapter", "label": "other (fake-adapter)", "request": "launch", "program": "/project/other", "cwd": "/project", "args": [], "env": {} } ]"#}; let debug_json_content = fs .load(path!("/project/.zed/debug.json").as_ref()) .await .expect("debug.json should exist") .lines() .filter(|line| !line.starts_with("//")) .collect::<Vec<_>>() .join("\n"); pretty_assertions::assert_eq!(expected_content, debug_json_content);} #[gpui::test]async fn test_debug_modal_subtitles_with_multiple_worktrees( executor: BackgroundExecutor, cx: &mut TestAppContext,) { init_test(cx); let fs = FakeFs::new(executor.clone()); fs.insert_tree( path!("/workspace1"), json!({ ".zed": { "debug.json": r#"[ { "adapter": "fake-adapter", "label": "Debug App 1", "request": "launch", "program": "./app1", "cwd": "." }, { "adapter": "fake-adapter", "label": "Debug Tests 1", "request": "launch", "program": "./test1", "cwd": "." } ]"# }, "main.rs": "fn main() {}" }), ) .await; let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); workspace .update(cx, |multi, window, cx| { multi.workspace().update(cx, |workspace, cx| { NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); }); }) .unwrap(); cx.run_until_parked(); let modal = workspace .update(cx, |workspace, _, cx| { workspace.active_modal::<NewProcessModal>(cx) }) .unwrap() .expect("Modal should be active"); cx.executor().run_until_parked(); let subtitles = modal.update_in(cx, |modal, _, cx| { modal.debug_picker_candidate_subtitles(cx) }); assert_eq!( subtitles.as_slice(), [path!(".zed/debug.json"), path!(".zed/debug.json")] );} #[gpui::test]async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { init_test(cx); let mut expected_adapters = vec![ "CodeLLDB", "Debugpy", "JavaScript", "Delve", "GDB", "fake-adapter", ]; let adapter_names = cx.update(|cx| { let registry = DapRegistry::global(cx); registry.enumerate_adapters::<Vec<_>>() }); 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) .await .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 .request_kind(&debug_scenario.config) .await .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 );}
use dap::DapRegistry;
use editor::Editor;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs as _, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{
DebugRequest, DebugScenario, LaunchRequest, SharedTaskContext, TaskContext, VariableName,
ZedDebugConfig,
};
use text::Point;
use util::path;
use crate::NewProcessMode;
use crate::new_process_modal::NewProcessModal;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_debug_session_substitutes_variables_and_relativizes_paths(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let test_variables = vec![(
VariableName::WorktreeRoot,
path!("/test/worktree/path").to_string(),
)]
.into_iter()
.collect();
let task_context: SharedTaskContext = TaskContext {
cwd: None,
task_variables: test_variables,
project_env: Default::default(),
}
.into();
let home_dir = paths::home_dir();
let test_cases: Vec<(&'static str, &'static str)> = vec![
// Absolute path - should not be relativized
(
path!("/absolute/path/to/program"),
path!("/absolute/path/to/program"),
),
// Relative path - should be prefixed with worktree root
(
format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
path!("/test/worktree/path/src/program"),
),
// Home directory path - should be expanded to full home directory path
(
format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
home_dir
.join("src")
.join("program")
.to_string_lossy()
.to_string()
.leak(),
),
// Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
(
format!(
"$ZED_WORKTREE_ROOT{0}src{0}program",
std::path::MAIN_SEPARATOR
)
.leak(),
path!("/test/worktree/path/src/program"),
),
];
let called_launch = Arc::new(AtomicBool::new(false));
for (input_path, expected_path) in test_cases {
let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
let called_launch = called_launch.clone();
move |client| {
client.on_request::<dap::requests::Launch, _>({
let called_launch = called_launch.clone();
move |_, args| {
let config = args.raw.as_object().unwrap();
assert_eq!(
config["program"].as_str().unwrap(),
expected_path,
"Program path was not correctly substituted for input: {}",
input_path
);
assert_eq!(
config["cwd"].as_str().unwrap(),
expected_path,
"CWD path was not correctly substituted for input: {}",
input_path
);
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
} else {
input_path.to_string()
};
assert_eq!(
config["otherField"].as_str().unwrap(),
&expected_other_field,
"Other field was incorrectly modified for input: {}",
input_path
);
called_launch.store(true, Ordering::SeqCst);
Ok(())
}
});
}
});
let scenario = DebugScenario {
adapter: "fake-adapter".into(),
label: "test-debug-session".into(),
build: None,
config: json!({
"request": "launch",
"program": input_path,
"cwd": input_path,
"otherField": input_path
}),
tcp_connection: None,
};
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
workspace.start_debug_session(
scenario,
task_context.clone(),
None,
None,
window,
cx,
);
})
})
.unwrap();
cx.run_until_parked();
assert!(called_launch.load(Ordering::SeqCst));
called_launch.store(false, Ordering::SeqCst);
}
}
#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
})
.unwrap();
cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<NewProcessModal>(cx)
})
.unwrap()
.expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/main", "/project", false, window, cx);
modal.save_debug_scenario(window, cx);
});
cx.executor().run_until_parked();
let editor = workspace
.update(cx, |workspace, _window, cx| {
workspace.active_item_as::<Editor>(cx).unwrap()
})
.unwrap();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
pretty_assertions::assert_eq!(expected_content, debug_json_content);
editor.update(cx, |editor, cx| {
assert_eq!(
editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head(),
Point::new(5, 2)
)
});
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/other", "/project", true, window, cx);
modal.save_debug_scenario(window, cx);
});
cx.executor().run_until_parked();
let expected_content = indoc::indoc! {r#"
[
{
"adapter": "fake-adapter",
"label": "main (fake-adapter)",
"request": "launch",
"program": "/project/main",
"cwd": "/project",
"args": [],
"env": {}
},
{
"adapter": "fake-adapter",
"label": "other (fake-adapter)",
"request": "launch",
"program": "/project/other",
"cwd": "/project",
"args": [],
"env": {}
}
]"#};
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist")
.lines()
.filter(|line| !line.starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(expected_content, debug_json_content);
}
#[gpui::test]
async fn test_debug_modal_subtitles_with_multiple_worktrees(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/workspace1"),
json!({
".zed": {
"debug.json": r#"[
{
"adapter": "fake-adapter",
"label": "Debug App 1",
"request": "launch",
"program": "./app1",
"cwd": "."
},
{
"adapter": "fake-adapter",
"label": "Debug Tests 1",
"request": "launch",
"program": "./test1",
"cwd": "."
}
]"#
},
"main.rs": "fn main() {}"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |multi, window, cx| {
multi.workspace().update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
});
})
.unwrap();
cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<NewProcessModal>(cx)
})
.unwrap()
.expect("Modal should be active");
cx.executor().run_until_parked();
let subtitles = modal.update_in(cx, |modal, _, cx| {
modal.debug_picker_candidate_subtitles(cx)
});
assert_eq!(
subtitles.as_slice(),
[path!(".zed/debug.json"), path!(".zed/debug.json")]
);
}
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
init_test(cx);
let mut expected_adapters = vec![
"CodeLLDB",
"Debugpy",
"JavaScript",
"Delve",
"GDB",
"fake-adapter",
];
let adapter_names = cx.update(|cx| {
let registry = DapRegistry::global(cx);
registry.enumerate_adapters::<Vec<_>>()
});
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)
.await
.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
.request_kind(&debug_scenario.config)
.await
.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
);
}