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