1use dap::DapRegistry;
2use editor::Editor;
3use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
4use project::{FakeFs, Fs as _, Project};
5use serde_json::json;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
9use text::Point;
10use util::path;
11
12use crate::NewProcessMode;
13use crate::tests::{init_test, init_test_workspace};
14
15#[gpui::test]
16async fn test_debug_session_substitutes_variables_and_relativizes_paths(
17 executor: BackgroundExecutor,
18 cx: &mut TestAppContext,
19) {
20 init_test(cx);
21
22 let fs = FakeFs::new(executor.clone());
23 fs.insert_tree(
24 path!("/project"),
25 json!({
26 "main.rs": "fn main() {}"
27 }),
28 )
29 .await;
30
31 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
32 let workspace = init_test_workspace(&project, cx).await;
33 let cx = &mut VisualTestContext::from_window(*workspace, cx);
34
35 let test_variables = vec![(
36 VariableName::WorktreeRoot,
37 path!("/test/worktree/path").to_string(),
38 )]
39 .into_iter()
40 .collect();
41
42 let task_context = TaskContext {
43 cwd: None,
44 task_variables: test_variables,
45 project_env: Default::default(),
46 };
47
48 let home_dir = paths::home_dir();
49
50 let test_cases: Vec<(&'static str, &'static str)> = vec![
51 // Absolute path - should not be relativized
52 (
53 path!("/absolute/path/to/program"),
54 path!("/absolute/path/to/program"),
55 ),
56 // Relative path - should be prefixed with worktree root
57 (
58 format!(".{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
59 path!("/test/worktree/path/src/program"),
60 ),
61 // Home directory path - should be expanded to full home directory path
62 (
63 format!("~{0}src{0}program", std::path::MAIN_SEPARATOR).leak(),
64 home_dir
65 .join("src")
66 .join("program")
67 .to_string_lossy()
68 .to_string()
69 .leak(),
70 ),
71 // Path with $ZED_WORKTREE_ROOT - should be substituted without double appending
72 (
73 format!(
74 "$ZED_WORKTREE_ROOT{0}src{0}program",
75 std::path::MAIN_SEPARATOR
76 )
77 .leak(),
78 path!("/test/worktree/path/src/program"),
79 ),
80 ];
81
82 let called_launch = Arc::new(AtomicBool::new(false));
83
84 for (input_path, expected_path) in test_cases {
85 let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
86 let called_launch = called_launch.clone();
87 move |client| {
88 client.on_request::<dap::requests::Launch, _>({
89 let called_launch = called_launch.clone();
90
91 move |_, args| {
92 let config = args.raw.as_object().unwrap();
93
94 assert_eq!(
95 config["program"].as_str().unwrap(),
96 expected_path,
97 "Program path was not correctly substituted for input: {}",
98 input_path
99 );
100
101 assert_eq!(
102 config["cwd"].as_str().unwrap(),
103 expected_path,
104 "CWD path was not correctly substituted for input: {}",
105 input_path
106 );
107
108 let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
109 input_path
110 .replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
111 .to_owned()
112 } else {
113 input_path.to_string()
114 };
115
116 assert_eq!(
117 config["otherField"].as_str().unwrap(),
118 &expected_other_field,
119 "Other field was incorrectly modified for input: {}",
120 input_path
121 );
122
123 called_launch.store(true, Ordering::SeqCst);
124
125 Ok(())
126 }
127 });
128 }
129 });
130
131 let scenario = DebugScenario {
132 adapter: "fake-adapter".into(),
133 label: "test-debug-session".into(),
134 build: None,
135 config: json!({
136 "request": "launch",
137 "program": input_path,
138 "cwd": input_path,
139 "otherField": input_path
140 }),
141 tcp_connection: None,
142 };
143
144 workspace
145 .update(cx, |workspace, window, cx| {
146 workspace.start_debug_session(
147 scenario,
148 task_context.clone(),
149 None,
150 None,
151 window,
152 cx,
153 )
154 })
155 .unwrap();
156
157 cx.run_until_parked();
158
159 assert!(called_launch.load(Ordering::SeqCst));
160 called_launch.store(false, Ordering::SeqCst);
161 }
162}
163
164#[gpui::test]
165async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
166 init_test(cx);
167
168 let fs = FakeFs::new(executor.clone());
169 fs.insert_tree(
170 path!("/project"),
171 json!({
172 "main.rs": "fn main() {}"
173 }),
174 )
175 .await;
176
177 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
178 let workspace = init_test_workspace(&project, cx).await;
179 let cx = &mut VisualTestContext::from_window(*workspace, cx);
180
181 workspace
182 .update(cx, |workspace, window, cx| {
183 crate::new_process_modal::NewProcessModal::show(
184 workspace,
185 window,
186 NewProcessMode::Debug,
187 None,
188 cx,
189 );
190 })
191 .unwrap();
192
193 cx.run_until_parked();
194
195 let modal = workspace
196 .update(cx, |workspace, _, cx| {
197 workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
198 })
199 .unwrap()
200 .expect("Modal should be active");
201
202 modal.update_in(cx, |modal, window, cx| {
203 modal.set_configure("/project/main", "/project", false, window, cx);
204 modal.save_debug_scenario(window, cx);
205 });
206
207 cx.executor().run_until_parked();
208
209 let editor = workspace
210 .update(cx, |workspace, _window, cx| {
211 workspace.active_item_as::<Editor>(cx).unwrap()
212 })
213 .unwrap();
214
215 let debug_json_content = fs
216 .load(path!("/project/.zed/debug.json").as_ref())
217 .await
218 .expect("debug.json should exist")
219 .lines()
220 .filter(|line| !line.starts_with("//"))
221 .collect::<Vec<_>>()
222 .join("\n");
223
224 let expected_content = indoc::indoc! {r#"
225 [
226 {
227 "adapter": "fake-adapter",
228 "label": "main (fake-adapter)",
229 "request": "launch",
230 "program": "/project/main",
231 "cwd": "/project",
232 "args": [],
233 "env": {}
234 }
235 ]"#};
236
237 pretty_assertions::assert_eq!(expected_content, debug_json_content);
238
239 editor.update(cx, |editor, cx| {
240 assert_eq!(
241 editor.selections.newest::<Point>(cx).head(),
242 Point::new(5, 2)
243 )
244 });
245
246 modal.update_in(cx, |modal, window, cx| {
247 modal.set_configure("/project/other", "/project", true, window, cx);
248 modal.save_debug_scenario(window, cx);
249 });
250
251 cx.executor().run_until_parked();
252
253 let expected_content = indoc::indoc! {r#"
254 [
255 {
256 "adapter": "fake-adapter",
257 "label": "main (fake-adapter)",
258 "request": "launch",
259 "program": "/project/main",
260 "cwd": "/project",
261 "args": [],
262 "env": {}
263 },
264 {
265 "adapter": "fake-adapter",
266 "label": "other (fake-adapter)",
267 "request": "launch",
268 "program": "/project/other",
269 "cwd": "/project",
270 "args": [],
271 "env": {}
272 }
273 ]"#};
274
275 let debug_json_content = fs
276 .load(path!("/project/.zed/debug.json").as_ref())
277 .await
278 .expect("debug.json should exist")
279 .lines()
280 .filter(|line| !line.starts_with("//"))
281 .collect::<Vec<_>>()
282 .join("\n");
283 pretty_assertions::assert_eq!(expected_content, debug_json_content);
284}
285
286#[gpui::test]
287async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
288 init_test(cx);
289
290 let mut expected_adapters = vec![
291 "CodeLLDB",
292 "Debugpy",
293 "JavaScript",
294 "Delve",
295 "GDB",
296 "fake-adapter",
297 ];
298
299 let adapter_names = cx.update(|cx| {
300 let registry = DapRegistry::global(cx);
301 registry.enumerate_adapters::<Vec<_>>()
302 });
303
304 let zed_config = ZedDebugConfig {
305 label: "test_debug_session".into(),
306 adapter: "test_adapter".into(),
307 request: DebugRequest::Launch(LaunchRequest {
308 program: "test_program".into(),
309 cwd: None,
310 args: vec![],
311 env: Default::default(),
312 }),
313 stop_on_entry: Some(true),
314 };
315
316 for adapter_name in adapter_names {
317 let adapter_str = adapter_name.to_string();
318 if let Some(pos) = expected_adapters.iter().position(|&x| x == adapter_str) {
319 expected_adapters.remove(pos);
320 }
321
322 let adapter = cx
323 .update(|cx| {
324 let registry = DapRegistry::global(cx);
325 registry.adapter(adapter_name.as_ref())
326 })
327 .unwrap_or_else(|| panic!("Adapter {} should exist", adapter_name));
328
329 let mut adapter_specific_config = zed_config.clone();
330 adapter_specific_config.adapter = adapter_name.to_string().into();
331
332 let debug_scenario = adapter
333 .config_from_zed_format(adapter_specific_config)
334 .await
335 .unwrap_or_else(|_| {
336 panic!(
337 "Adapter {} should successfully convert from Zed format",
338 adapter_name
339 )
340 });
341
342 assert!(
343 debug_scenario.config.is_object(),
344 "Adapter {} should produce a JSON object for config",
345 adapter_name
346 );
347
348 let request_type = adapter
349 .request_kind(&debug_scenario.config)
350 .await
351 .unwrap_or_else(|_| {
352 panic!(
353 "Adapter {} should validate the config successfully",
354 adapter_name
355 )
356 });
357
358 match request_type {
359 dap::StartDebuggingRequestArgumentsRequest::Launch => {}
360 dap::StartDebuggingRequestArgumentsRequest::Attach => {
361 panic!(
362 "Expected Launch request but got Attach for adapter {}",
363 adapter_name
364 );
365 }
366 }
367 }
368
369 assert!(
370 expected_adapters.is_empty(),
371 "The following expected adapters were not found in the registry: {:?}",
372 expected_adapters
373 );
374}