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